diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 0000000..a9c6a75 --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,7 @@ +# Changesets + +> **Language / 语言:** [English](./README.md) | [简体中文](./README.zh-CN.md) + +Run `npm run changeset` for every user-visible package change. Commit the generated +markdown file with the implementation so release versioning and changelog updates +remain reviewable. diff --git a/.changeset/README.zh-CN.md b/.changeset/README.zh-CN.md new file mode 100644 index 0000000..75d7e90 --- /dev/null +++ b/.changeset/README.zh-CN.md @@ -0,0 +1,5 @@ +# Changesets + +> **Language / 语言:** [English](./README.md) | [简体中文](./README.zh-CN.md) + +每次用户可见的包变更请运行 `npm run changeset`。将生成的 markdown 文件与实现一并提交,以便 release 版本与 changelog 更新可审查。 diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000..2be13d4 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [] +} diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index cdda3c9..2f80f88 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -4,6 +4,8 @@ about: Report a problem in LeRobot Studio labels: bug --- +> **Language / 语言:** [English](./bug_report.md) | [简体中文](./bug_report.zh-CN.md) + ## Description ## Steps to reproduce diff --git a/.github/ISSUE_TEMPLATE/bug_report.zh-CN.md b/.github/ISSUE_TEMPLATE/bug_report.zh-CN.md new file mode 100644 index 0000000..4a6640d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.zh-CN.md @@ -0,0 +1,22 @@ +--- +name: 缺陷报告 +about: 报告 LeRobot Studio 中的问题 +labels: bug +--- + +> **Language / 语言:** [English](./bug_report.md) | [简体中文](./bug_report.zh-CN.md) + +## 描述 + +## 复现步骤 + +## 预期行为 + +## 实际行为 + +## 环境 + +- 操作系统: +- 浏览器: +- Node 版本: +- 包 / 提交: diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 22b7c11..e0924ce 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -4,6 +4,8 @@ about: Propose an improvement to LeRobot Studio labels: enhancement --- +> **Language / 语言:** [English](./feature_request.md) | [简体中文](./feature_request.zh-CN.md) + ## Problem to solve Describe the user need or workflow this would improve. diff --git a/.github/ISSUE_TEMPLATE/feature_request.zh-CN.md b/.github/ISSUE_TEMPLATE/feature_request.zh-CN.md new file mode 100644 index 0000000..cdb64f4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.zh-CN.md @@ -0,0 +1,23 @@ +--- +name: 功能建议 +about: 为 LeRobot Studio 提出改进建议 +labels: enhancement +--- + +> **Language / 语言:** [English](./feature_request.md) | [简体中文](./feature_request.zh-CN.md) + +## 要解决的问题 + +描述该改进将帮助的用户需求或工作流。 + +## 建议方案 + +描述你希望的行为或界面。 + +## 已考虑的替代方案 + +你尝试过哪些变通办法或其他方案? + +## 补充说明 + +如有需要,请附上截图、数据集格式/版本细节或链接。 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 144ec59..ae03964 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -3,7 +3,13 @@ ## Checklist - [ ] `npm run lint` passes +- [ ] `npm run typecheck` passes - [ ] `npm run test:unit` passes -- [ ] Relevant builds (`build:web` / `build:lib`) pass +- [ ] Relevant builds (`build` / `build:lib`) pass +- [ ] `npm run check:package` passes after `npm run build:lib` - [ ] Added a changeset when publishing API changes -- [ ] Updated docs when behavior or public API changed +- [ ] Updated the API report when the stable public API changed +- [ ] Updated docs (including `.zh-CN.md` pairs) when behavior or public API changed +- [ ] `npm run check:docs-i18n` passes + +> **Language / 语言:** [English](./PULL_REQUEST_TEMPLATE.md) | [简体中文](./PULL_REQUEST_TEMPLATE.zh-CN.md) diff --git a/.github/PULL_REQUEST_TEMPLATE.zh-CN.md b/.github/PULL_REQUEST_TEMPLATE.zh-CN.md new file mode 100644 index 0000000..a7d72d2 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.zh-CN.md @@ -0,0 +1,15 @@ +## 摘要 + +## 检查清单 + +- [ ] `npm run lint` 通过 +- [ ] `npm run typecheck` 通过 +- [ ] `npm run test:unit` 通过 +- [ ] 相关构建(`build` / `build:lib`)通过 +- [ ] 在 `npm run build:lib` 后 `npm run check:package` 通过 +- [ ] 发布 API 变更时已添加 changeset +- [ ] 稳定公开 API 变更时已更新 API 报告 +- [ ] 行为或公开 API 变更时已更新文档(含对应 `.zh-CN.md`) +- [ ] `npm run check:docs-i18n` 通过 + +> **Language / 语言:** [English](./PULL_REQUEST_TEMPLATE.md) | [简体中文](./PULL_REQUEST_TEMPLATE.zh-CN.md) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3157795..9e84c7d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,3 +9,18 @@ updates: directory: / schedule: interval: weekly + open-pull-requests-limit: 10 + groups: + github-actions: + patterns: + - '*' + - package-ecosystem: docker + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 + - package-ecosystem: docker + directory: /tests/npm-consumer + schedule: + interval: weekly + open-pull-requests-limit: 5 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eeab96c..66496da 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,16 @@ name: CI on: + workflow_call: push: branches: [main, master] pull_request: +permissions: + contents: read + concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} + group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: @@ -14,8 +18,10 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: '.nvmrc' cache: npm @@ -25,6 +31,8 @@ jobs: run: npm run fixtures:generate - name: Format check run: npm run format:check + - name: Check documentation i18n pairs + run: npm run check:docs-i18n - name: Lint run: npm run lint - name: Typecheck @@ -33,41 +41,70 @@ jobs: run: npm run test:coverage - name: Build library run: npm run build:lib - - name: Verify npm consumer typecheck - run: npm run verify:npm-consumer + - name: Install Chromium for npm consumer verification + run: npx playwright install --with-deps chromium + - name: Verify publishable package and API report + run: npm run check:package + - name: Validate CycloneDX SBOM generation + run: npm run sbom - name: Build web app run: npm run build + - name: Build documentation + run: npm run docs:build + - name: Check npm and web bundle sizes + run: npm run check:bundle-size browser-tests: runs-on: ubuntu-latest timeout-minutes: 60 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: '.nvmrc' cache: npm - run: npm ci - - run: npx playwright install --with-deps chromium + - name: Install browser engines + run: npx playwright install --with-deps chromium firefox webkit - name: Generate test fixtures run: npm run fixtures:generate - - run: npm run test:browser + - name: Chromium full browser suite + run: npm run test:browser + - name: Cross-browser compatibility smoke + run: npm run test:browser:smoke + + npm-consumer-docker: + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Verify npm tarball in isolated Docker consumers + run: npm run test:npm-consumer:docker official-compat: runs-on: ubuntu-latest timeout-minutes: 45 steps: - - uses: actions/checkout@v4 - - uses: actions/checkout@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: huggingface/lerobot - ref: adccdea1cfbec83ed98263feb7e59f7d047c5692 + ref: 7e241bd630a3719a56157a497ce5d08f244784f1 # v0.6.1 path: official-lerobot - - uses: actions/setup-node@v4 + persist-credentials: false + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: '.nvmrc' cache: npm - - uses: astral-sh/setup-uv@v6 + - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + enable-cache: true - run: npm ci - name: Generate test fixtures run: npm run fixtures:generate diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..733e289 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,45 @@ +name: CodeQL + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + schedule: + - cron: '17 4 * * 1' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: codeql-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + actions: read + contents: read + security-events: write + strategy: + fail-fast: false + matrix: + language: [javascript-typescript] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Initialize CodeQL + uses: github/codeql-action/init@d1ba80a13dd99fba24a470575428917156a28b43 # v4.37.5 + with: + languages: ${{ matrix.language }} + - name: Analyze + uses: github/codeql-action/analyze@d1ba80a13dd99fba24a470575428917156a28b43 # v4.37.5 + with: + category: /language:${{ matrix.language }} + # Private repos without GitHub Advanced Security cannot upload SARIF results. + upload: ${{ github.event.repository.visibility == 'public' }} diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..0814903 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,28 @@ +name: Dependency Review + +on: + pull_request: + branches: [main, master] + +permissions: + contents: read + +concurrency: + group: dependency-review-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + dependency-review: + # Dependency Review requires GitHub Advanced Security on private repositories. + if: github.event.repository.visibility == 'public' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Review dependency changes + uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 + with: + fail-on-severity: moderate + comment-summary-in-pr: never diff --git a/.github/workflows/docs-pages.yml b/.github/workflows/docs-pages.yml new file mode 100644 index 0000000..800e8e1 --- /dev/null +++ b/.github/workflows/docs-pages.yml @@ -0,0 +1,57 @@ +name: Documentation Pages + +on: + push: + branches: [main] + paths: + - 'docs/**' + - 'etc/lerobot-studio.api.md' + - 'package.json' + - 'package-lock.json' + - '.github/workflows/docs-pages.yml' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: '.nvmrc' + cache: npm + - run: npm ci + - name: Build documentation for project Pages + env: + DOCS_BASE: /${{ github.event.repository.name }}/ + run: npm run docs:build + - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 + with: + path: docs/.vitepress/dist + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + needs: build + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + pages: write + id-token: write + steps: + - uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 + - name: Deploy documentation + id: deployment + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index f56b9d6..1e5701b 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -5,32 +5,83 @@ on: - cron: '0 3 * * *' workflow_dispatch: +permissions: + contents: read + +concurrency: + group: nightly-${{ github.ref }} + cancel-in-progress: true + jobs: browser: runs-on: ubuntu-latest timeout-minutes: 60 steps: - - uses: actions/checkout@v4 - - uses: actions/checkout@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: huggingface/lerobot - ref: adccdea1cfbec83ed98263feb7e59f7d047c5692 + ref: 7e241bd630a3719a56157a497ce5d08f244784f1 # v0.6.1 path: official-lerobot - - uses: astral-sh/setup-uv@v6 - - uses: actions/setup-node@v4 + persist-credentials: false + - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + enable-cache: true + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: '.nvmrc' cache: npm - run: npm ci + - name: Generate test fixtures + run: npm run fixtures:generate - name: Install official LeRobot dataset reader working-directory: official-lerobot run: uv sync --extra dataset - - run: npx playwright install --with-deps chromium + - name: Install browser engines + run: npx playwright install --with-deps chromium firefox webkit - run: npm run test:unit - - run: npm run test:browser + - name: Chromium full browser suite + run: npm run test:browser + - name: Cross-browser compatibility smoke + run: npm run test:browser:smoke - name: Official LeRobot metadata compatibility run: npx vitest run tests/compat env: LEROBOT_PYTHON: ${{ github.workspace }}/official-lerobot/.venv/bin/python - run: npm run build - run: npm run build:lib + + upstream-main-compat: + name: Upstream main compatibility (warning) + continue-on-error: true + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: huggingface/lerobot + ref: main + path: official-lerobot + persist-credentials: false + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: '.nvmrc' + cache: npm + - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + enable-cache: true + - run: npm ci + - name: Generate test fixtures + run: npm run fixtures:generate + - name: Install upstream LeRobot dataset reader + working-directory: official-lerobot + run: uv sync --extra dataset + - name: Check compatibility with upstream main + env: + LEROBOT_PYTHON: ${{ github.workspace }}/official-lerobot/.venv/bin/python + run: npm run test:compat diff --git a/.github/workflows/release-dry-run.yml b/.github/workflows/release-dry-run.yml new file mode 100644 index 0000000..0156235 --- /dev/null +++ b/.github/workflows/release-dry-run.yml @@ -0,0 +1,47 @@ +name: Release Dry Run + +on: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: release-dry-run-${{ github.ref }} + cancel-in-progress: true + +jobs: + ci: + uses: ./.github/workflows/ci.yml + permissions: + contents: read + + artifacts: + needs: ci + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: '.nvmrc' + cache: npm + - run: npm ci + - run: npm run build:lib + - name: Validate npm package without publishing + run: npm publish --dry-run --access public + - name: Generate CycloneDX SBOM + run: npm run sbom + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + - name: Build container without pushing + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 + with: + context: . + push: false + provenance: mode=max + sbom: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d97e22f..d3c3edc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,27 +7,38 @@ on: workflow_dispatch: permissions: - contents: write - packages: write - id-token: write + contents: read + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false jobs: + ci: + uses: ./.github/workflows/ci.yml + permissions: + contents: read + release: + needs: ci runs-on: ubuntu-latest timeout-minutes: 60 + permissions: + contents: write + packages: write + id-token: write steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: '.nvmrc' cache: npm registry-url: https://registry.npmjs.org - run: npm ci - - run: npx playwright install --with-deps chromium - - run: npm run test:unit - - run: npm run test:browser - run: npm run build:lib - - run: npm run build + - run: npm run sbom - name: Verify tag matches npm package version env: TAG_VERSION: ${{ github.ref_name }} @@ -38,14 +49,23 @@ jobs: echo "Tag $TAG_VERSION must match package version $expected_tag" >&2 exit 1 } - - name: Publish npm package + - name: Bootstrap first npm publish + if: github.ref_name == 'v1.0.0' env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_BOOTSTRAP_TOKEN }} + run: | + test -n "$NODE_AUTH_TOKEN" || { + echo "NPM_BOOTSTRAP_TOKEN is required only for the first package publication" >&2 + exit 1 + } + npm publish --access public --provenance + - name: Publish npm package with trusted publishing + if: github.ref_name != 'v1.0.0' run: npm publish --access public --provenance - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - name: Login to GHCR - uses: docker/login-action@v3 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.actor }} @@ -54,15 +74,18 @@ jobs: id: meta run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" - name: Build and push Docker image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . push: true + provenance: mode=max + sbom: true platforms: linux/amd64,linux/arm64 tags: | ghcr.io/ioai-tech/lerobot-studio:latest ghcr.io/ioai-tech/lerobot-studio:${{ steps.meta.outputs.version }} - name: GitHub Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2 with: generate_release_notes: true + files: sbom.cdx.json diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 0000000..5326973 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,40 @@ +name: OpenSSF Scorecard + +on: + branch_protection_rule: + schedule: + - cron: '23 5 * * 2' + push: + branches: [main, master] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: scorecard-${{ github.ref }} + cancel-in-progress: true + +jobs: + scorecard: + if: github.event.repository.visibility == 'public' + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + id-token: write + security-events: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Run Scorecard analysis + uses: ossf/scorecard-action@55891bbd73f2425e97637d96e306fc9d491d0b21 # v2.4.4 + with: + results_file: results.sarif + results_format: sarif + publish_results: true + - name: Upload Scorecard results to code scanning + uses: github/codeql-action/upload-sarif@d1ba80a13dd99fba24a470575428917156a28b43 # v4.37.5 + with: + sarif_file: results.sarif diff --git a/.gitignore b/.gitignore index a89a4a1..07d29a3 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,8 @@ dist-ssr dist-lib dev-dist coverage +docs/.vitepress/cache +/temp/ *.local .env .env.* @@ -20,6 +22,7 @@ coverage .vitest-attachments/ tests/browser/__screenshots__/ *.tgz +sbom.cdx.json # Editor directories and files .vscode/* diff --git a/CHANGELOG.md b/CHANGELOG.md index dfac3a3..ffe243b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +> **Language / 语言:** [English](./CHANGELOG.md) | [简体中文](./CHANGELOG.zh-CN.md) + All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), @@ -10,11 +12,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Single-package source layout with logical `core`, `platform`, `ui`, `react`, and `web` boundaries -- Published package identity `@ioai/lerobot-studio` (MIT) +- Package identity `@ioai/lerobot-studio` (MIT) - Shared `ViewerLayout` for app and library shells - `LeRobotStudioProvider` public composition root - Minimal synthetic LeRobot v2/v3 fixtures under `tests/fixtures/datasets` - GitHub Actions CI, nightly browser tests, and release workflow (npm + GHCR) +- Governance, support, security response targets, privacy disclosure, + compatibility matrix, CODEOWNERS, and deprecation policy + +### Changed + +- Defined the stable 1.0 library contract as viewer-only, ESM/CSR-only, + browser-only, React `^19.0.0`, zero telemetry, and backward compatible within + major version 1 +- Defined exact LeRobot `v2.1` and `v3.0` support; other declared versions are + unknown rather than prefix-compatible +- Classified unknown-version read-only warnings, blocked export, browser + validation, consumer validation, and official training-ready export as 1.0 + release gates, not current verified capabilities ### Removed diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md new file mode 100644 index 0000000..fa4bd8c --- /dev/null +++ b/CHANGELOG.zh-CN.md @@ -0,0 +1,32 @@ +# 变更日志 + +> **Language / 语言:** [English](./CHANGELOG.md) | [简体中文](./CHANGELOG.zh-CN.md) + +本项目的所有重要变更将记录于此文件。 + +格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/), +本项目遵循 [Semantic Versioning](https://semver.org/lang/zh-CN/)。 + +## [1.0.0] - 未发布 + +### Added + +- 单包源码布局,逻辑边界为 `core`、`platform`、`ui`、`react`、`web` +- 包标识 `@ioai/lerobot-studio`(MIT) +- 应用与库外壳共享的 `ViewerLayout` +- 公开组合根 `LeRobotStudioProvider` +- `tests/fixtures/datasets` 下的最小合成 LeRobot v2/v3 fixture +- GitHub Actions CI、夜间浏览器测试与发布工作流(npm + GHCR) +- 治理、支持、安全响应目标、隐私披露、兼容性矩阵、CODEOWNERS 与弃用策略 + +### Changed + +- 将稳定 1.0 库契约定义为仅查看、仅 ESM/CSR、仅浏览器、React `^19.0.0`、零遥测,且在 major 1 内向后兼容 +- 定义精确支持 LeRobot `v2.1` 与 `v3.0`;其他声明版本视为未知,而非前缀兼容 +- 将未知版本只读警告、阻止导出、浏览器验证、消费者验证与官方训练就绪导出归类为 1.0 发布门禁,而非当前已验证能力 + +### Removed + +- 内部 GitLab CI / 私有部署配置 +- 开源 Web 外壳中的 Google Analytics 与 Microsoft Clarity +- 硬编码 monorepo `export` 复制脚本 diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..cd1ee19 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,13 @@ +# Replace this planned team if the public repository uses a different +# write-enabled maintainer team. GitHub does not enforce this file until the +# referenced team exists and branch protection requires CODEOWNERS review. +* @ioai-tech/maintainers + +/SECURITY.md @ioai-tech/maintainers +/GOVERNANCE.md @ioai-tech/maintainers +/CODE_OF_CONDUCT.md @ioai-tech/maintainers +/CODEOWNERS @ioai-tech/maintainers +/NOTICE @ioai-tech/maintainers +/docs/compatibility.md @ioai-tech/maintainers +/docs/deprecation.md @ioai-tech/maintainers +/docs/privacy.md @ioai-tech/maintainers diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 0b0a632..62e4858 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,5 +1,7 @@ # Contributor Covenant Code of Conduct +> **Language / 语言:** [English](./CODE_OF_CONDUCT.md) | [简体中文](./CODE_OF_CONDUCT.zh-CN.md) + ## Our Pledge We as members, contributors, and leaders pledge to make participation in our @@ -21,12 +23,21 @@ Examples of unacceptable behavior include: ## Enforcement -Report unacceptable behavior to the maintainers through -[GitHub Issues](https://github.com/ioai-tech/lerobot-studio/issues). Do not use -public issues to report security vulnerabilities; follow [SECURITY.md](./SECURITY.md) -instead. Maintainers will review reports and respond as appropriate. +Do not place sensitive conduct reports, personal information, or evidence in a +public issue. Email `io@io-ai.tech` with the subject “LeRobot Studio conduct +report”, or contact an IO-AI.TECH organization maintainer privately through +their GitHub profile. Share only the information needed to assess the report. + +Maintainers will protect reporter privacy as far as practical, disclose +information only to those needed for review, and avoid retaliation. A +maintainer who is involved in a report must recuse themselves. Responses may +include a warning, content removal, temporary restriction, permanent ban, or +referral to the relevant platform or authority. + +Do not use conduct reporting for security vulnerabilities; follow +[SECURITY.md](./SECURITY.md) instead. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), -version 2.1. +This Code of Conduct is adapted from the +[Contributor Covenant](https://www.contributor-covenant.org), version 2.1. diff --git a/CODE_OF_CONDUCT.zh-CN.md b/CODE_OF_CONDUCT.zh-CN.md new file mode 100644 index 0000000..55f7b95 --- /dev/null +++ b/CODE_OF_CONDUCT.zh-CN.md @@ -0,0 +1,33 @@ +# Contributor Covenant 行为准则 + +> **Language / 语言:** [English](./CODE_OF_CONDUCT.md) | [简体中文](./CODE_OF_CONDUCT.zh-CN.md) + +## 我们的承诺 + +作为成员、贡献者与领导者,我们承诺让每个人在社区中的参与体验都不受骚扰。 + +## 我们的标准 + +有助于积极环境的行为示例包括: + +- 展现同理心与友善 +- 尊重不同观点与经历 +- 给予并优雅地接受建设性反馈 + +不可接受的行为示例包括: + +- 骚扰、引战或侮辱性评论 +- 公开或私下骚扰 +- 未经同意发布他人私人信息 + +## 执行 + +请勿在公开 Issue 中放置敏感的 conduct 报告、个人信息或证据。请发送邮件至 `io@io-ai.tech`,主题注明 “LeRobot Studio conduct report”,或通过 GitHub 个人资料私下联系 IO-AI.TECH 组织维护者。仅分享评估报告所需的信息。 + +维护者会在实际可行范围内保护报告者隐私,仅向审查所需人员披露信息,并避免报复。涉及报告的维护者须回避。回应可能包括警告、内容移除、临时限制、永久封禁,或转交相关平台或机构。 + +请勿将 conduct 报告用于安全漏洞;请改用 [SECURITY.zh-CN.md](./SECURITY.zh-CN.md)。 + +## 归属 + +本行为准则改编自 [Contributor Covenant](https://www.contributor-covenant.org) 2.1 版。 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b4e5492..ddad4d3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,7 @@ # Contributing to LeRobot Studio +> **Language / 语言:** [English](./CONTRIBUTING.md) | [简体中文](./CONTRIBUTING.zh-CN.md) + ## Development setup Requirements: Node.js 24 and npm 11.7.0. @@ -10,11 +12,47 @@ npm run fixtures:generate npm run dev ``` -Before a pull request, run `npm run format:check`, `npm run lint`, `npm run typecheck`, `npm run test:unit`, and the relevant build target. +Before a pull request, run `npm run format:check`, `npm run lint`, +`npm run typecheck`, `npm run test:unit`, and the relevant build target. Run +browser, compatibility, and npm-consumer checks when the change affects those +surfaces. ## Architecture guidelines -The repository has one publishable package. Keep the dependency direction `web → react → platform → core`; UI is shared by React and the app. `core` remains free of React and browser APIs, while WASM, workers, and file-system integration stay in `src/platform`. The public npm API is `src/react/index.ts`. +The repository has one publishable package. Keep the dependency direction +`web → react → platform → core`; UI is shared by React and the app. `core` +remains free of React and browser APIs, while WASM, workers, and file-system +integration stay in `src/platform`. The public npm API is +`src/react/index.ts`. + +The stable 1.0 library is viewer-only, ESM/CSR-only, browser-only, and targets +React `^19.0.0`. Do not add editing or export services to the stable public API, +SSR/RSC paths, CommonJS output, telemetry, or undocumented network calls. + +## Compatibility and data integrity + +Read [Compatibility](./docs/compatibility.md) before changing loaders, +validators, media handling, export, packaging, or browser APIs. + +- Treat dataset input as untrusted and preserve source data. +- Support LeRobot `v2.1` and `v3.0` by exact declared version only. +- Keep unknown versions warning-marked and read-only; export must be blocked in + both UI and service layers. +- Do not call export supported until official LeRobot training-readiness tests + pass for the complete output. +- Add fixtures and regression tests for behavior changes. Record actual browser + versions for browser compatibility claims. + +## Pull requests + +Keep changes focused and explain user-visible behavior, compatibility impact, +test evidence, privacy/security impact, and migration steps. Public API changes +must follow the [deprecation policy](./docs/deprecation.md). A maintainer and +required CODEOWNERS approve changes; authors do not approve their own pull +requests. + +By contributing, you agree that your contribution is licensed under the +repository’s [MIT License](./LICENSE). ## Code of conduct @@ -23,3 +61,5 @@ Please follow [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md). ## Security Please report vulnerabilities via [SECURITY.md](./SECURITY.md). + +For usage questions and support boundaries, see [SUPPORT.md](./SUPPORT.md). diff --git a/CONTRIBUTING.zh-CN.md b/CONTRIBUTING.zh-CN.md new file mode 100644 index 0000000..2cbec89 --- /dev/null +++ b/CONTRIBUTING.zh-CN.md @@ -0,0 +1,48 @@ +# 为 LeRobot Studio 贡献 + +> **Language / 语言:** [English](./CONTRIBUTING.md) | [简体中文](./CONTRIBUTING.zh-CN.md) + +## 开发环境 + +要求:Node.js 24 与 npm 11.7.0。 + +```bash +npm ci +npm run fixtures:generate +npm run dev +``` + +提交 Pull Request 前,请运行 `npm run format:check`、`npm run lint`、 +`npm run typecheck`、`npm run test:unit` 以及相关的构建目标。若变更影响浏览器、兼容性或 npm 消费者,请一并运行对应检查。 + +## 架构准则 + +本仓库只有一个可发布包。保持依赖方向为 `web → react → platform → core`;UI 由 React 与应用共享。`core` 不得依赖 React 与浏览器 API;WASM、Worker 与文件系统集成留在 `src/platform`。公开 npm API 为 `src/react/index.ts`。 + +稳定 1.0 库仅提供查看能力、仅 ESM/CSR、仅浏览器,目标 React `^19.0.0`。请勿向稳定公开 API 添加编辑或导出服务、SSR/RSC 路径、CommonJS 输出、遥测或未记录的网络调用。 + +## 兼容性与数据完整性 + +在修改加载器、校验器、媒体处理、导出、打包或浏览器 API 前,请阅读 [兼容性](./docs/compatibility.zh-CN.md)。 + +- 将数据集输入视为不可信,并保护源数据不被修改。 +- 仅按声明的精确版本支持 LeRobot `v2.1` 与 `v3.0`。 +- 未知版本保持带警告的只读;在 UI 与服务层均须阻止导出。 +- 在官方 LeRobot 训练就绪测试对完整输出通过之前,不得宣称导出受支持。 +- 为行为变更添加 fixture 与回归测试;浏览器兼容性声明须记录实际浏览器版本。 + +## Pull Request + +保持变更聚焦,并说明用户可见行为、兼容性影响、测试证据、隐私/安全影响与迁移步骤。公开 API 变更须遵循 [弃用策略](./docs/deprecation.zh-CN.md)。须由维护者与必需的 CODEOWNERS 审批;作者不得自行批准自己的 PR。 + +贡献即表示你同意在仓库 [MIT 许可证](./LICENSE) 下授权你的贡献。 + +## 行为准则 + +请遵守 [CODE_OF_CONDUCT.zh-CN.md](./CODE_OF_CONDUCT.zh-CN.md)。 + +## 安全 + +请通过 [SECURITY.zh-CN.md](./SECURITY.zh-CN.md) 报告漏洞。 + +使用问题与支持边界见 [SUPPORT.zh-CN.md](./SUPPORT.zh-CN.md)。 diff --git a/Dockerfile b/Dockerfile index f679f2b..7a625c7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1 -FROM node:24-bookworm-slim AS build +FROM node:24-bookworm-slim@sha256:cd84903a12dbd26b46f1f3b8144a2568c41c5d37ddd0c7a80a34c7a19786b35f AS build WORKDIR /app ARG SITE_URL @@ -11,13 +11,23 @@ ENV VITE_SAMPLES_BASE_URL=${VITE_SAMPLES_BASE_URL} ENV VITE_BASE=${BASE_PATH} COPY package.json package-lock.json ./ -RUN npm ci +RUN --mount=type=cache,target=/root/.npm npm ci COPY . . RUN npm run build -FROM busybox:1.37 AS runtime +FROM busybox:1.37@sha256:9db7b59979c38555a39def84a31fb98b5296952f9e3afd4f6f11f05b07adfab0 AS runtime COPY --from=build /app/dist /dist -COPY docker/httpd.conf /etc/httpd.conf -EXPOSE 80 -HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 CMD wget -qO- http://127.0.0.1/ >/dev/null 2>&1 || exit 1 -CMD ["busybox", "httpd", "-f", "-p", "80", "-h", "/dist", "-c", "/etc/httpd.conf"] +# SPA index + MIME types for static assets (formerly docker/httpd.conf) +RUN printf '%s\n' \ + 'I:index.html' \ + '.wasm:application/wasm' \ + '.js:application/javascript' \ + '.mjs:application/javascript' \ + '.json:application/json' \ + '.map:application/json' \ + '.svg:image/svg+xml' \ + > /etc/httpd.conf +USER 65532:65532 +EXPOSE 8080 +HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 CMD wget -qO- http://127.0.0.1:8080/ >/dev/null 2>&1 || exit 1 +CMD ["busybox", "httpd", "-f", "-p", "8080", "-h", "/dist", "-c", "/etc/httpd.conf"] diff --git a/GOVERNANCE.md b/GOVERNANCE.md new file mode 100644 index 0000000..2f56d73 --- /dev/null +++ b/GOVERNANCE.md @@ -0,0 +1,73 @@ +# Governance + +> **Language / 语言:** [English](./GOVERNANCE.md) | [简体中文](./GOVERNANCE.zh-CN.md) + +LeRobot Studio is stewarded by IO-AI.TECH. This document defines project +decision-making and release authority; it does not grant legal authority on +behalf of the company. + +## Roles + +- **Contributors** propose issues, documentation, code, tests, and reviews. +- **Maintainers** triage issues, review and merge changes, enforce project + contracts, and manage community spaces. +- **Release maintainers** are a least-privilege subset of maintainers with npm, + GitHub release, container registry, and signing access. +- **Security maintainers** can access private security advisories and coordinate + disclosure. + +IO-AI.TECH appoints and removes maintainers based on sustained, constructive +participation, technical judgment, security practices, and availability. +Repository and team membership are the authoritative role records. + +## Decisions + +Routine changes use lazy consensus through reviewed pull requests. The author +does not approve their own change. Required checks and CODEOWNERS review must +pass before merge. + +Changes to the stable public API, dataset compatibility, privacy or telemetry +contract, security policy, licensing, governance, or release process require: + +1. a written proposal in an issue or pull request; +2. compatibility and migration analysis; and +3. approval from at least two maintainers, including a relevant CODEOWNER. + +When consensus cannot be reached, IO-AI.TECH’s designated project lead makes +the final decision and records the rationale. Security maintainers may act +privately and urgently to protect users. + +## Release authority + +Only release maintainers may publish npm packages, GitHub releases, tags, or +official container images. Production credentials must not be available to +untrusted pull-request jobs. Releases require: + +- a clean, reviewed commit on the protected release branch; +- passing required checks and acceptance gates; +- an updated changelog and version/tag agreement; +- provenance and artifact review where the release platform supports them; and +- approval by two maintainers, at least one of whom is a release maintainer. + +Published versions and tags are immutable. A faulty release is followed by a +new version; tags and registry artifacts are not silently replaced. Security +releases may use an expedited private review while retaining two-person +approval. + +The stable `1.0.0` release must satisfy +[Compatibility and release gates](./docs/compatibility.md), including official +LeRobot training-readiness validation for every advertised export path. + +## Compatibility and deprecation + +Major version 1 maintains backward compatibility for documented public APIs. +Deprecations follow [docs/deprecation.md](./docs/deprecation.md). Exact LeRobot +version support and the zero-telemetry npm contract cannot be weakened in a +minor or patch release. + +## Conduct, security, and conflicts + +Community participation follows [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md). +Vulnerabilities follow [SECURITY.md](./SECURITY.md). Maintainers disclose +material conflicts of interest and recuse themselves when impartial review is +not possible. diff --git a/GOVERNANCE.zh-CN.md b/GOVERNANCE.zh-CN.md new file mode 100644 index 0000000..a5efc9c --- /dev/null +++ b/GOVERNANCE.zh-CN.md @@ -0,0 +1,48 @@ +# 治理 + +> **Language / 语言:** [English](./GOVERNANCE.md) | [简体中文](./GOVERNANCE.zh-CN.md) + +LeRobot Studio 由 IO-AI.TECH stewardship。本文档定义项目决策与发布权限;不代表公司授予的法律授权。 + +## 角色 + +- **贡献者**:提出 Issue、文档、代码、测试与审查。 +- **维护者**:分流 Issue、审查并合并变更、执行项目契约、管理社区空间。 +- **发布维护者**:维护者中权限最小化的子集,拥有 npm、GitHub release、容器 registry 与签名访问权。 +- **安全维护者**:可访问私有安全公告并协调披露。 + +IO-AI.TECH 根据持续、建设性的参与、技术判断、安全实践与可用性任命与撤换维护者。仓库与团队成员身份是角色的权威记录。 + +## 决策 + +常规变更通过已审查 Pull Request 的懒共识(lazy consensus)进行。作者不得自行批准自己的变更。合并前须通过必需检查与 CODEOWNERS 审查。 + +对稳定公开 API、数据集兼容性、隐私或遥测契约、安全策略、许可证、治理或发布流程的变更需要: + +1. 在 Issue 或 Pull Request 中的书面提案; +2. 兼容性与迁移分析;以及 +3. 至少两名维护者批准,其中须包含相关 CODEOWNER。 + +无法达成共识时,IO-AI.TECH 指定的项目负责人做最终决定并记录理由。安全维护者可私下、紧急地行动以保护用户。 + +## 发布权限 + +仅发布维护者可发布 npm 包、GitHub release、标签或官方容器镜像。生产凭据不得对不受信任的 Pull Request 作业可用。发布要求: + +- 受保护发布分支上的干净、已审查提交; +- 通过必需检查与验收门禁; +- 更新的 changelog 与版本/标签一致; +- 在发布平台支持时审查来源与制品;以及 +- 两名维护者批准,其中至少一人为发布维护者。 + +已发布版本与标签不可变。有问题的发布以后续新版本跟进;不得静默替换标签与 registry 制品。安全发布可在保留双人批准的同时采用加急私有审查。 + +稳定 `1.0.0` 发布须满足 [兼容性与发布门禁](./docs/compatibility.zh-CN.md),包括对所 advertised 的每条导出路径的官方 LeRobot 训练就绪验证。 + +## 兼容性与弃用 + +Major 1 对已文档化的公开 API 保持向后兼容。弃用遵循 [docs/deprecation.zh-CN.md](./docs/deprecation.zh-CN.md)。精确的 LeRobot 版本支持与零遥测 npm 契约不得在 minor 或 patch 中削弱。 + +## 行为、安全与利益冲突 + +社区参与遵循 [CODE_OF_CONDUCT.zh-CN.md](./CODE_OF_CONDUCT.zh-CN.md)。漏洞遵循 [SECURITY.zh-CN.md](./SECURITY.zh-CN.md)。维护者须披露重大利益冲突,并在无法 impartial 审查时回避。 diff --git a/NOTICE b/NOTICE index 4240a76..3092333 100644 --- a/NOTICE +++ b/NOTICE @@ -5,7 +5,8 @@ This repository depends on third-party packages. The exact dependency graph, versions, package registry locations, integrity hashes, and SPDX license identifiers are recorded in `package-lock.json`. -The following non-MIT license identifiers are present in the current lockfile: +Examples of non-MIT license identifiers present in the current lockfile +include: - `@fontsource-variable/geist` 5.3.0 — OFL-1.1 - `argparse` 2.0.1 — Python-2.0 @@ -15,7 +16,9 @@ The following non-MIT license identifiers are present in the current lockfile: - `spawndamnit` 3.0.1 — `SEE LICENSE IN LICENSE` - `tslib` 2.8.1 — 0BSD -Transitive copies of the packages above may also appear in the lockfile. This -notice is an index, not a replacement for the applicable third-party license -texts. Before distributing a built artifact, regenerate and review the -dependency inventory for the lockfile used to produce that artifact. +This list is representative, not exhaustive; development tools, optional +platform packages, and transitive dependencies may use additional licenses. +This notice is an index, not a replacement for the applicable third-party +license texts. Before distributing a built artifact, generate and review a +complete production-artifact dependency and asset inventory from the exact +lockfile and build output used for that release. diff --git a/README.md b/README.md index 4d50070..1266531 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,31 @@ # LeRobot Studio -Browser-based visualizer for [LeRobot](https://github.com/huggingface/lerobot) datasets, by [IO-AI.TECH](https://io-ai.tech). +> **Language / 语言:** [English](./README.md) | [简体中文](./README.zh-CN.md) + +Browser-based viewer for [LeRobot](https://github.com/huggingface/lerobot) +datasets, by [IO-AI.TECH](https://io-ai.tech). Live demo: [https://lerobot.studio](https://lerobot.studio) -- Open local folders, local archives, or remote archives (CORS + Range required) -- Synced video / charts / raw feature inspection -- Export and light editing workflows -- Embeddable React package: `@ioai/lerobot-studio` +This repository is preparing the stable `1.0.0` contract. Viewing is the only +stable public library workflow: the package does not mutate source datasets and +does not expose an editor API. Export in the standalone application is not part +of the stable library API and remains a **1.0 release gate** until its output is +validated as directly consumable by official LeRobot training. + +- Open local folders, local archives, or remote archives +- Inspect synchronized video, charts, and raw features +- Embed the client-only React viewer through `@ioai/lerobot-studio` +- Process dataset content locally in the browser ## Open source -LeRobot Studio is developed in the open at -[ioai-tech/lerobot-studio](https://github.com/ioai-tech/lerobot-studio). +LeRobot Studio is preparing for public development at +`github.com/ioai-tech/lerobot-studio`. That public repository and its issue +tracker are part of the GitHub migration and may not be available yet. -- Report reproducible bugs or propose improvements through - [GitHub Issues](https://github.com/ioai-tech/lerobot-studio/issues). +- After publication, report reproducible bugs or propose improvements through + the repository’s GitHub Issues. - Read [CONTRIBUTING.md](./CONTRIBUTING.md) before submitting a pull request. - Follow the [Code of Conduct](./CODE_OF_CONDUCT.md) in all community spaces. - Report security vulnerabilities only through the private process in @@ -40,9 +50,12 @@ Docker: ```bash docker build -t lerobot-studio . -docker run --rm -p 8080:80 lerobot-studio +docker run --rm -p 8080:8080 lerobot-studio ``` +Then open [http://localhost:8080](http://localhost:8080). For production +hosting, see [Deployment](./docs/deployment.md). + ## Repository layout ``` @@ -57,6 +70,8 @@ docs/ Architecture and ops docs ## Library usage +After the `1.0.0` package is published: + ```bash npm install @ioai/lerobot-studio ``` @@ -66,29 +81,98 @@ import { LeRobotViewer } from '@ioai/lerobot-studio'; import '@ioai/lerobot-studio/style.css'; export function App() { - return ; + return ( +
+ +
+ ); } ``` +The host must give the viewer an explicit height; the component fills its +container and does not choose a page height. The `1.0.0` compatibility contract +is React and React DOM `^19.0.0`, ESM-only, browser-only, and CSR-only. Vite and +Next.js client-only consumer validation remains a 1.0 release gate. + +### Vite + +Import and render the component normally in client code. Do not externalize its +CSS, workers, or WASM assets from the browser build. + +### Next.js + +Load the viewer from a Client Component with SSR disabled: + +```tsx +'use client'; + +import dynamic from 'next/dynamic'; + +const Viewer = dynamic( + () => import('@ioai/lerobot-studio').then((module) => module.LeRobotViewer), + { ssr: false }, +); +``` + +LeRobot Studio does not support React Server Components, server rendering, +CommonJS, or execution in Node.js. + +## Compatibility contract + +- Stable line: `1.0.0`; minor and patch releases remain backward compatible + within major version 1, subject to the [deprecation policy](./docs/deprecation.md). +- Exact 1.0 dataset support targets LeRobot `v2.1` and `v3.0`. Prefix-compatible + variants such as `v2.0`, `v2.x`, or `v3.x` are not covered. Open and inspect + behavior for exact versions is **automated CI verified**; the final manual + release gate for `1.0.0` is still pending. +- Unknown versions open in warning-marked read-only mode when safely parseable, + with export disabled. Automated CI and browser suites verify this behavior; + the final manual release gate for `1.0.0` is still pending. +- Export for supported versions is verified against official LeRobot readers in + CI, but must not be advertised as generally supported until maintainers + complete the final manual release gate. No best-effort export is considered + supported. +- The npm library ships with zero telemetry. It must not send analytics, + diagnostics, dataset metadata, filenames, or usage events. + +See [Compatibility and release gates](./docs/compatibility.md) for browser, +dataset, network, and capability matrices. + ## Documentation +- [Quick Start](./docs/quick-start.md) +- [Stable API](./docs/api.md) +- [Data formats](./docs/data-formats.md) +- [CORS and HTTP Range](./docs/cors.md) +- [Browser support](./docs/browser.md) +- [Troubleshooting](./docs/troubleshooting.md) +- [Migration](./docs/migration.md) - [Architecture](./docs/architecture.md) +- [Compatibility and release gates](./docs/compatibility.md) - [Development](./docs/development.md) - [Deployment](./docs/deployment.md) +- [Privacy](./docs/privacy.md) +- [Deprecation policy](./docs/deprecation.md) - [GitHub migration checklist](./docs/github-migration.md) - [Contributing](./CONTRIBUTING.md) +- [Governance](./GOVERNANCE.md) +- [Support](./SUPPORT.md) - [Security](./SECURITY.md) - [Third-party notices](./NOTICE) ## Deploy -The public site at [https://lerobot.studio](https://lerobot.studio) is deployed by -Cloudflare Workers Builds connected to this GitHub repository (`main` branch). -See [Deployment](./docs/deployment.md). +The public site is [https://lerobot.studio](https://lerobot.studio). Connecting +the migrated GitHub `main` branch to Cloudflare Workers Builds is the planned +production path and must be verified during migration. See +[Deployment](./docs/deployment.md). ## Privacy -The open-source distribution does not ship third-party analytics (no Google Analytics / Clarity). +The npm library has a zero-telemetry contract. The application uses browser +storage for preferences, recent-source metadata, and—when the browser permits +it—local file handles. Dataset bytes are processed locally unless the user +opens a remote URL. See [Privacy](./docs/privacy.md). ## License diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 0000000..86c306b --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,147 @@ +# LeRobot Studio + +> **Language / 语言:** [English](./README.md) | [简体中文](./README.zh-CN.md) + +基于浏览器的 [LeRobot](https://github.com/huggingface/lerobot) 数据集查看器,由 [IO-AI.TECH](https://io-ai.tech) 维护。 + +在线演示:[https://lerobot.studio](https://lerobot.studio) + +本仓库正在准备稳定的 `1.0.0` 契约。查看(viewing)是唯一稳定的公开库工作流:包不会修改源数据集,也不暴露编辑器 API。独立应用中的导出不属于稳定库 API,在官方 LeRobot 训练流程验证其输出可直接消费之前,导出仍是一项 **1.0 发布门禁**。 + +- 打开本地文件夹、本地归档或远程归档 +- 检查同步的视频、图表与原始特征 +- 通过 `@ioai/lerobot-studio` 嵌入纯客户端 React 查看器 +- 在浏览器本地处理数据集内容 + +## 开源 + +LeRobot Studio 正在准备于 `github.com/ioai-tech/lerobot-studio` 进行公开开发。该公开仓库及其 Issue 跟踪器是 GitHub 迁移计划的一部分,可能尚未可用。 + +- 公开后,请通过仓库的 GitHub Issues 报告可复现缺陷或提出改进建议。 +- 提交 Pull Request 前请阅读 [CONTRIBUTING.md](./CONTRIBUTING.zh-CN.md)。 +- 在所有社区空间中遵守 [行为准则](./CODE_OF_CONDUCT.zh-CN.md)。 +- 安全漏洞仅通过 [SECURITY.md](./SECURITY.zh-CN.md) 中的私有流程报告。 + +## 快速开始 + +```bash +npm ci +npm run fixtures:generate +npm run dev +``` + +生产构建: + +```bash +npm run build +npm run preview +``` + +Docker: + +```bash +docker build -t lerobot-studio . +docker run --rm -p 8080:8080 lerobot-studio +``` + +然后打开 [http://localhost:8080](http://localhost:8080)。生产托管见 [部署](./docs/deployment.zh-CN.md)。 + +## 仓库结构 + +``` +src/core 领域逻辑(无 React) +src/platform 浏览器/WASM 适配器与 Worker +src/ui 共享 UI 基元 +src/react 已发布的 React 库 API +src/web 独立 Web 应用外壳 +tests/ 单元测试 + 浏览器测试 +docs/ 架构与运维文档 +``` + +## 库用法 + +`1.0.0` 包发布后: + +```bash +npm install @ioai/lerobot-studio +``` + +```tsx +import { LeRobotViewer } from '@ioai/lerobot-studio'; +import '@ioai/lerobot-studio/style.css'; + +export function App() { + return ( +
+ +
+ ); +} +``` + +宿主必须为查看器提供明确高度;组件会填满容器,不会自行决定页面高度。`1.0.0` 兼容性契约为 React 与 React DOM `^19.0.0`、仅 ESM、仅浏览器、仅 CSR。Vite 与 Next.js 纯客户端消费者验证仍是 1.0 发布门禁。 + +### Vite + +在客户端代码中正常导入并渲染组件。不要在浏览器构建中将其 CSS、Worker 或 WASM 资源外部化。 + +### Next.js + +从 Client Component 加载查看器,并禁用 SSR: + +```tsx +'use client'; + +import dynamic from 'next/dynamic'; + +const Viewer = dynamic( + () => import('@ioai/lerobot-studio').then((module) => module.LeRobotViewer), + { ssr: false }, +); +``` + +LeRobot Studio 不支持 React Server Components、服务端渲染、CommonJS 或在 Node.js 中执行。 + +## 兼容性契约 + +- 稳定线:`1.0.0`;在 major 1 内,minor 与 patch 保持向后兼容,受 [弃用策略](./docs/deprecation.zh-CN.md) 约束。 +- 1.0 精确数据集支持目标为 LeRobot `v2.1` 与 `v3.0`。前缀兼容变体如 `v2.0`、`v2.x` 或 `v3.x` 不在覆盖范围内。精确版本的打开与检查行为**已在自动化 CI 中验证**;`1.0.0` 的最终人工发布门禁仍待完成。 +- 未知版本在可安全解析时以带警告的只读模式打开,且导出禁用。该行为已在自动化 CI 与浏览器套件中验证;`1.0.0` 的最终人工发布门禁仍待完成。 +- 受支持版本的导出已在 CI 中对照官方 LeRobot reader 验证,但在维护者完成最终人工发布门禁之前,不得宣称为普遍受支持。任何“尽力而为”的导出均不视为受支持。 +- npm 库为零遥测契约:不得发送分析、诊断、数据集元数据、文件名或使用事件。 + +浏览器、数据集、网络与能力矩阵见 [兼容性与发布门禁](./docs/compatibility.zh-CN.md)。 + +## 文档 + +- [快速开始](./docs/quick-start.zh-CN.md) +- [稳定 API](./docs/api.zh-CN.md) +- [数据格式](./docs/data-formats.zh-CN.md) +- [CORS 与 HTTP Range](./docs/cors.zh-CN.md) +- [浏览器支持](./docs/browser.zh-CN.md) +- [故障排查](./docs/troubleshooting.zh-CN.md) +- [迁移](./docs/migration.zh-CN.md) +- [架构](./docs/architecture.zh-CN.md) +- [兼容性与发布门禁](./docs/compatibility.zh-CN.md) +- [开发](./docs/development.zh-CN.md) +- [部署](./docs/deployment.zh-CN.md) +- [隐私](./docs/privacy.zh-CN.md) +- [弃用策略](./docs/deprecation.zh-CN.md) +- [GitHub 迁移清单](./docs/github-migration.zh-CN.md) +- [贡献指南](./CONTRIBUTING.zh-CN.md) +- [治理](./GOVERNANCE.zh-CN.md) +- [支持](./SUPPORT.zh-CN.md) +- [安全](./SECURITY.zh-CN.md) +- [第三方声明](./NOTICE) + +## 部署 + +公开站点为 [https://lerobot.studio](https://lerobot.studio)。将迁移后的 GitHub `main` 分支接入 Cloudflare Workers Builds 是计划中的生产路径,须在迁移期间验证。见 [部署](./docs/deployment.zh-CN.md)。 + +## 隐私 + +npm 库为零遥测契约。应用使用浏览器存储保存偏好、最近来源元数据,以及在浏览器允许时保存本地文件句柄。除非用户打开远程 URL,数据集字节均在本地处理。见 [隐私](./docs/privacy.zh-CN.md)。 + +## 许可证 + +[MIT](./LICENSE) — Copyright (c) 2026 IO-AI.TECH diff --git a/SECURITY.md b/SECURITY.md index 9a2541c..da1ab85 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,17 +1,24 @@ # Security Policy +> **Language / 语言:** [English](./SECURITY.md) | [简体中文](./SECURITY.zh-CN.md) + ## Supported versions -Security fixes are accepted for the latest released major version of -`@ioai/lerobot-studio`. +After `1.0.0` is released, security fixes are provided for the latest minor +release in major version 1. Older minors may receive a coordinated upgrade path +rather than a backport. Unreleased branches, development snapshots, forks, +third-party deployments, and modified distributions receive best-effort +support only. ## Reporting a vulnerability Please **do not** open a public issue for security vulnerabilities. -Submit a [private GitHub security advisory](https://github.com/ioai-tech/lerobot-studio/security/advisories/new). -This is the only supported reporting channel; this repository does not publish a -security email address. +After the public GitHub migration, submit a private GitHub security advisory +from the repository’s **Security → Advisories → Report a vulnerability** page. +Until that repository and advisory form are available, email +`io@io-ai.tech` with the subject “LeRobot Studio security report”. Do not send +secrets or sensitive dataset content in the first message. Include: @@ -20,5 +27,57 @@ Include: - Impact assessment - Any known mitigations -Maintainers will use the private advisory to coordinate follow-up and -disclosure. +Do not include sensitive dataset content unless maintainers request a minimal, +sanitized reproducer through the private advisory. + +## Response targets + +These are service targets, not guarantees: + +- acknowledgment within 3 business days; +- initial severity and scope assessment within 7 business days; +- a private status update at least every 7 business days while an accepted + report remains unresolved; +- critical-severity remediation target: 7 calendar days after confirmation; +- high-severity remediation target: 30 calendar days after confirmation; and +- moderate/low-severity remediation target: the next appropriate scheduled + release. + +Complex dependency, browser, or coordinated-disclosure cases may take longer. +Maintainers will explain material delays in the private advisory. + +## Disclosure and release + +Maintainers validate the report, assign severity based on practical impact, +prepare fixes and advisories privately, and coordinate disclosure with the +reporter. Please allow a reasonable remediation period before public +disclosure. Security releases may shorten the normal deprecation window. + +The project will publish an advisory when users need to act and will credit the +reporter unless anonymity is requested. Release authority follows +[GOVERNANCE.md](./GOVERNANCE.md). + +## Supply-chain security + +Pull requests are checked by CI, dependency review, and CodeQL. Scheduled +OpenSSF Scorecard analysis reports repository-level supply-chain findings. +Workflow permissions are read-only by default; jobs that upload security +results or publish releases receive only their required permissions. Actions +outside the release workflow are pinned to full commit SHAs and tracked by +Dependabot. + +Repository administrators must separately enable and verify the GitHub security +features and branch rules described in +[docs/github-migration.md](./docs/github-migration.md). Checked-in workflows do +not prove that those settings are active. Report a suspected compromised +dependency, action, package, release artifact, or maintainer credential through +the private vulnerability channel above. + +## Scope + +In scope are vulnerabilities in this repository’s distributed web application +and npm package, including unsafe parsing of untrusted datasets, cross-origin or +storage boundary violations, unintended data disclosure, dependency or build +compromise, and bypass of read-only/export restrictions. Service availability, +remote dataset hosts, host-application code, unsupported browsers, and social +engineering are normally out of scope unless they demonstrate a project defect. diff --git a/SECURITY.zh-CN.md b/SECURITY.zh-CN.md new file mode 100644 index 0000000..7bfafb2 --- /dev/null +++ b/SECURITY.zh-CN.md @@ -0,0 +1,51 @@ +# 安全策略 + +> **Language / 语言:** [English](./SECURITY.md) | [简体中文](./SECURITY.zh-CN.md) + +## 受支持版本 + +`1.0.0` 发布后,major 1 的最新 minor 版本会获得安全修复。较旧的 minor 可能提供协调升级路径而非 backport。未发布分支、开发快照、Fork、第三方部署与修改过的发行版仅尽力支持。 + +## 报告漏洞 + +请 **不要** 为安全漏洞公开开 Issue。 + +公开 GitHub 迁移完成后,请从仓库 **Security → Advisories → Report a vulnerability** 提交私有 GitHub 安全公告。在仓库与公告表单可用之前,请发送邮件至 `io@io-ai.tech`,主题注明 “LeRobot Studio security report”。首次邮件中请勿包含密钥或敏感数据集内容。 + +请包含: + +- 问题描述 +- 复现步骤 +- 影响评估 +- 已知缓解措施 + +除非维护者通过私有公告请求最小化、脱敏的复现材料,否则请勿包含敏感数据集内容。 + +## 响应目标 + +以下为服务目标,非保证: + +- 3 个工作日内确认收到; +- 7 个工作日内完成初步严重性与范围评估; +- 对已接受的报告,在未解决期间至少每 7 个工作日提供一次私有状态更新; +- 严重(critical)级别:确认后 7 个日历日内修复; +- 高(high)级别:确认后 30 个日历日内修复; +- 中/低级别:在下一个合适的计划发布中修复。 + +复杂的依赖、浏览器或协调披露案例可能耗时更长。维护者会在私有公告中说明重大延迟。 + +## 披露与发布 + +维护者会验证报告、按实际影响定级、在私有环境中准备修复与公告,并与报告者协调披露时间。请在公开披露前留出合理的修复窗口。安全发布可能缩短常规弃用窗口。 + +当用户需要采取行动时,项目会发布公告,并在报告者未要求匿名时致谢。发布权限遵循 [GOVERNANCE.zh-CN.md](./GOVERNANCE.zh-CN.md)。 + +## 供应链安全 + +Pull Request 会经 CI、依赖审查与 CodeQL 检查。OpenSSF Scorecard 定时分析会报告仓库级供应链发现。工作流权限默认为只读;仅上传安全结果或发布 release 的作业获得所需权限。release 工作流外的 Actions 均 pin 到完整 commit SHA,并由 Dependabot 跟踪。 + +仓库管理员须单独启用并验证 [docs/github-migration.zh-CN.md](./docs/github-migration.zh-CN.md) 中描述的 GitHub 安全功能与分支规则。仅检查入库的工作流并不能证明这些设置已生效。若怀疑依赖、Action、包、发布制品或维护者凭据被 compromise,请通过上述私有漏洞渠道报告。 + +## 范围 + +范围内包括本仓库分发的 Web 应用与 npm 包中的漏洞,例如对不可信数据集的不安全解析、跨源或存储边界违规、非预期数据泄露、依赖或构建 compromise,以及绕过只读/导出限制。服务可用性、远程数据集宿主、宿主应用代码、不受支持的浏览器与社会工程学通常不在范围内,除非能证明是项目缺陷。 diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 0000000..3ae3596 --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,53 @@ +# Support + +> **Language / 语言:** [English](./SUPPORT.md) | [简体中文](./SUPPORT.zh-CN.md) + +LeRobot Studio is community-supported open-source software. No response time, +availability, compatibility, or remediation commitment is created by this +document. Security reports use the separate targets in +[SECURITY.md](./SECURITY.md). + +## Where to ask + +- Reproducible product defects: after the public migration, use GitHub Issues + in `ioai-tech/lerobot-studio` +- Feature and compatibility proposals: after the public migration, use GitHub + Issues with the use case, dataset version, and migration impact +- Vulnerabilities: private reporting only through + [SECURITY.md](./SECURITY.md) +- Contributions: [CONTRIBUTING.md](./CONTRIBUTING.md) + +The public issue tracker is not considered available until the GitHub migration +checklist is complete. Before opening an issue, search existing reports and +test the latest supported release. Include the LeRobot `codebase_version`, +browser and OS versions, input type, minimal reproduction, console errors, and +whether the issue occurs in the standalone app or npm package. Remove +credentials, signed URLs, personal data, and confidential dataset content. + +## Supported boundary + +For stable 1.0, support is limited to the documented viewer API, exact LeRobot +`v2.1` and `v3.0`, supported browser/input combinations, and documented +deployment behavior. See [Compatibility](./docs/compatibility.md). + +The following are outside community support unless a project defect can be +reproduced within that boundary: + +- custom or unknown LeRobot formats and manually repaired datasets; +- SSR, React Server Components, CommonJS, Node.js execution, or React versions + outside the documented range; +- host-application code, authentication, proxies, CORS configuration, remote + storage availability, and browser codec availability; +- forks, private patches, unsupported releases, and third-party deployments; +- dataset recovery, annotation, training, model quality, or robotics advice; + and +- guaranteed export before official training-readiness validation is complete. + +Unknown dataset versions are not supported formats. The 1.0 target is safe, +warning-marked read-only inspection when possible, with export prohibited. + +## Commercial support + +This repository does not promise private or commercial support. Any separate +agreement with IO-AI.TECH is governed by that agreement, not by project issues +or this document. diff --git a/SUPPORT.zh-CN.md b/SUPPORT.zh-CN.md new file mode 100644 index 0000000..f3c7f77 --- /dev/null +++ b/SUPPORT.zh-CN.md @@ -0,0 +1,33 @@ +# 支持 + +> **Language / 语言:** [English](./SUPPORT.md) | [简体中文](./SUPPORT.zh-CN.md) + +LeRobot Studio 是社区支持的开源软件。本文档不承诺响应时间、可用性、兼容性或修复义务。安全报告使用 [SECURITY.zh-CN.md](./SECURITY.zh-CN.md) 中的单独目标。 + +## 提问渠道 + +- 可复现的产品缺陷:公开迁移完成后,在 `ioai-tech/lerobot-studio` 的 GitHub Issues 提交 +- 功能与兼容性建议:公开迁移完成后,在 GitHub Issues 中说明用例、数据集版本与迁移影响 +- 漏洞:仅通过 [SECURITY.zh-CN.md](./SECURITY.zh-CN.md) 私有报告 +- 贡献:[CONTRIBUTING.zh-CN.md](./CONTRIBUTING.zh-CN.md) + +在完成 GitHub 迁移清单之前,公开 Issue 跟踪器视为不可用。开 Issue 前请搜索已有报告,并在最新受支持版本上复现。请提供 LeRobot `codebase_version`、浏览器与 OS 版本、输入类型、最小复现、控制台错误,以及问题出现在独立应用还是 npm 包中。请移除凭据、签名 URL、个人数据与机密数据集内容。 + +## 支持边界 + +稳定 1.0 的支持范围限于已文档化的查看器 API、精确的 LeRobot `v2.1` 与 `v3.0`、受支持的浏览器/输入组合以及已文档化的部署行为。见 [兼容性](./docs/compatibility.zh-CN.md)。 + +以下情况不在社区支持范围内,除非能在上述边界内复现项目缺陷: + +- 自定义或未知 LeRobot 格式与手工修复的数据集; +- SSR、React Server Components、CommonJS、Node.js 执行,或文档约定范围外的 React 版本; +- 宿主应用代码、认证、代理、CORS 配置、远程存储可用性与浏览器编解码器可用性; +- Fork、私有补丁、不受支持的发行版与第三方部署; +- 数据集恢复、标注、训练、模型质量或机器人学建议; +- 在官方训练就绪验证完成之前保证导出。 + +未知数据集版本不是受支持格式。1.0 目标是在可能时提供安全的、带警告的只读检查,并禁止导出。 + +## 商业支持 + +本仓库不承诺私有或商业支持。与 IO-AI.TECH 的任何单独协议由该协议管辖,而非项目 Issue 或本文档。 diff --git a/api-extractor.json b/api-extractor.json new file mode 100644 index 0000000..58bf6dc --- /dev/null +++ b/api-extractor.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "mainEntryPointFilePath": "/temp/types/react/public-api.d.ts", + "newlineKind": "lf", + "compiler": { + "tsconfigFilePath": "/tsconfig.lib.json", + "skipLibCheck": true + }, + "apiReport": { + "enabled": true, + "reportFolder": "/etc", + "reportTempFolder": "/temp" + }, + "docModel": { + "enabled": false + }, + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "/dist-lib/lerobot.d.ts" + }, + "tsdocMetadata": { + "enabled": false + }, + "messages": { + "extractorMessageReporting": { + "ae-forgotten-export": { + "logLevel": "error" + }, + "ae-missing-release-tag": { + "logLevel": "none" + } + } + } +} diff --git a/components.json b/components.json index d773367..71297e7 100644 --- a/components.json +++ b/components.json @@ -13,11 +13,11 @@ "iconLibrary": "lucide", "rtl": false, "aliases": { - "components": "src/components", - "utils": "src/lib/utils", - "ui": "src/ui/components", - "lib": "src/lib", - "hooks": "src/hooks" + "components": "src/ui/components", + "utils": "src/ui/lib/utils", + "ui": "src/ui/components/ui", + "lib": "src/ui/lib", + "hooks": "src/react/hooks" }, "menuColor": "default", "menuAccent": "subtle", diff --git a/docker-compose.yml b/docker-compose.yml index b102288..e6bfb7b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,5 +10,5 @@ services: # - BASE_PATH=/lerobot/ -> http://localhost:8080/lerobot/ BASE_PATH: ${BASE_PATH-/} ports: - - '8080:80' + - '8080:8080' restart: unless-stopped diff --git a/docker/httpd.conf b/docker/httpd.conf deleted file mode 100644 index 046daac..0000000 --- a/docker/httpd.conf +++ /dev/null @@ -1,7 +0,0 @@ -I:index.html -.wasm:application/wasm -.js:application/javascript -.mjs:application/javascript -.json:application/json -.map:application/json -.svg:image/svg+xml diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts new file mode 100644 index 0000000..c4b5c6f --- /dev/null +++ b/docs/.vitepress/config.ts @@ -0,0 +1,180 @@ +import { readdirSync, statSync } from 'node:fs'; +import { join, relative } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { defineConfig } from 'vitepress'; + +const docsDir = fileURLToPath(new URL('..', import.meta.url)); + +/** Map same-directory `*.zh-CN.md` sources to VitePress `zh-CN/` routes. */ +function collectZhRewrites(): Record { + const rewrites: Record = {}; + + /** @param {string} dir */ + function walk(dir: string) { + for (const name of readdirSync(dir)) { + const full = join(dir, name); + if (statSync(full).isDirectory()) { + if (name === '.vitepress') continue; + walk(full); + continue; + } + if (!name.endsWith('.zh-CN.md')) continue; + + const rel = relative(docsDir, full).replace(/\\/g, '/'); + const slug = rel.replace(/\.zh-CN\.md$/, ''); + const source = `${slug}.zh-CN.md`; + const route = slug === 'index' ? 'zh-CN/index.md' : `zh-CN/${slug}.md`; + rewrites[source] = route; + } + } + + walk(docsDir); + return rewrites; +} + +const outline = { level: [2, 3] as [number, number] }; + +export default defineConfig({ + title: 'LeRobot Studio', + description: 'Browser-based viewer and React library for LeRobot datasets', + lang: 'en-US', + base: process.env.DOCS_BASE ?? '/', + cleanUrls: true, + lastUpdated: true, + rewrites: collectZhRewrites(), + // Language switchers link to sibling *.zh-CN.md files for GitHub browsing. + // Those sources are rewritten to /zh-CN/* routes and are not resolvable as + // relative page paths during the VitePress dead-link pass. + ignoreDeadLinks: [/\.zh-CN(\.md)?$/], + markdown: { + lineNumbers: true, + }, + locales: { + root: { + label: 'English', + lang: 'en-US', + themeConfig: { + nav: [ + { text: 'Guide', link: '/quick-start' }, + { text: 'API', link: '/api' }, + { text: 'Data formats', link: '/data-formats' }, + { text: 'Operations', link: '/deployment' }, + { text: 'Live demo', link: 'https://lerobot.studio' }, + ], + sidebar: [ + { + text: 'Start', + items: [ + { text: 'Introduction', link: '/' }, + { text: 'Quick Start', link: '/quick-start' }, + { text: 'API', link: '/api' }, + ], + }, + { + text: 'Compatibility', + items: [ + { text: 'Data formats', link: '/data-formats' }, + { text: 'CORS and Range', link: '/cors' }, + { text: 'Browser support', link: '/browser' }, + { text: 'Release gates', link: '/compatibility' }, + ], + }, + { + text: 'Guides', + items: [ + { text: 'Privacy', link: '/privacy' }, + { text: 'Troubleshooting', link: '/troubleshooting' }, + { text: 'Migration', link: '/migration' }, + { text: 'Deprecation policy', link: '/deprecation' }, + ], + }, + { + text: 'Project', + collapsed: true, + items: [ + { text: 'Architecture', link: '/architecture' }, + { text: 'Development', link: '/development' }, + { text: 'Deployment', link: '/deployment' }, + { text: 'GitHub migration', link: '/github-migration' }, + ], + }, + ], + search: { provider: 'local' }, + outline, + footer: { + message: 'Released under the MIT License.', + copyright: 'Copyright © 2026 IO-AI.TECH', + }, + }, + }, + 'zh-CN': { + label: '简体中文', + lang: 'zh-CN', + link: '/zh-CN/', + title: 'LeRobot Studio', + description: '在浏览器中查看 LeRobot 数据集的 React 库与可视化应用', + markdown: { + container: { + tipLabel: '提示', + warningLabel: '警告', + dangerLabel: '危险', + infoLabel: '信息', + detailsLabel: '详细信息', + }, + }, + themeConfig: { + nav: [ + { text: '指南', link: '/quick-start' }, + { text: 'API', link: '/api' }, + { text: '数据格式', link: '/data-formats' }, + { text: '运维', link: '/deployment' }, + { text: '在线演示', link: 'https://lerobot.studio' }, + ], + sidebar: [ + { + text: '入门', + items: [ + { text: '简介', link: '/' }, + { text: '快速开始', link: '/quick-start' }, + { text: 'API', link: '/api' }, + ], + }, + { + text: '兼容性', + items: [ + { text: '数据格式', link: '/data-formats' }, + { text: 'CORS 与 Range', link: '/cors' }, + { text: '浏览器支持', link: '/browser' }, + { text: '发布门禁', link: '/compatibility' }, + ], + }, + { + text: '指南', + items: [ + { text: '隐私', link: '/privacy' }, + { text: '故障排查', link: '/troubleshooting' }, + { text: '迁移', link: '/migration' }, + { text: '弃用策略', link: '/deprecation' }, + ], + }, + { + text: '项目', + collapsed: true, + items: [ + { text: '架构', link: '/architecture' }, + { text: '开发', link: '/development' }, + { text: '部署', link: '/deployment' }, + { text: 'GitHub 迁移', link: '/github-migration' }, + ], + }, + ], + search: { provider: 'local' }, + outline, + footer: { + message: '基于 MIT 许可证发布。', + copyright: 'Copyright © 2026 IO-AI.TECH', + }, + }, + }, + }, +}); diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..c02178f --- /dev/null +++ b/docs/api.md @@ -0,0 +1,56 @@ +# Stable API + +> **Language / 语言:** [English](./api.md) | [简体中文](./api.zh-CN.md) + +The generated API report is the source of truth for the publishable surface. +The stable `1.0` contract is **viewer-only**: it contains the viewer, browser +data-source factories, and their public types. Product internals, source +mutation, editing, and export services are not public API. + +::: warning Publication status +The package is preparing for `1.0.0` and has not been published. The API report +describes the intended stable surface, but publication still depends on the +[1.0 release gates](./compatibility.md). +::: + +## Primary entry point + +```tsx +import { LeRobotViewer } from '@ioai/lerobot-studio'; +import '@ioai/lerobot-studio/style.css'; + + { + console.error(error.code, error.message); + }} +/>; +``` + +`dataSource` accepts either a remote archive URL or a custom `DataSource`. +Use `createArchiveDataSourceFromFile` for a local ZIP, TAR, or TAR.GZ archive, +and `createArchiveDataSourceFromUrl` when you need a reusable remote source. + +The optional `onExport` prop is only a host-owned UI callback. It does not +export data and does not make export part of the stable npm API. + +## Custom data sources + +A custom `DataSource` supplies browser-readable bytes and text by path. It may +also provide path listing and object URL lifecycle methods. Implementations +must treat dataset content as untrusted input and should release object URLs +and other resources from `clear()`. + +Progress callbacks use the `download`, `index`, `gunzip`, and `read` loading +phases. Fatal viewer errors are classified as invalid source, unavailable +remote source, unsupported archive, or dataset load failure. + +## Generated API report + +The following section is included directly from +`etc/lerobot-studio.api.md`. Update public declarations through the library +source and `npm run api:report`; do not hand-edit the report or duplicate it +here. + + diff --git a/docs/api.zh-CN.md b/docs/api.zh-CN.md new file mode 100644 index 0000000..ee002e5 --- /dev/null +++ b/docs/api.zh-CN.md @@ -0,0 +1,42 @@ +# 稳定 API + +> **Language / 语言:** [English](./api.md) | [简体中文](./api.zh-CN.md) + +生成的 API 报告是可发布表面的权威来源。稳定 `1.0` 契约为 **仅查看**:包含查看器、浏览器数据源工厂及其公开类型。产品内部实现、源数据修改、编辑与导出服务不属于公开 API。 + +::: warning 发布状态 +包正在准备 `1.0.0`,尚未发布。API 报告描述目标稳定公开面,但发布仍取决于 [1.0 发布门禁](./compatibility)。 +::: + +## 主入口 + +```tsx +import { LeRobotViewer } from '@ioai/lerobot-studio'; +import '@ioai/lerobot-studio/style.css'; + + { + console.error(error.code, error.message); + }} +/>; +``` + +`dataSource` 接受远程归档 URL 或自定义 `DataSource`。本地 ZIP、TAR 或 TAR.GZ 归档请用 `createArchiveDataSourceFromFile`;需要可复用的远程源时用 `createArchiveDataSourceFromUrl`。 + +可选的 `onExport` 属性仅是宿主拥有的 UI 回调。它不会导出数据,也不会使导出成为稳定 npm API 的一部分。 + +## 自定义数据源 + +自定义 `DataSource` 按路径提供浏览器可读的字节与文本。它也可提供路径列表与 object URL 生命周期方法。实现必须将数据集内容视为不可信输入,并应在 `clear()` 中释放 object URL 与其他资源。 + +进度回调使用 `download`、`index`、`gunzip`、`read` 加载阶段。致命查看器错误分为无效源、远程源不可用、不支持的归档或数据集加载失败。 + +## 生成的 API 报告 + +以下章节直接引用 `etc/lerobot-studio.api.md`。该文件由 API Extractor 自动生成,**请勿手工翻译**。请通过库源码与 `npm run api:report` 更新公开声明;不要在此重复或手改报告。 + +完整例外说明见 [文档国际化例外清单](./i18n-exceptions)。 + + diff --git a/docs/architecture.md b/docs/architecture.md index e451402..cc142b6 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,6 +1,9 @@ # Architecture -LeRobot Studio is a single npm package and SPA repository. The directory boundaries are architectural, not separately published packages. +> **Language / 语言:** [English](./architecture.md) | [简体中文](./architecture.zh-CN.md) + +LeRobot Studio is a single npm package and client-rendered SPA repository. The +directory boundaries are architectural, not separately published packages. ``` src/core Pure TypeScript domain logic @@ -29,4 +32,32 @@ src/web → src/react → src/platform → src/core ## Public API -The publishable surface is `src/react/index.ts`. Styles are exposed as `@ioai/lerobot-studio/style.css`. +The publishable surface is `src/react/index.ts`. Styles are exposed as +`@ioai/lerobot-studio/style.css`. + +The stable 1.0 package contract is viewer-only, ESM-only, browser-only, and +CSR-only with React and React DOM `^19.0.0`. “Viewer-only” means that dataset +viewing is the only stable library workflow; source mutation, editing, export +services, and standalone application controls are not stable public APIs. Any +export that exceeds that boundary must remain internal or be explicitly +classified before 1.0. + +Public APIs remain backward compatible within major version 1 according to the +[deprecation policy](./deprecation.md). + +## Trust boundaries + +- Dataset parsing, validation, media handling, and export operate on untrusted + input and must fail without modifying source data. +- Unknown dataset versions must not silently select a version adapter. The 1.0 + behavior is warning-marked read-only inspection when safe, with export + disabled. +- Remote sources cross an external network boundary; CORS, Range, + authentication, availability, and remote logging are controlled by the host + and source server. +- Browser storage is optional and failure-tolerant. See [Privacy](./privacy.md). +- The npm library has a zero-telemetry contract. + +These trust-boundary guarantees include 1.0 release gates tracked in +[Compatibility](./compatibility.md). Core paths are **automated CI verified**; +the final manual release gate for `1.0.0` is still pending. diff --git a/docs/architecture.zh-CN.md b/docs/architecture.zh-CN.md new file mode 100644 index 0000000..a0dcc54 --- /dev/null +++ b/docs/architecture.zh-CN.md @@ -0,0 +1,48 @@ +# 架构 + +> **Language / 语言:** [English](./architecture.md) | [简体中文](./architecture.zh-CN.md) + +LeRobot Studio 是单个 npm 包与客户端渲染 SPA 仓库。目录边界是架构划分,而非分别发布的包。 + +``` +src/core 纯 TypeScript 领域逻辑 +src/platform 浏览器、WASM、Worker 与 File System Access 适配器 +src/ui shadcn Base UI 基元与主题 token +src/react 公开可嵌入 React 库 API +src/web SPA 外壳、URL 状态与仅部署用的组合根 +``` + +## 依赖规则 + +``` +src/web → src/react → src/platform → src/core + ↘ src/ui ↗ +``` + +- `core` 不得导入 React、DOM、浏览器 API 或上层模块。 +- `platform` 不得导入 React UI。 +- `react` 暴露库 API,可消费 platform、core 与 UI 模块。 +- `web` 不从 npm 导出;它是独立应用的组合根。 + +## 构建目标 + +- `npm run build` 生成 Cloudflare Workers 静态资源目录 `dist/`。 +- `npm run build:lib` 生成 `@ioai/lerobot-studio` 消费者的 `dist-lib/`。库构建在 `.lerobot-root` 下作用域化 CSS,防止泄漏到宿主页面。 + +## 公开 API + +可发布表面为 `src/react/index.ts`。样式通过 `@ioai/lerobot-studio/style.css` 暴露。 + +稳定 1.0 包契约为仅查看、仅 ESM、仅浏览器、仅 CSR,React 与 React DOM `^19.0.0`。“仅查看”指数据集查看是唯一稳定的库工作流;源数据修改、编辑、导出服务与独立应用控件不是稳定公开 API。超出该边界的任何导出须保持内部,或在 1.0 前明确分类。 + +公开 API 在 major 1 内按 [弃用策略](./deprecation) 保持向后兼容。 + +## 信任边界 + +- 数据集解析、校验、媒体处理与导出处理不可信输入,且必须在不修改源数据的情况下失败。 +- 未知数据集版本不得静默选择版本适配器。1.0 行为是在安全时提供带警告的只读检查,并禁用导出。 +- 远程源跨越外部网络边界;CORS、Range、认证、可用性与远程日志由宿主与源服务器控制。 +- 浏览器存储是可选且容错的。见 [隐私](./privacy)。 +- npm 库为零遥测契约。 + +这些信任边界保证包含在 [兼容性](./compatibility) 中跟踪的 1.0 发布门禁;核心路径已在自动化 CI 中验证,最终人工发布门禁仍待完成。 diff --git a/docs/browser.md b/docs/browser.md new file mode 100644 index 0000000..87a6289 --- /dev/null +++ b/docs/browser.md @@ -0,0 +1,40 @@ +# Browser support + +> **Language / 语言:** [English](./browser.md) | [简体中文](./browser.zh-CN.md) + +LeRobot Studio targets modern browsers, but feature availability depends on +browser APIs, media codecs, dataset encoding, and deployment policy. “Modern” +does not guarantee that every video can be decoded or encoded. + +## Validation scope + +CI runs the full browser suite in Chromium and a compatibility smoke suite in +Firefox and WebKit. The smoke suite covers viewer composition, localized error +states, keyboard and accessibility primitives, unknown-version read-only +behavior, and remote or custom `DataSource` contracts. + +| Capability | Chromium | Firefox | WebKit | +| ---------------------------- | --------------------------- | --------------------------- | --------------------------- | +| Viewer and error states | CI smoke | CI smoke | CI smoke | +| Remote/custom data source | CI smoke | CI smoke | CI smoke | +| Local archive viewing | Extended validation pending | Extended validation pending | Extended validation pending | +| Restorable directory handles | Capability detected | Archive-only fallback | Archive-only fallback | +| Directory export | Chromium only | Unavailable | Unavailable | +| Video encoding/export | Full suite with WebCodecs | Capability detected | Capability detected | +| Video playback | Codec dependent | Codec dependent | Codec dependent | + +This matrix separates tested scope from intended support. Release evidence must +record actual browser versions, and all 1.0 claims remain subject to the +[release gates](./compatibility.md). + +## Client-only requirement + +The package accesses browser facilities and must be rendered only on the +client. It does not support SSR, hydration of server-rendered viewer markup, +React Server Components, Node.js, or CommonJS. + +## Graceful degradation + +Missing File System Access or WebCodecs capabilities should remove or disable +the affected operation rather than break inspection. Unsupported media should +produce a clear error while non-media inspection remains available where safe. diff --git a/docs/browser.zh-CN.md b/docs/browser.zh-CN.md new file mode 100644 index 0000000..4ef61bc --- /dev/null +++ b/docs/browser.zh-CN.md @@ -0,0 +1,29 @@ +# 浏览器支持 + +> **Language / 语言:** [English](./browser.md) | [简体中文](./browser.zh-CN.md) + +LeRobot Studio 面向现代浏览器,但功能可用性取决于浏览器 API、媒体编解码器、数据集编码与部署策略。“现代”并不保证每个视频都能解码或编码。 + +## 验证范围 + +CI 在 Chromium 中运行完整浏览器套件,在 Firefox 与 WebKit 中运行兼容性冒烟套件。冒烟覆盖查看器组合、本地化错误状态、键盘与无障碍基元、未知版本只读行为,以及远程或自定义 `DataSource` 契约。 + +| 能力 | Chromium | Firefox | WebKit | +| ----------------- | ----------------------- | -------------- | -------------- | +| 查看器与错误状态 | CI 冒烟 | CI 冒烟 | CI 冒烟 | +| 远程/自定义数据源 | CI 冒烟 | CI 冒烟 | CI 冒烟 | +| 本地归档查看 | 待扩展验证 | 待扩展验证 | 待扩展验证 | +| 可恢复目录句柄 | 能力检测 | 仅归档回退 | 仅归档回退 | +| 目录导出 | 仅 Chromium | 不可用 | 不可用 | +| 视频编码/导出 | 含 WebCodecs 的完整套件 | 能力检测 | 能力检测 | +| 视频播放 | 取决于编解码器 | 取决于编解码器 | 取决于编解码器 | + +本矩阵区分已测试范围与目标支持范围。发布证据须记录实际浏览器版本,所有 1.0 声明仍受 [发布门禁](./compatibility) 约束。 + +## 仅客户端要求 + +包访问浏览器设施,必须仅在客户端渲染。不支持 SSR、服务端渲染查看器标记的 hydration、React Server Components、Node.js 或 CommonJS。 + +## 优雅降级 + +缺少 File System Access 或 WebCodecs 能力时,应移除或禁用受影响操作,而非破坏检查。不支持的媒体应产生明确错误,在安全时非媒体检查仍应可用。 diff --git a/docs/compatibility.md b/docs/compatibility.md new file mode 100644 index 0000000..0876e25 --- /dev/null +++ b/docs/compatibility.md @@ -0,0 +1,121 @@ +# Compatibility and release gates + +> **Language / 语言:** [English](./compatibility.md) | [简体中文](./compatibility.zh-CN.md) + +This document separates the stable `1.0.0` contract from final release +approval. Rows marked **CI verified; release gate pending** pass automated +acceptance checks (unit, browser, and official LeRobot compatibility jobs). +They must not be advertised as generally supported until maintainers complete +the final manual release gate for `1.0.0`. + +## Runtime contract + +- React and React DOM: `^19.0.0` +- Module format: ESM only +- Rendering: client-side rendering only +- Runtime: modern browsers only; Node.js, CommonJS, React Server Components, + server-side rendering, and hydration of server-rendered viewer markup are out + of scope +- Compatibility: public APIs remain backward compatible throughout major + version 1, subject to the [deprecation policy](./deprecation.md) +- Telemetry: zero telemetry in the npm library + +The package metadata declares this React peer range. Validating Vite and Next.js +client-only consumers remains a 1.0 release gate. + +## Dataset support + +| Dataset `codebase_version` | Open and inspect | Export | Status | +| -------------------------- | ---------------------------------------------- | ---------------------------------------- | --------------------------------- | +| Exactly `v2.1` | Automated CI verified | Official reader roundtrip verified in CI | CI verified; release gate pending | +| Exactly `v3.0` | Automated CI verified | Official reader roundtrip verified in CI | CI verified; release gate pending | +| Missing or any other value | Warning-marked read-only when safely parseable | Prohibited (enforced in CI) | CI verified; release gate pending | + +“Exact” applies to the declared dataset version, not a prefix family. In +particular, `v2.0`, future `v2.x`, future `v3.x`, and malformed or missing +versions are unknown. Unknown data must never silently select a compatible +adapter. If safe parsing is impossible, loading must fail without modifying the +source. + +Supported export means that the complete exported dataset can be consumed by +the corresponding official LeRobot release for training without manual repair. +The validation fixture set covers metadata, tasks, episodes, Parquet data, +timestamps, images/video, and cross-version export where offered. CI verifies +these paths against the pinned official LeRobot `v0.6.1` reader; advertising +export as generally supported still requires the final manual release gate. + +For v3 data, Studio rolls Parquet files by the configured data-file size and +uses `chunks_size` as the maximum number of files per chunk. Browser export +currently keeps each episode video in a separate MP4 and rolls video chunks by +file count. Official LeRobot may losslessly concatenate several episode videos +into one size-bounded MP4; Studio does not claim byte-for-byte sharding parity, +but its per-episode layout and metadata references are validated by the official +v0.6.1 reader. + +## Input and network support + +| Input | Contract | +| --------------- | -------------------------------------------------------------------------------------------- | +| Local archive | Browser-local read; archive types shown by the UI must be tested before 1.0 | +| Local directory | Requires the File System Access API; fallback selection may not preserve a restorable handle | +| Remote archive | Requires cross-origin `GET`; byte-range access is required for scalable large-archive use | +| Sample URL | Same origin, CORS, and Range rules as any remote archive | + +For remote archives, the origin should: + +- answer the preflight or request with an appropriate + `Access-Control-Allow-Origin`; +- allow `GET` and the `Range` request header when preflighted; +- expose `Content-Range`, `Accept-Ranges`, and `Content-Length` when the client + needs to inspect them; and +- respond to a valid byte request with `206 Partial Content` and a valid + `Content-Range`. + +A server that ignores Range may force a full download and is not considered a +supported large-dataset configuration. Authentication, private object-store +credentials, signed-URL generation, and proxying are host-application +responsibilities. + +## Browser capability matrix + +The automated matrix uses Playwright Chromium, Firefox, and WebKit. “Smoke” +means Viewer composition, localized error states, keyboard/accessibility +primitives, unknown-version read-only behavior, and remote/custom `DataSource` +contracts. It does not imply codec support for every dataset. + +| Capability | Chromium (Chrome/Edge) | Firefox | WebKit (Safari engine) | +| --------------------------------------- | ------------------------------------------------ | ------------------------------------------ | ------------------------------------------ | +| Viewer/error/accessibility/read-only | CI smoke | CI smoke | CI smoke | +| Remote or custom `DataSource` | CI smoke | CI smoke | CI smoke | +| Local archive viewing | Extended validation pending | Extended validation pending | Extended validation pending | +| Directory picker and restorable handles | File System Access capability detected | Explicit ZIP-only fallback when absent | Explicit ZIP-only fallback when absent | +| ZIP download | Capability detected; export suite verified in CI | Capability detected in smoke | Capability detected in smoke | +| Directory export | Chromium-only; requires File System Access | Not run; directory option must stay absent | Not run; directory option must stay absent | +| Video encoding/export | Full browser export suite, including WebCodecs | Not run; WebCodecs capability is detected | Not run; WebCodecs capability is detected | +| Video decode/playback | Dataset/codec dependent | Dataset/codec dependent | Dataset/codec dependent | + +CI installs all three engines. Chromium retains the complete browser suite, +including export and encoding scenarios. Firefox and WebKit run only the smoke +scope above; a missing browser capability is tested as an explicit degradation +contract rather than converted into a skipped test. + +The pinned official compatibility gate uses LeRobot `v0.6.1` commit +`7e241bd630a3719a56157a497ce5d08f244784f1`. Nightly also checks the current +upstream `main` as a non-blocking warning job. Exact browser versions must be +recorded in release evidence. A browser being “modern” does not guarantee codec +support; unsupported media must produce a clear error while non-media +inspection remains available where safe. + +## 1.0 acceptance gates + +- Enforce exact `v2.1` and `v3.0` detection (**automated CI verified**). +- Provide a visible warning and immutable mode for unknown versions; disable + every export entry point and service call in that mode (**automated CI + verified**). +- Validate supported exports with official LeRobot training workflows (**CI + verified**; final manual release approval still pending). +- Keep React peer dependencies at `^19.0.0` and test Vite and Next.js + client-only consumers. +- Run the browser matrix and retain versioned evidence. +- Confirm the npm artifact has no telemetry or host-application side effects + beyond documented browser storage. diff --git a/docs/compatibility.zh-CN.md b/docs/compatibility.zh-CN.md new file mode 100644 index 0000000..23a665e --- /dev/null +++ b/docs/compatibility.zh-CN.md @@ -0,0 +1,77 @@ +# 兼容性与发布门禁 + +> **Language / 语言:** [English](./compatibility.md) | [简体中文](./compatibility.zh-CN.md) + +本文档区分稳定 `1.0.0` 契约与最终发布审批。标记为 **CI 已验证;发布门禁待完成** 的行已通过自动化验收检查(单元、浏览器与官方 LeRobot 兼容性作业)。在维护者完成 `1.0.0` 的最终人工发布门禁之前,不得将其宣称为普遍受支持。 + +## 运行时契约 + +- React 与 React DOM:`^19.0.0` +- 模块格式:仅 ESM +- 渲染:仅客户端渲染 +- 运行时:仅现代浏览器;Node.js、CommonJS、React Server Components、服务端渲染与服务端渲染查看器标记的 hydration 不在范围内 +- 兼容性:公开 API 在 major 1 内保持向后兼容,受 [弃用策略](./deprecation) 约束 +- 遥测:npm 库为零遥测 + +包元数据声明此 React peer 范围。验证 Vite 与 Next.js 纯客户端消费者仍是 1.0 发布门禁。 + +## 数据集支持 + +| 数据集 `codebase_version` | 打开与检查 | 导出 | 状态 | +| ------------------------- | ------------------------ | ------------------------------- | ------------------------- | +| 精确 `v2.1` | 自动化 CI 已验证 | CI 中已通过官方 reader 往返验证 | CI 已验证;发布门禁待完成 | +| 精确 `v3.0` | 自动化 CI 已验证 | CI 中已通过官方 reader 往返验证 | CI 已验证;发布门禁待完成 | +| 缺失或任何其他值 | 可安全解析时带警告的只读 | 禁止(CI 强制执行) | CI 已验证;发布门禁待完成 | + +“精确”指声明的数据集版本,而非前缀族。具体而言,`v2.0`、未来 `v2.x`、未来 `v3.x` 以及格式错误或缺失的版本均视为未知。未知数据绝不得静默选择兼容适配器。若无法安全解析,加载必须失败且不修改源数据。 + +受支持导出指完整导出数据集可被对应官方 LeRobot 版本用于训练,且无需手工修复。验证 fixture 集覆盖元数据、任务、episode、Parquet 数据、时间戳、图像/视频以及提供的跨版本导出。CI 已对照固定的官方 LeRobot `v0.6.1` reader 验证这些路径;将其宣称为普遍受支持仍须完成最终人工发布门禁。 + +对于 v3 数据,Studio 按配置的数据文件大小滚动 Parquet 文件,并以 `chunks_size` 作为每个 chunk 的最大文件数。浏览器导出目前将每个 episode 视频保留在独立 MP4 中,并按文件数滚动 video chunk。官方 LeRobot 可能将多个 episode 视频无损拼接为一个有大小上限的 MP4;Studio 不声称字节级分片完全一致,但其按 episode 分文件的布局与元数据引用已在 CI 中通过官方 LeRobot `v0.6.1` reader 验证。 + +## 输入与网络支持 + +| 输入 | 契约 | +| -------- | ----------------------------------------------------------- | +| 本地归档 | 浏览器本地读取;UI 展示的归档类型须在 1.0 前测试 | +| 本地目录 | 需要 File System Access API;回退选择可能无法保留可恢复句柄 | +| 远程归档 | 需要跨源 `GET`;大型归档的可扩展访问需要字节 Range | +| 示例 URL | 与任何远程归档相同的源、CORS 与 Range 规则 | + +对于远程归档,源站应: + +- 在 preflight 或请求中返回合适的 `Access-Control-Allow-Origin`; +- 在 preflight 时允许 `GET` 与 `Range` 请求头; +- 在客户端需要读取时暴露 `Content-Range`、`Accept-Ranges` 与 `Content-Length`;以及 +- 对有效字节请求响应 `206 Partial Content` 与有效的 `Content-Range`。 + +忽略 Range 的服务器可能迫使完整下载,不被视为受支持的大型数据集配置。认证、私有对象存储凭据、签名 URL 生成与代理属于宿主应用职责。 + +## 浏览器能力矩阵 + +自动化矩阵使用 Playwright Chromium、Firefox 与 WebKit。“冒烟(smoke)”指查看器组合、本地化错误状态、键盘/无障碍基元、未知版本只读行为,以及远程/自定义 `DataSource` 契约。它并不暗示对每个数据集的编解码器支持。 + +| 能力 | Chromium (Chrome/Edge) | Firefox | WebKit (Safari 引擎) | +| ------------------------- | ------------------------------------ | --------------------------- | --------------------------- | +| 查看器/错误/无障碍/只读 | CI 冒烟 | CI 冒烟 | CI 冒烟 | +| 远程或自定义 `DataSource` | CI 冒烟 | CI 冒烟 | CI 冒烟 | +| 本地归档查看 | 待扩展验证 | 待扩展验证 | 待扩展验证 | +| 目录选择器与可恢复句柄 | 检测到 File System Access 能力 | 缺失时明确 ZIP-only 回退 | 缺失时明确 ZIP-only 回退 | +| ZIP 下载 | 能力检测;CI 已验证完整导出套件 | 冒烟中能力检测 | 冒烟中能力检测 | +| 目录导出 | 仅 Chromium;需要 File System Access | 未运行;目录选项须保持缺失 | 未运行;目录选项须保持缺失 | +| 视频编码/导出 | 完整浏览器导出套件,含 WebCodecs | 未运行;检测 WebCodecs 能力 | 未运行;检测 WebCodecs 能力 | +| 视频解码/播放 | 取决于数据集/编解码器 | 取决于数据集/编解码器 | 取决于数据集/编解码器 | + +CI 安装全部三种引擎。Chromium 保留完整浏览器套件,含导出与编码场景。Firefox 与 WebKit 仅运行上述冒烟范围;缺失的浏览器能力应测试为明确的降级契约,而非跳过测试。 + +官方兼容性门禁固定使用 LeRobot `v0.6.1` commit +`7e241bd630a3719a56157a497ce5d08f244784f1`。Nightly 还以非阻塞警告作业检查当前上游 `main`。精确浏览器版本须记录在发布证据中。浏览器“现代”并不保证编解码器支持;不支持的媒体须产生明确错误,在安全时非媒体检查仍应可用。 + +## 1.0 验收门禁 + +- 强制执行精确 `v2.1` 与 `v3.0` 检测(自动化 CI 已验证)。 +- 为未知版本提供可见警告与不可变模式;在该模式下禁用每个导出入口与服务调用(自动化 CI 已验证)。 +- 用官方 LeRobot 训练工作流验证受支持导出(CI 已验证;最终人工发布审批仍待完成)。 +- 保持 React peer 依赖为 `^19.0.0`,并测试 Vite 与 Next.js 纯客户端消费者。 +- 运行浏览器矩阵并保留带版本的证据。 +- 确认 npm 制品除文档约定的浏览器存储外无遥测或宿主应用副作用。 diff --git a/docs/cors.md b/docs/cors.md new file mode 100644 index 0000000..fc2437f --- /dev/null +++ b/docs/cors.md @@ -0,0 +1,52 @@ +# CORS and HTTP Range + +> **Language / 语言:** [English](./cors.md) | [简体中文](./cors.zh-CN.md) + +The browser fetches a remote archive from the URL supplied by the host or user. +That archive server controls CORS, byte-range behavior, authentication, and +request logging. + +## Required response behavior + +For a public, credential-free archive, a typical configuration includes: + +```http +Access-Control-Allow-Origin: * +Accept-Ranges: bytes +Access-Control-Expose-Headers: Accept-Ranges, Content-Length, Content-Range +``` + +When the viewer requests `Range: bytes=0-65535`, the server should return: + +```http +HTTP/1.1 206 Partial Content +Content-Range: bytes 0-65535/123456789 +Content-Length: 65536 +``` + +If a preflight occurs, allow: + +```http +Access-Control-Allow-Methods: GET, OPTIONS +Access-Control-Allow-Headers: Range +``` + +Use the viewer origin instead of `*` for credentialed requests. Never combine +`Access-Control-Allow-Origin: *` with credentials. + +## Why Range matters + +A server that ignores `Range` may force the browser to download the complete +archive before inspection. That behavior is not a supported large-dataset +configuration. Redirects, CDNs, and signed URLs must preserve both the CORS +headers and `206 Partial Content` behavior. + +## Host responsibilities + +The embedding application owns private credentials, signed-URL generation, +proxies, and authorization. The stable viewer does not accept or manage server +credentials. Prefer short-lived signed URLs over exposing object-store secrets +to browser code. + +See [Troubleshooting](./troubleshooting.md) for checks to run when a remote +archive does not open. diff --git a/docs/cors.zh-CN.md b/docs/cors.zh-CN.md new file mode 100644 index 0000000..7042d2f --- /dev/null +++ b/docs/cors.zh-CN.md @@ -0,0 +1,42 @@ +# CORS 与 HTTP Range + +> **Language / 语言:** [English](./cors.md) | [简体中文](./cors.zh-CN.md) + +浏览器从宿主或用户提供的 URL 获取远程归档。该归档服务器控制 CORS、字节 Range 行为、认证与请求日志。 + +## 必需的响应行为 + +对于公开、无凭据的归档,典型配置包括: + +```http +Access-Control-Allow-Origin: * +Accept-Ranges: bytes +Access-Control-Expose-Headers: Accept-Ranges, Content-Length, Content-Range +``` + +当查看器请求 `Range: bytes=0-65535` 时,服务器应返回: + +```http +HTTP/1.1 206 Partial Content +Content-Range: bytes 0-65535/123456789 +Content-Length: 65536 +``` + +若发生 preflight,请允许: + +```http +Access-Control-Allow-Methods: GET, OPTIONS +Access-Control-Allow-Headers: Range +``` + +带凭据的请求请使用查看器源而非 `*`。切勿将 `Access-Control-Allow-Origin: *` 与凭据组合使用。 + +## 为何 Range 重要 + +忽略 `Range` 的服务器可能迫使浏览器在检查前下载完整归档。该行为不是受支持的大型数据集配置。重定向、CDN 与签名 URL 必须同时保留 CORS 头与 `206 Partial Content` 行为。 + +## 宿主职责 + +嵌入应用负责私有凭据、签名 URL 生成、代理与授权。稳定查看器不接受或管理服务器凭据。相比向浏览器代码暴露对象存储密钥,更推荐使用短生命周期签名 URL。 + +远程归档无法打开时的检查步骤见 [故障排查](./troubleshooting)。 diff --git a/docs/data-formats.md b/docs/data-formats.md new file mode 100644 index 0000000..f842de6 --- /dev/null +++ b/docs/data-formats.md @@ -0,0 +1,50 @@ +# Data formats + +> **Language / 语言:** [English](./data-formats.md) | [简体中文](./data-formats.zh-CN.md) + +Format support is version-specific. A matching major prefix is not enough: +`v2.0`, future `v2.x`, future `v3.x`, malformed values, and missing +`codebase_version` are unknown formats. + +## 1.0 format matrix + +| Declared version | Open and inspect | Export | Status | +| -------------------------- | --------------------------------------------------- | ---------------------------------------- | --------------------------------- | +| Exactly `v2.1` | Automated CI verified | Official reader roundtrip verified in CI | CI verified; release gate pending | +| Exactly `v3.0` | Automated CI verified | Official reader roundtrip verified in CI | CI verified; release gate pending | +| Missing or any other value | Warning-marked read-only mode when safely parseable | Prohibited (enforced in CI) | CI verified; release gate pending | + +These entries reflect automated verification in CI. Advertising a capability as +generally supported for the `1.0.0` release still requires the final manual +release gate. See [Compatibility and release gates](./compatibility.md) for the +full matrix and acceptance checklist. + +## Inputs + +- Local archives: ZIP, TAR, or TAR.GZ through the public archive factory. +- Local directories: require File System Access API support; other browsers + may offer an archive-only fallback. +- Remote archives: require cross-origin `GET`. Large archives also require + working byte ranges. +- Custom sources: implement the stable [`DataSource`](./api.md) contract. + +Archive contents and paths are untrusted input. Loading must fail safely and +must not modify the source dataset. + +## Parquet and video sharding + +For v3 export, Studio rolls Parquet files by the configured data-file size and +uses `chunks_size` as the maximum file count in each chunk. + +Video layout differs from official LeRobot: + +- Studio currently writes one MP4 per episode and rolls video chunks by file + count. +- Official LeRobot may losslessly concatenate multiple episode videos into one + size-bounded MP4. + +Studio therefore does **not** claim byte-for-byte video sharding parity. +Per-episode layout and metadata references are verified in CI against the +pinned official LeRobot `v0.6.1` reader. Advertising export as generally +supported still requires the final manual release gate; export remains outside +the stable npm API until then. diff --git a/docs/data-formats.zh-CN.md b/docs/data-formats.zh-CN.md new file mode 100644 index 0000000..29573f2 --- /dev/null +++ b/docs/data-formats.zh-CN.md @@ -0,0 +1,35 @@ +# 数据格式 + +> **Language / 语言:** [English](./data-formats.md) | [简体中文](./data-formats.zh-CN.md) + +格式支持是版本特定的。匹配 major 前缀不足:`v2.0`、未来 `v2.x`、未来 `v3.x`、格式错误的值以及缺失的 `codebase_version` 均为未知格式。 + +## 1.0 格式矩阵 + +| 声明版本 | 打开与检查 | 导出 | 状态 | +| ---------------- | ---------------------------- | ------------------------------- | ------------------------- | +| 精确 `v2.1` | 自动化 CI 已验证 | CI 中已通过官方 reader 往返验证 | CI 已验证;发布门禁待完成 | +| 精确 `v3.0` | 自动化 CI 已验证 | CI 中已通过官方 reader 往返验证 | CI 已验证;发布门禁待完成 | +| 缺失或任何其他值 | 可安全解析时带警告的只读模式 | 禁止(CI 强制执行) | CI 已验证;发布门禁待完成 | + +这些条目反映 CI 中的自动化验证。将某项能力宣称为 `1.0.0` 普遍受支持,仍须完成最终人工发布门禁。完整矩阵与验收清单见 [兼容性与发布门禁](./compatibility)。 + +## 输入 + +- 本地归档:通过公开归档工厂支持 ZIP、TAR、TAR.GZ。 +- 本地目录:需要 File System Access API 支持;其他浏览器可能提供仅归档回退。 +- 远程归档:需要跨源 `GET`。大型归档还需要可用的字节 Range。 +- 自定义源:实现稳定的 [`DataSource`](./api) 契约。 + +归档内容与路径是不可信输入。加载必须安全失败,且不得修改源数据集。 + +## Parquet 与 video 分片 + +v3 导出时,Studio 按配置的数据文件大小滚动 Parquet 文件,并以 `chunks_size` 作为每个 chunk 的最大文件数。 + +视频布局与官方 LeRobot 不同: + +- Studio 目前为每个 episode 写入一个 MP4,并按文件数滚动 video chunk。 +- 官方 LeRobot 可能将多个 episode 视频无损拼接为一个有大小上限的 MP4。 + +因此 Studio **不** 声称 video 分片的字节级完全一致。按 episode 分文件的布局与元数据引用已在 CI 中通过固定的官方 LeRobot `v0.6.1` reader 验证。将其宣称为普遍受支持仍须完成最终人工发布门禁;在此之前,导出仍在稳定 npm API 范围之外。 diff --git a/docs/deployment.md b/docs/deployment.md index dea89a1..1fcd1c3 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -1,14 +1,27 @@ # Deployment -## Production site (Cloudflare Workers) +> **Language / 语言:** [English](./deployment.md) | [简体中文](./deployment.zh-CN.md) + +## Production target (Cloudflare Workers) Production is served at **https://lerobot.studio**. -The GitHub repository [`ioai-tech/lerobot-studio`](https://github.com/ioai-tech/lerobot-studio) -is connected directly to a Cloudflare Worker. Merges to `main` trigger a Cloudflare -build and deploy — no GitHub Actions secrets are required for this path. +The planned post-migration setup connects `ioai-tech/lerobot-studio` directly to +a Cloudflare Worker so merges to `main` trigger a Cloudflare build and deploy +without GitHub Actions deployment secrets. The repository connection and first +deployment must be verified during the +[GitHub migration](./github-migration.md); this document does not assert that +the public repository is already available. + +The Cloudflare repository integration is a separate trust boundary from GitHub +Actions. Grant it access only to this repository, restrict production deploys +to the protected `main` branch, and review its build logs and dependency update +behavior. Fork pull-request workflows must not receive Cloudflare credentials; +the planned integration does not require storing a Cloudflare API token as a +GitHub Actions secret. -Worker configuration lives in [`wrangler.jsonc`](../wrangler.jsonc): +Worker configuration lives in +[`wrangler.jsonc`](https://github.com/ioai-tech/lerobot-studio/blob/main/wrangler.jsonc): - Worker name: `lerobot-studio` - Assets directory: `dist` @@ -35,7 +48,13 @@ npx wrangler deploy ```bash docker build -t lerobot-studio . -docker run --rm -p 8080:80 lerobot-studio +docker run --rm -p 8080:8080 lerobot-studio +``` + +Or use the root `docker-compose.yml` (maps host port 8080): + +```bash +docker compose up --build ``` Optional build args: @@ -44,7 +63,7 @@ Optional build args: - `SITE_URL` — canonical site URL for SEO assets (production default: `https://lerobot.studio`) - `VITE_SAMPLES_BASE_URL` — remote sample manifest base URL -Published images (after open release): +Planned published images (after a successful public release): - `ghcr.io/ioai-tech/lerobot-studio:latest` - `ghcr.io/ioai-tech/lerobot-studio:` @@ -58,13 +77,59 @@ SITE_URL=https://lerobot.studio npm run build Serve `dist` with any static file server. Configure SPA fallback to `index.html`. +## Documentation site (GitHub Pages) + +The documentation is a separate VitePress build: + +```bash +npm run docs:build +``` + +`.github/workflows/docs-pages.yml` builds and deploys +`docs/.vitepress/dist` after documentation changes reach `main`. Its default +`DOCS_BASE` is `//`, so it works at the GitHub project Pages +URL without assuming that a custom documentation domain exists. + +Repository administrators must complete these steps after the public +repository exists: + +1. Open **Settings → Pages** and set **Source** to **GitHub Actions**. +2. Run the **Documentation Pages** workflow once and verify the generated + `https://ioai-tech.github.io/lerobot-studio/` URL. +3. Add the workflow’s `build` and `deploy` jobs to the protected-branch policy + if documentation deployment is required for merges. +4. Only after DNS ownership is ready, configure a custom domain in GitHub + Pages, add the documented DNS records, enable HTTPS, and verify the domain. +5. For a root custom domain such as `docs.lerobot.studio`, change the workflow + `DOCS_BASE` to `/`. Do not add a `CNAME` file or advertise that hostname + until GitHub Pages and DNS both confirm it. + +The workflow requires `pages: write` and `id-token: write` only in the deploy +job. Pull requests build the same site through the read-only CI job but do not +deploy it. + ## Sample datasets and CORS -Remote archives require: +Remote archives must allow cross-origin `GET`. Scalable access to large +archives also requires byte ranges. A typical object response includes: + +- `Access-Control-Allow-Origin: https://your-viewer.example` (or `*` for a + genuinely public, credential-free object); +- `Accept-Ranges: bytes`; +- `Content-Length`; +- a `206 Partial Content` response with a valid `Content-Range` when the client + sends `Range: bytes=...`; and +- `Access-Control-Expose-Headers: Accept-Ranges, Content-Length, Content-Range` + when those headers must be read by browser code. -- CORS allowing `GET` from your site origin -- HTTP `Range` support for large archives +If a preflight occurs, allow `GET`, `OPTIONS`, and the `Range` request header. +Do not use wildcard origins with credentialed requests. A server that ignores +Range may trigger a full download and is not a supported large-dataset setup. +See [Compatibility](./compatibility.md) for the complete network boundary. ## Privacy -The open-source web shell does **not** include Google Analytics or Microsoft Clarity. +The npm library has a zero-telemetry contract. The open-source web shell does +not include Google Analytics or Microsoft Clarity. Deployers are responsible +for disclosing logs, analytics, proxies, sample endpoints, authentication, and +retention that they add. See [Privacy](./privacy.md). diff --git a/docs/deployment.zh-CN.md b/docs/deployment.zh-CN.md new file mode 100644 index 0000000..bc7a90b --- /dev/null +++ b/docs/deployment.zh-CN.md @@ -0,0 +1,104 @@ +# 部署 + +> **Language / 语言:** [English](./deployment.md) | [简体中文](./deployment.zh-CN.md) + +## 生产目标(Cloudflare Workers) + +生产环境部署于 **https://lerobot.studio**。 + +迁移后的计划配置将 `ioai-tech/lerobot-studio` 直接连接到 Cloudflare Worker,使合并到 `main` 触发 Cloudflare 构建与部署,而无需 GitHub Actions 部署密钥。仓库连接与首次部署须在 [GitHub 迁移](./github-migration) 期间验证;本文档不断言公开仓库已可用。 + +Cloudflare 仓库集成与 GitHub Actions 是独立的信任边界。仅授予对本仓库的访问,将生产部署限制在受保护的 `main` 分支,并审查其构建日志与依赖更新行为。Fork 的 Pull Request 工作流不得获得 Cloudflare 凭据;计划中的集成不需要在 GitHub Actions secret 中存储 Cloudflare API token。 + +Worker 配置位于 +[`wrangler.jsonc`](https://github.com/ioai-tech/lerobot-studio/blob/main/wrangler.jsonc): + +- Worker 名称:`lerobot-studio` +- 资源目录:`dist` +- SPA 回退:`not_found_handling = "single-page-application"` +- 自定义域:`lerobot.studio`(已在 Cloudflare 绑定) + +推荐的 Cloudflare 构建设置(控制台 / Workers Builds): + +| 设置 | 值 | +| -------------------------------- | ------------------------------------------- | +| Build command | `npm ci && npm run build` | +| Deploy command | `npx wrangler deploy`(或 Cloudflare 默认) | +| Root directory | 仓库根目录 | +| Env `SITE_URL` / `VITE_SITE_URL` | `https://lerobot.studio` | + +本地演练: + +```bash +SITE_URL=https://lerobot.studio npm run build +npx wrangler deploy +``` + +## Docker + +```bash +docker build -t lerobot-studio . +docker run --rm -p 8080:8080 lerobot-studio +``` + +也可以使用根目录的 `docker-compose.yml`(映射宿主机端口 8080): + +```bash +docker compose up --build +``` + +可选构建参数: + +- `BASE_PATH` — 子目录部署路径(例如 `/lerobot/`) +- `SITE_URL` — SEO 资源的规范站点 URL(生产默认:`https://lerobot.studio`) +- `VITE_SAMPLES_BASE_URL` — 远程示例 manifest 基 URL + +计划中的公开镜像(成功公开发布后): + +- `ghcr.io/ioai-tech/lerobot-studio:latest` +- `ghcr.io/ioai-tech/lerobot-studio:` + +## 静态托管 + +```bash +SITE_URL=https://lerobot.studio npm run build +``` + +用任意静态文件服务器提供 `dist`。将 SPA 回退配置为 `index.html`。 + +## 文档站点(GitHub Pages) + +文档是独立的 VitePress 构建: + +```bash +npm run docs:build +``` + +`.github/workflows/docs-pages.yml` 在文档变更到达 `main` 后构建并部署 `docs/.vitepress/dist`。默认 `DOCS_BASE` 为 `//`,因此可在 GitHub 项目 Pages URL 工作,而不假设存在自定义文档域。 + +公开仓库存在后,仓库管理员须完成: + +1. 打开 **Settings → Pages**,将 **Source** 设为 **GitHub Actions**。 +2. 运行一次 **Documentation Pages** 工作流,并验证生成的 `https://ioai-tech.github.io/lerobot-studio/` URL。 +3. 若合并需要文档部署,将工作流的 `build` 与 `deploy` 作业加入受保护分支策略。 +4. 仅在 DNS 所有权就绪后,在 GitHub Pages 配置自定义域、添加文档记录的 DNS 记录、启用 HTTPS 并验证域。 +5. 对于根自定义域如 `docs.lerobot.studio`,将工作流 `DOCS_BASE` 改为 `/`。在 GitHub Pages 与 DNS 均确认之前,不要添加 `CNAME` 文件或宣传该主机名。 + +工作流仅在 deploy 作业中需要 `pages: write` 与 `id-token: write`。Pull Request 通过只读 CI 作业构建相同站点,但不部署。 + +## 示例数据集与 CORS + +远程归档须允许跨源 `GET`。大型归档的可扩展访问还需要字节 Range。典型的对象响应包括: + +- `Access-Control-Allow-Origin: https://your-viewer.example`(或对真正公开、无凭据的对象使用 `*`); +- `Accept-Ranges: bytes`; +- `Content-Length`; +- 客户端发送 `Range: bytes=...` 时的 `206 Partial Content` 与有效 `Content-Range`;以及 +- 当浏览器代码需要读取这些头时的 + `Access-Control-Expose-Headers: Accept-Ranges, Content-Length, Content-Range`。 + +若发生 preflight,请允许 `GET`、`OPTIONS` 与 `Range` 请求头。带凭据请求不要使用通配符源。忽略 Range 的服务器可能触发完整下载,不是受支持的大型数据集配置。完整网络边界见 [兼容性](./compatibility)。 + +## 隐私 + +npm 库为零遥测契约。开源 Web 外壳不包含 Google Analytics 或 Microsoft Clarity。部署者负责披露其添加的日志、分析、代理、示例端点、认证与保留策略。见 [隐私](./privacy)。 diff --git a/docs/deprecation.md b/docs/deprecation.md new file mode 100644 index 0000000..ad553c5 --- /dev/null +++ b/docs/deprecation.md @@ -0,0 +1,30 @@ +# Deprecation policy + +> **Language / 语言:** [English](./deprecation.md) | [简体中文](./deprecation.zh-CN.md) + +LeRobot Studio follows Semantic Versioning for the public +`@ioai/lerobot-studio` API. + +## Major version 1 + +- Minor and patch releases must remain backward compatible with documented + public APIs and behavior. +- Deprecations must be documented in the changelog and migration guidance. +- A deprecated public API remains available for at least one minor release and + at least 90 days, whichever is longer, before removal in the next major + release. +- Security, privacy, legal, or data-integrity issues may require faster removal. + The release notes must explain the exception and provide mitigation or + migration guidance where practical. +- Undocumented internals, `src/**` paths, generated files, and standalone web + application UI details are not public API. + +## Dataset and browser support + +Removing a documented dataset version, browser class, input method, or export +target is a breaking change. Adding support for a new exact LeRobot version is +not automatic: it requires explicit validation and documentation. + +Warnings must identify the deprecated capability, the replacement, and the +earliest release in which removal may occur. Silent behavior changes are not an +acceptable deprecation mechanism. diff --git a/docs/deprecation.zh-CN.md b/docs/deprecation.zh-CN.md new file mode 100644 index 0000000..ff12977 --- /dev/null +++ b/docs/deprecation.zh-CN.md @@ -0,0 +1,19 @@ +# 弃用策略 + +> **Language / 语言:** [English](./deprecation.md) | [简体中文](./deprecation.zh-CN.md) + +LeRobot Studio 对公开 `@ioai/lerobot-studio` API 遵循 Semantic Versioning。 + +## Major 版本 1 + +- Minor 与 patch 发布须与已文档化的公开 API 与行为保持向后兼容。 +- 弃用须在 changelog 与迁移指南中记录。 +- 已弃用的公开 API 在下一个 major 发布移除前,至少保留一个 minor 发布且至少 90 天,以较长者为准。 +- 安全、隐私、法律或数据完整性问题可能需要更快移除。发布说明须解释例外,并在可行时提供缓解或迁移指南。 +- 未文档化的内部实现、`src/**` 路径、生成文件与独立 Web 应用 UI 细节不是公开 API。 + +## 数据集与浏览器支持 + +移除已文档化的数据集版本、浏览器类别、输入方式或导出目标属于 breaking change。新增对新的精确 LeRobot 版本的支持不是自动的:需要明确验证与文档。 + +警告须标识已弃用能力、替代方案,以及可能移除的最早发布。静默行为变更不是可接受的弃用机制。 diff --git a/docs/development.md b/docs/development.md index 7c34908..0cf79da 100644 --- a/docs/development.md +++ b/docs/development.md @@ -1,5 +1,7 @@ # Development +> **Language / 语言:** [English](./development.md) | [简体中文](./development.zh-CN.md) + ## Setup ```bash @@ -12,15 +14,52 @@ Node 24 is required (`.nvmrc`). This repository is a single npm package with two ## Commands -| Command | Purpose | -| --------------------------- | ------------------------------------------ | -| `npm run dev` | Start the SPA | -| `npm run build` | Build the SPA to `dist/` | -| `npm run build:lib` | Build the published library to `dist-lib/` | -| `npm run typecheck` | Type-check application and library sources | -| `npm run test:unit` | Run Node-based Vitest tests | -| `npm run test:browser` | Run Playwright browser tests | -| `npm run fixtures:generate` | Recreate synthetic LeRobot fixtures | +| Command | Purpose | +| --------------------------- | ----------------------------------------------- | +| `npm run dev` | Start the SPA | +| `npm run build` | Build the SPA to `dist/` | +| `npm run build:lib` | Build the published library to `dist-lib/` | +| `npm run docs:dev` | Start the VitePress documentation site | +| `npm run docs:build` | Build documentation and check links | +| `npm run check:docs-i18n` | Verify Markdown `.zh-CN.md` pairs or exceptions | +| `npm run check:bundle-size` | Enforce npm and Web bundle size budgets | +| `npm run typecheck` | Type-check application and library sources | +| `npm run test:unit` | Run Node-based Vitest tests | +| `npm run test:browser` | Run Playwright browser tests | +| `npm run fixtures:generate` | Recreate synthetic LeRobot fixtures | + +## Bundle size gate + +Build both targets before running the size gate: + +```bash +npm run build:lib +npm run build +npm run check:bundle-size +``` + +The gate measures raw and gzip sizes for the stable library entry, library and +initial Web CSS, Web entry/modulepreload chunks, lazy DataLoader and Dockview +chunks, Worker/WASM artifacts, and the packed npm tarball. It also fails when +required artifacts are missing, an unexpected JavaScript chunk exceeds the +per-chunk ceiling, or the complete npm tarball exceeds its packed or unpacked +budget. Hashed filenames are discovered from build output and `dist/index.html` +rather than being fixed in the script. + +The hard limits are approximately 5% above the measured baseline. The roughly +26 MB library Worker chunk is a known baseline that still needs optimization, +not an ideal or optimized target. Passing this gate only means that bundle size +has not materially regressed. + +For a local negative test, lower every budget without changing the checked-in +limits: + +```bash +BUNDLE_SIZE_BUDGET_SCALE=0.01 npm run check:bundle-size +``` + +The scale override can only tighten budgets (`0 < scale <= 1`); it cannot relax +the CI gate. ## Package validation @@ -29,6 +68,55 @@ npm run build:lib npm run verify:npm-consumer ``` +The 1.0 release requires consumer fixtures for Vite and Next.js client-only +usage, React/React DOM `^19.0.0`, ESM import, CSS loading, workers, and WASM. +Server rendering, React Server Components, CommonJS, and Node.js execution are +outside the package contract. + +### Docker npm consumer gate + +CI also runs an isolated Docker build that packs the library from the repository, +installs only the resulting tarball in fresh React 19 consumers, and verifies +type-checking, Vite production output, Next.js App Router client-only builds, +and static serving from a non-root mount path: + +```bash +npm run test:npm-consumer:docker +``` + +The Dockerfile lives in `tests/npm-consumer/`. Its primary build context +contains the isolated consumer fixtures, while the named BuildKit context +(`lerobot=.`) supplies the repository source used to create the package. The +pack stage runs `npm run build:lib` and then `npm pack`, exactly matching the +normal declaration, Vite, and API Extractor pipeline. Locally, when Docker is +unavailable, the runner validates the Dockerfile structure and falls back to +`npm run verify:npm-consumer`. + +The Docker consumers install **only** the packed tarball (never `file:../..` or +`src/`), pin React 19.2.8, type-check public API imports, build with Vite and +Next App Router (client-only/`ssr: false`), and assert the Vite bundle is +servable from a non-root mount path. + +## Compatibility work + +Use [Compatibility and release gates](./compatibility.md) as the acceptance +source for version and browser work. In particular: + +- match dataset versions exactly (`v2.1` and `v3.0`), never by major prefix; +- keep unknown versions warning-marked, read-only, and unable to reach any + export UI or service; +- treat official LeRobot training-readiness checks as required export tests; + and +- record actual browser versions and results rather than inferring support from + API detection. + +Changes to public behavior must follow the +[deprecation policy](./deprecation.md). + ## UI components UI primitives live in `src/ui` and are managed from the repository root with the shadcn CLI. Keep generated components in `src/ui/components` and use local `@/ui` or relative imports. Do not reintroduce `@radix-ui/*`; prefer Base UI composition via `render`. + +## Documentation i18n + +Every repository-owned English Markdown file must have a sibling `.zh-CN.md` copy or an explicit entry in `scripts/docs-i18n-exceptions.json`. Ephemeral directories such as `temp/` are listed in `skipScanDirs` and are not scanned. Language switchers use relative `./page.zh-CN.md` links so GitHub browsing works; VitePress rewrites those sources to `/zh-CN/*` routes. Run `npm run check:docs-i18n` to verify. See [Documentation i18n exceptions](./i18n-exceptions.md). diff --git a/docs/development.zh-CN.md b/docs/development.zh-CN.md new file mode 100644 index 0000000..7f0a2fb --- /dev/null +++ b/docs/development.zh-CN.md @@ -0,0 +1,99 @@ +# 开发 + +> **Language / 语言:** [English](./development.md) | [简体中文](./development.zh-CN.md) + +## 环境 + +```bash +npm ci +npm run fixtures:generate +npm run dev +``` + +需要 Node 24(`.nvmrc`)。本仓库是单个 npm 包,有两个构建目标。 + +## 命令 + +| 命令 | 用途 | +| --------------------------- | ----------------------------------- | +| `npm run dev` | 启动 SPA | +| `npm run build` | 构建 SPA 到 `dist/` | +| `npm run build:lib` | 构建已发布库到 `dist-lib/` | +| `npm run docs:dev` | 启动 VitePress 文档站点 | +| `npm run docs:build` | 构建文档并检查链接 | +| `npm run check:docs-i18n` | 检查 Markdown 中英文配对或显式例外 | +| `npm run check:bundle-size` | 强制执行 npm 与 Web bundle 大小预算 | +| `npm run typecheck` | 类型检查应用与库源码 | +| `npm run test:unit` | 运行基于 Node 的 Vitest 测试 | +| `npm run test:browser` | 运行 Playwright 浏览器测试 | +| `npm run fixtures:generate` | 重建合成 LeRobot fixture | + +## Bundle 大小门禁 + +运行大小门禁前先构建两个目标: + +```bash +npm run build:lib +npm run build +npm run check:bundle-size +``` + +门禁测量稳定库入口、库与初始 Web CSS、Web entry/modulepreload chunk、懒加载 DataLoader 与 Dockview chunk、Worker/WASM 制品,以及打包 npm tarball 的原始与 gzip 大小。还会在缺少必需制品、意外 JavaScript chunk 超过 per-chunk 上限,或完整 npm tarball 超过 packed/unpacked 预算时失败。哈希文件名从构建输出与 `dist/index.html` 发现,而非在脚本中写死。 + +硬限制约为测量基线的 5% 上方。约 26 MB 的库 Worker chunk 是已知基线,仍需优化,并非理想或已优化目标。通过此门禁仅表示 bundle 大小未出现显著回退。 + +本地负向测试可在不改变入库限制的情况下降低所有预算: + +```bash +BUNDLE_SIZE_BUDGET_SCALE=0.01 npm run check:bundle-size +``` + +scale 覆盖只能收紧预算(`0 < scale <= 1`);不能放松 CI 门禁。 + +## 包验证 + +```bash +npm run build:lib +npm run verify:npm-consumer +``` + +1.0 发布需要 Vite 与 Next.js 纯客户端用法的消费者 fixture、React/React DOM `^19.0.0`、ESM 导入、CSS 加载、Worker 与 WASM。服务端渲染、React Server Components、CommonJS 与 Node.js 执行不在包契约范围内。 + +### Docker npm 消费者门禁 + +CI 还会执行隔离的 Docker 构建:先从仓库生成 npm tarball,再仅安装该 +tarball 到全新的 React 19 消费者中,验证类型检查、Vite 生产构建、Next.js +App Router 纯客户端构建,以及非根路径下的静态服务: + +```bash +npm run test:npm-consumer:docker +``` + +Dockerfile 位于 `tests/npm-consumer/`。主构建上下文提供隔离的消费者 fixture, +命名 BuildKit 上下文 `lerobot=.` 提供用于打包的仓库源码。打包阶段执行 +`npm run build:lib` 后再执行 `npm pack`,与正常的声明、Vite 和 API Extractor +流水线一致。本机没有 Docker 时,运行器会检查 Dockerfile 结构,并回退执行 +`npm run verify:npm-consumer`。 + +Docker 消费者只安装打包后的 tarball,不使用 `file:../..` 或 `src/`;固定 +React 19.2.8,检查公开 API 类型,并构建 Vite 与 Next App Router +(纯客户端、`ssr: false`)示例。 + +## 兼容性工作 + +版本与浏览器工作的验收来源为 [兼容性与发布门禁](./compatibility)。尤其: + +- 精确匹配数据集版本(`v2.1` 与 `v3.0`),绝不按 major 前缀; +- 未知版本保持带警告、只读,且无法到达任何导出 UI 或服务; +- 将官方 LeRobot 训练就绪检查视为必需的导出测试;以及 +- 记录实际浏览器版本与结果,而非从 API 检测推断支持。 + +公开行为变更须遵循 [弃用策略](./deprecation)。 + +## UI 组件 + +UI 基元位于 `src/ui`,从仓库根目录用 shadcn CLI 管理。将生成组件保留在 `src/ui/components`,使用本地 `@/ui` 或相对导入。不要重新引入 `@radix-ui/*`;优先通过 `render` 使用 Base UI 组合。 + +## 文档国际化 + +每个仓库自有英文 Markdown 须有同目录 `.zh-CN.md` 副本,或在 `scripts/docs-i18n-exceptions.json` 中登记显式例外。`temp/` 等临时目录列入 `skipScanDirs`,不参与扫描。语言切换行使用相对路径 `./page.zh-CN.md`,以便在 GitHub 上直接阅读;VitePress 将这些源文件重写为 `/zh-CN/*` 路由。运行 `npm run check:docs-i18n` 验证。例外说明见 [文档国际化例外清单](./i18n-exceptions)。 diff --git a/docs/github-migration.md b/docs/github-migration.md index 225a39a..f0256e4 100644 --- a/docs/github-migration.md +++ b/docs/github-migration.md @@ -1,5 +1,7 @@ # Migrating to a fresh GitHub repository +> **Language / 语言:** [English](./github-migration.md) | [简体中文](./github-migration.zh-CN.md) + This branch is prepared to be copied into a new public GitHub repository **without private Git history**. @@ -10,7 +12,7 @@ This branch is prepared to be copied into a new public GitHub repository ```bash rsync -a --exclude '.git' --exclude 'node_modules' --exclude 'dist' \ - --exclude 'dist' --exclude 'dist-lib' \ + --exclude 'dist-lib' \ ./ /path/to/lerobot-studio-public/ ``` @@ -25,13 +27,61 @@ git remote add origin git@github.com:ioai-tech/lerobot-studio.git git push -u origin main ``` -4. Configure GitHub: - - Protect `main` - - Add `NPM_TOKEN` repository secret for releases +4. Configure GitHub (these are manual administrator actions; the files in this + repository do not enable repository or organization settings): + - In **Settings → Actions → General**, set the default workflow token to + read-only and leave “Allow GitHub Actions to create and approve pull + requests” disabled. + - In the fork pull-request settings, do not send write tokens or Actions + secrets to workflows from forks. Require approval for first-time or + outside contributors. The checked-in PR workflows use `pull_request` + rather than `pull_request_target`, request read-only contents by default, + and do not consume repository secrets. + - Create a branch ruleset for `main`: require a pull request, at least one + approval, CODEOWNERS review, conversation resolution, and successful + required checks; block force pushes and deletion. Select the CI jobs + (`quality`, `browser-tests`, and `official-compat`), Dependency Review, and + CodeQL checks after they have run once so GitHub exposes their exact check + names. + - Create the `@ioai-tech/maintainers` team referenced by `CODEOWNERS`, or + replace that placeholder with an existing team that has write access + - In **Settings → Code security**, enable the dependency graph, Dependabot + alerts and security updates, secret scanning and push protection, and + private vulnerability reporting. Use the checked-in CodeQL advanced-setup + workflow; do not also enable CodeQL default setup. + - Verify the Dependency Review, CodeQL, and OpenSSF Scorecard workflows + succeed. Scorecard publication sends public repository assessment data to + `api.scorecard.dev`; disable `publish_results` in + `.github/workflows/scorecard.yml` if that publication is not desired. + - Keep GitHub Actions restricted to GitHub-authored and explicitly approved + third-party actions where organization policy supports it. All non-release + workflows are pinned to full commit SHAs and Dependabot is configured to + propose GitHub Actions updates. + - Add a one-time `NPM_BOOTSTRAP_TOKEN` repository secret for the initial + `v1.0.0` publication. npm cannot configure a trusted publisher before the + scoped package exists. + - Immediately after `v1.0.0` exists, configure npm Trusted Publishing for + `ioai-tech/lerobot-studio` and `.github/workflows/release.yml`, then delete + the bootstrap secret. Later releases use GitHub OIDC and no npm token. - Enable GitHub Packages / GHCR permissions for Actions + - Add a tag ruleset for `v*` so only release maintainers can create or update + release tags. - Verify Actions workflows run on the first PR + - In **Settings → Pages**, choose **GitHub Actions** as the source, run the + Documentation Pages workflow, and verify the project Pages URL. A custom + `docs.lerobot.studio` domain and its DNS records are optional later + administrator work; they are not preconfigured by this repository. - Connect the repo to Cloudflare Workers Builds for `main` → https://lerobot.studio (no Cloudflare API tokens needed in GitHub Secrets; use Cloudflare’s Git integration) - Confirm Worker `lerobot-studio` and custom domain `lerobot.studio` remain bound -5. Cut the first release tag (`v1.0.0` or `v0.1.0`) to exercise the release workflow. +5. Complete every gate in [Compatibility](./compatibility.md), including exact + dataset-version handling and official training-readiness validation. +6. Run the `Release Dry Run` workflow. It executes the reusable CI, + `npm publish --dry-run`, CycloneDX SBOM generation, and a no-push container + build without requiring release credentials. +7. Follow + [Governance](https://github.com/ioai-tech/lerobot-studio/blob/main/GOVERNANCE.md) + to approve and cut the stable `v1.0.0` release. Published npm versions, + container tags, and GitHub release artifacts are immutable; the release + attaches the SBOM and provenance. diff --git a/docs/github-migration.zh-CN.md b/docs/github-migration.zh-CN.md new file mode 100644 index 0000000..40b89a9 --- /dev/null +++ b/docs/github-migration.zh-CN.md @@ -0,0 +1,50 @@ +# 迁移到全新的 GitHub 仓库 + +> **Language / 语言:** [English](./github-migration.md) | [简体中文](./github-migration.zh-CN.md) + +本分支已准备好复制到新的公开 GitHub 仓库,**不包含私有 Git 历史**。 + +## 步骤 + +1. 创建空的公开仓库(推荐:`ioai-tech/lerobot-studio`)。 +2. 复制最终 worktree(排除 `.git`、`node_modules`、`dist`、构建缓存): + +```bash +rsync -a --exclude '.git' --exclude 'node_modules' --exclude 'dist' \ + --exclude 'dist-lib' \ + ./ /path/to/lerobot-studio-public/ +``` + +3. 初始化干净历史: + +```bash +cd /path/to/lerobot-studio-public +git init -b main +git add . +git commit -m "chore: initial public release of LeRobot Studio" +git remote add origin git@github.com:ioai-tech/lerobot-studio.git +git push -u origin main +``` + +4. 配置 GitHub(以下为管理员手工操作;本仓库中的文件不会启用仓库或组织设置): + - 在 **Settings → Actions → General** 中,将默认工作流 token 设为只读,并禁用 “Allow GitHub Actions to create and approve pull requests”。 + - 在 Fork Pull Request 设置中,不要向来自 Fork 的工作流发送 write token 或 Actions secret。要求首次或外部贡献者批准。已检入的 PR 工作流使用 `pull_request` 而非 `pull_request_target`,默认请求只读 contents,且不消耗仓库 secret。 + - 为 `main` 创建分支 ruleset:要求 Pull Request、至少一次批准、CODEOWNERS 审查、对话解决与必需检查成功;阻止 force push 与删除。在 CI 作业(`quality`、`browser-tests`、`official-compat`)、Dependency Review 与 CodeQL 检查各运行一次后选中它们,以便 GitHub 暴露精确检查名称。 + - 创建 `CODEOWNERS` 引用的 `@ioai-tech/maintainers` 团队,或替换为已有 write 权限的团队。 + - 在 **Settings → Code security** 中启用依赖图、Dependabot 告警与安全更新、secret scanning 与 push protection,以及私有漏洞报告。使用已检入的 CodeQL advanced-setup 工作流;不要同时启用 CodeQL default setup。 + - 验证 Dependency Review、CodeQL 与 OpenSSF Scorecard 工作流成功。Scorecard 发布会向 `api.scorecard.dev` 发送公开仓库评估数据;若不需要该发布,请在 `.github/workflows/scorecard.yml` 中禁用 `publish_results`。 + - 在组织策略支持的情况下,将 GitHub Actions 限制为 GitHub -authored 与明确批准的第三方 actions。所有非 release 工作流 pin 到完整 commit SHA,并由 Dependabot 配置提议 GitHub Actions 更新。 + - 为初始 `v1.0.0` 发布添加一次性 `NPM_BOOTSTRAP_TOKEN` 仓库 secret。在 scoped 包存在之前,npm 无法配置 trusted publisher。 + - `v1.0.0` 存在后立即为 `ioai-tech/lerobot-studio` 与 `.github/workflows/release.yml` 配置 npm Trusted Publishing,然后删除 bootstrap secret。后续发布使用 GitHub OIDC,无需 npm token。 + - 为 Actions 启用 GitHub Packages / GHCR 权限。 + - 为 `v*` 添加 tag ruleset,使仅 release 维护者可创建或更新 release 标签。 + - 验证 Actions 工作流在首个 PR 上运行。 + - 在 **Settings → Pages** 中选择 **GitHub Actions** 作为源,运行 Documentation Pages 工作流,并验证项目 Pages URL。自定义 `docs.lerobot.studio` 域与其 DNS 记录是可选的后续管理员工作;本仓库未预配置。 + - 将仓库连接到 Cloudflare Workers Builds,使 `main` → https://lerobot.studio(GitHub Secrets 中无需 Cloudflare API token;使用 Cloudflare Git 集成)。 + - 确认 Worker `lerobot-studio` 与自定义域 `lerobot.studio` 保持绑定。 + +5. 完成 [兼容性](./compatibility) 中的每个门禁,包括精确数据集版本处理与官方训练就绪验证。 +6. 运行 `Release Dry Run` 工作流。它执行可复用 CI、`npm publish --dry-run`、CycloneDX SBOM 生成,以及无需 release 凭据的无 push 容器构建。 +7. 遵循 + [治理](https://github.com/ioai-tech/lerobot-studio/blob/main/GOVERNANCE.md) + 批准并发布稳定 `v1.0.0`。已发布的 npm 版本、容器标签与 GitHub release 制品不可变;release 附带 SBOM 与来源证明。 diff --git a/docs/i18n-exceptions.md b/docs/i18n-exceptions.md new file mode 100644 index 0000000..8a67b0b --- /dev/null +++ b/docs/i18n-exceptions.md @@ -0,0 +1,41 @@ +# Documentation i18n exceptions + +> **Language / 语言:** [English](./i18n-exceptions.md) | [简体中文](./i18n-exceptions.zh-CN.md) + +LeRobot Studio keeps English and Simplified Chinese (`.zh-CN.md`) copies of +every repository-owned Markdown file. A CI check (`npm run check:docs-i18n`) +enforces pairing or an explicit exception entry in +[`scripts/docs-i18n-exceptions.json`](../scripts/docs-i18n-exceptions.json). + +## Machine-generated reports + +| Path | Reason | Chinese entry | +| --------------------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------- | +| `etc/lerobot-studio.api.md` | API Extractor output; regenerate with `npm run api:report` | [`docs/api.zh-CN.md`](./api.zh-CN.md) wraps the same English report with Chinese guidance | + +Do not hand-edit generated API reports or create translated copies of them. +Update public declarations in library source, run `npm run api:report`, and +keep the Chinese API page’s explanatory text in sync. + +## Directories excluded from scans + +These paths are intentionally **not** scanned for Markdown pairing. They do not +need exception entries: + +| Directory | Reason | +| --------- | --------------------------------------------------------------------------------------------- | +| `temp/` | Ephemeral build output (for example local API Extractor copies). Not published documentation. | + +If a path under an excluded directory appears in `exceptions`, `check:docs-i18n` +fails as redundant. + +## Adding a new exception + +1. Confirm the file is generated, third-party, or otherwise unsuitable for a + `.zh-CN.md` sibling and is **not** under a `skipScanDirs` directory. +2. Add an object to `scripts/docs-i18n-exceptions.json` with `path`, `reason`, + `zhEntry` (path to the Chinese explanation page, or `null`), and bilingual + `noteEn` / `noteZh` fields. +3. Document the exception on this page and in + [`i18n-exceptions.zh-CN.md`](./i18n-exceptions.zh-CN.md). +4. Run `npm run check:docs-i18n`. diff --git a/docs/i18n-exceptions.zh-CN.md b/docs/i18n-exceptions.zh-CN.md new file mode 100644 index 0000000..56ab77c --- /dev/null +++ b/docs/i18n-exceptions.zh-CN.md @@ -0,0 +1,35 @@ +# 文档国际化例外清单 + +> **Language / 语言:** [English](./i18n-exceptions.md) | [简体中文](./i18n-exceptions.zh-CN.md) + +LeRobot Studio 为仓库内每一份英文 Markdown 维护简体中文副本(`.zh-CN.md`)。 +CI 通过 `npm run check:docs-i18n` 检查配对关系,或在 +[`scripts/docs-i18n-exceptions.json`](../scripts/docs-i18n-exceptions.json) +中登记显式例外。 + +## 机器生成报告 + +| 路径 | 原因 | 中文入口 | +| --------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------ | +| `etc/lerobot-studio.api.md` | API Extractor 输出;用 `npm run api:report` 重新生成 | [`docs/api.zh-CN.md`](./api.zh-CN.md) 以中文说明包裹同一份英文报告 | + +请勿手工编辑或翻译生成的 API 报告。应修改库源码中的公开声明、运行 +`npm run api:report`,并同步更新中文 API 页中的说明文字。 + +## 不参与扫描的目录 + +以下路径**不会**纳入 Markdown 配对扫描,也**不需要**登记为例外: + +| 目录 | 原因 | +| ------- | --------------------------------------------------------- | +| `temp/` | 临时构建输出(例如本地 API Extractor 副本),非发布文档。 | + +若 `exceptions` 中出现位于上述目录下的路径,`check:docs-i18n` 会报冗余错误。 + +## 新增例外 + +1. 确认文件为生成物、第三方内容或其他不适合 `.zh-CN.md` 配对的情况,且**不在** `skipScanDirs` 目录下。 +2. 在 `scripts/docs-i18n-exceptions.json` 中添加 `path`、`reason`、 + `zhEntry`(中文说明页路径,或 `null`)以及双语 `noteEn` / `noteZh`。 +3. 在本页与 [`i18n-exceptions.md`](./i18n-exceptions.md) 中记录该例外。 +4. 运行 `npm run check:docs-i18n`。 diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..27aa7c5 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,38 @@ +--- +layout: home + +hero: + name: LeRobot Studio + text: Inspect LeRobot datasets in the browser + tagline: A client-only React viewer for synchronized video, charts, and raw features. + actions: + - theme: brand + text: Quick Start + link: /quick-start + - theme: alt + text: Stable API + link: /api + - theme: alt + text: Live demo + link: https://lerobot.studio + +features: + - title: Viewer-only contract + details: Embed the stable client-side viewer without exposing dataset mutation or editor APIs. + - title: Browser-local processing + details: Local dataset content stays on the device. The npm library has a zero-telemetry contract. + - title: Explicit compatibility + details: Exact dataset versions, browser capabilities, and release gates are documented separately. +--- + +> **Language / 语言:** [English](./index.md) | [简体中文](./index.zh-CN.md) + +## Current release status + +LeRobot Studio is preparing the stable `1.0.0` contract. The package is not yet +published. Viewing is the only stable public library workflow; source mutation, +editing, and export services are not part of the stable API. + +Start with the [Quick Start](./quick-start.md), then review the +[compatibility and release gates](./compatibility.md) before treating a dataset +format or browser capability as supported. diff --git a/docs/index.zh-CN.md b/docs/index.zh-CN.md new file mode 100644 index 0000000..df58d0a --- /dev/null +++ b/docs/index.zh-CN.md @@ -0,0 +1,34 @@ +--- +layout: home + +hero: + name: LeRobot Studio + text: 在浏览器中查看 LeRobot 数据集 + tagline: 纯客户端 React 查看器,同步展示视频、图表与原始特征。 + actions: + - theme: brand + text: 快速开始 + link: /zh-CN/quick-start + - theme: alt + text: 稳定 API + link: /zh-CN/api + - theme: alt + text: 在线演示 + link: https://lerobot.studio + +features: + - title: 仅查看契约 + details: 嵌入稳定的客户端查看器,不暴露数据集修改或编辑器 API。 + - title: 浏览器本地处理 + details: 本地数据集内容保留在设备上。npm 库为零遥测契约。 + - title: 明确的兼容性 + details: 精确的数据集版本、浏览器能力与发布门禁分别记录于文档。 +--- + +> **Language / 语言:** [English](./index.md) | [简体中文](./index.zh-CN.md) + +## 当前发布状态 + +LeRobot Studio 正在准备稳定的 `1.0.0` 契约。包尚未发布。查看是唯一稳定的公开库工作流;源数据修改、编辑与导出服务不属于稳定 API。 + +请从 [快速开始](./quick-start) 入手,在将某数据集格式或浏览器能力视为受支持之前,先阅读 [兼容性与发布门禁](./compatibility)。 diff --git a/docs/migration.md b/docs/migration.md new file mode 100644 index 0000000..c3bccdb --- /dev/null +++ b/docs/migration.md @@ -0,0 +1,49 @@ +# Migration + +> **Language / 语言:** [English](./migration.md) | [简体中文](./migration.zh-CN.md) + +## Before 1.0 + +The package has not been published, so there is no supported npm upgrade path +yet. Do not depend on `src/**`, generated declarations, standalone application +components, export services, or other implementation details. + +Prepare an embedding application by depending only on the +[viewer-only API report](./api.md): + +- `LeRobotViewer` +- `createArchiveDataSourceFromFile` +- `createArchiveDataSourceFromUrl` +- the public viewer and `DataSource` types +- `@ioai/lerobot-studio/style.css` + +An `onExport` handler is a host-owned button callback, not an exported dataset +service. + +## Moving from application internals + +1. Replace deep imports with the package root. +2. Move export, authentication, signed URL, and proxy behavior into the host + application. +3. Supply a remote URL or implement the stable `DataSource` interface. +4. Render only on the client and give the viewer container an explicit height. +5. Treat all non-exact dataset versions as unknown instead of choosing an + adapter by major prefix. + +## After 1.0 + +Major version 1 follows Semantic Versioning. Minor and patch releases retain +backward compatibility for documented public APIs. A deprecated API remains +available for at least one minor release and 90 days, except where security, +privacy, legal, or data-integrity issues require faster action. + +Before upgrading: + +1. read the project changelog and release notes; +2. review documented deprecations and exact dataset-version support; +3. rebuild a real Vite or client-only Next.js consumer; and +4. test target browsers and representative dataset codecs. + +See the [deprecation policy](./deprecation.md) for the complete contract. The +repository publication procedure is documented separately in +[GitHub migration](./github-migration.md). diff --git a/docs/migration.zh-CN.md b/docs/migration.zh-CN.md new file mode 100644 index 0000000..b1f9d60 --- /dev/null +++ b/docs/migration.zh-CN.md @@ -0,0 +1,38 @@ +# 迁移 + +> **Language / 语言:** [English](./migration.md) | [简体中文](./migration.zh-CN.md) + +## 1.0 之前 + +包尚未发布,因此尚无受支持的 npm 升级路径。请勿依赖 `src/**`、生成的声明、独立应用组件、导出服务或其他实现细节。 + +请仅依赖 [仅查看 API 报告](./api) 准备嵌入应用: + +- `LeRobotViewer` +- `createArchiveDataSourceFromFile` +- `createArchiveDataSourceFromUrl` +- 公开查看器与 `DataSource` 类型 +- `@ioai/lerobot-studio/style.css` + +`onExport` 处理器是宿主拥有的按钮回调,不是导出的数据集服务。 + +## 从应用内部实现迁移 + +1. 用包根导入替换深层导入。 +2. 将导出、认证、签名 URL 与代理行为移入宿主应用。 +3. 提供远程 URL 或实现稳定的 `DataSource` 接口。 +4. 仅在客户端渲染,并为查看器容器提供明确高度。 +5. 将所有非精确数据集版本视为未知,而非按 major 前缀选择适配器。 + +## 1.0 之后 + +Major 1 遵循 Semantic Versioning。Minor 与 patch 发布对已文档化的公开 API 保持向后兼容。已弃用 API 至少保留一个 minor 发布与 90 天,除非安全、隐私、法律或数据完整性问题需要更快行动。 + +升级前: + +1. 阅读项目 changelog 与 release 说明; +2. 审查已记录的弃用说明与精确数据集版本支持; +3. 重建真实的 Vite 或纯客户端 Next.js 消费者;以及 +4. 在目标浏览器与代表性数据集编解码器上测试。 + +完整契约见 [弃用策略](./deprecation)。仓库发布流程单独记录在 [GitHub 迁移](./github-migration)。 diff --git a/docs/privacy.md b/docs/privacy.md new file mode 100644 index 0000000..a1339c0 --- /dev/null +++ b/docs/privacy.md @@ -0,0 +1,57 @@ +# Privacy + +> **Language / 语言:** [English](./privacy.md) | [简体中文](./privacy.zh-CN.md) + +## Data processing + +LeRobot Studio is designed to process dataset content in the browser. Opening a +local file or directory does not intentionally upload its contents. Opening a +remote archive necessarily sends requests to the user-provided remote host, and +that host may log normal request data such as IP address, user agent, referrer, +and requested URL. + +The `@ioai/lerobot-studio` npm library has a zero-telemetry contract. It must not +send analytics, diagnostics, dataset metadata, filenames, usage events, or +identifiers to IO-AI.TECH or third parties. Host applications remain +responsible for disclosing any telemetry or network behavior they add around +the library. + +## Browser storage + +The current application may store: + +- UI preferences such as theme and language in `localStorage`; +- recent-source metadata in `localStorage`, including labels, source kinds, + remote URLs, and local path-like display labels; and +- user-granted `FileSystemHandle` objects in the `lerobot-studio` IndexedDB + database when the browser supports handle persistence. + +Dataset payload bytes are not intentionally persisted in these stores by the +history feature. A stored handle does not bypass browser permission checks; the +browser may require the user to grant access again. Private browsing, +permissions, quota, or browser policy may disable persistence without disabling +all viewing. + +Users can remove recent items in the application. Clearing site data in browser +settings removes the application’s `localStorage` and IndexedDB data. Removing +browser storage does not delete source files or data held by remote hosts. + +## Network boundaries + +The standalone application may contact: + +- the URL explicitly opened by the user; +- configured sample-dataset endpoints; and +- same-origin application assets. + +The npm library must not introduce undisclosed network destinations. Embedding +applications should apply an appropriate Content Security Policy and document +their own sample endpoints, proxies, authentication, logging, and retention. + +## Sensitive datasets + +Do not assume that browser-local processing makes a dataset non-sensitive. +Filenames, paths, video frames, images, task descriptions, and sensor data may +contain personal or confidential information. Use a trusted device and origin, +review remote URLs before opening them, and follow the dataset owner’s access +and retention requirements. diff --git a/docs/privacy.zh-CN.md b/docs/privacy.zh-CN.md new file mode 100644 index 0000000..c64116f --- /dev/null +++ b/docs/privacy.zh-CN.md @@ -0,0 +1,35 @@ +# 隐私 + +> **Language / 语言:** [English](./privacy.md) | [简体中文](./privacy.zh-CN.md) + +## 数据处理 + +LeRobot Studio 设计为在浏览器中处理数据集内容。打开本地文件或目录不会故意上传其内容。打开远程归档必然向用户提供的远程主机发送请求,该主机可能记录 IP 地址、User-Agent、Referer 与请求 URL 等正常请求数据。 + +`@ioai/lerobot-studio` npm 库为零遥测契约。它不得向 IO-AI.TECH 或第三方发送分析、诊断、数据集元数据、文件名、使用事件或标识符。宿主应用仍负责披露其在库周围添加的任何遥测或网络行为。 + +## 浏览器存储 + +当前应用可能存储: + +- UI 偏好(如主题与语言)于 `localStorage`; +- 最近来源元数据于 `localStorage`,包括标签、来源类型、远程 URL 与类路径的本地显示标签;以及 +- 当浏览器支持句柄持久化时,用户授予的 `FileSystemHandle` 对象于 `lerobot-studio` IndexedDB 数据库。 + +历史功能不会故意在这些存储中持久化数据集载荷字节。存储的句柄不会绕过浏览器权限检查;浏览器可能要求用户再次授权。隐私浏览、权限、配额或浏览器策略可能禁用持久化而不禁用所有查看。 + +用户可在应用中移除最近项。在浏览器设置中清除站点数据会移除应用的 `localStorage` 与 IndexedDB 数据。移除浏览器存储不会删除源文件或远程主机持有的数据。 + +## 网络边界 + +独立应用可能联系: + +- 用户明确打开的 URL; +- 配置的示例数据集端点;以及 +- 同源应用资源。 + +npm 库不得引入未披露的网络目的地。嵌入应用应应用合适的内容安全策略,并记录自己的示例端点、代理、认证、日志与保留策略。 + +## 敏感数据集 + +不要假设浏览器本地处理使数据集不敏感。文件名、路径、视频帧、图像、任务描述与传感器数据可能包含个人或机密信息。请使用可信设备与源,打开远程 URL 前仔细审查,并遵循数据集所有者的访问与保留要求。 diff --git a/docs/quick-start.md b/docs/quick-start.md new file mode 100644 index 0000000..fe1b89a --- /dev/null +++ b/docs/quick-start.md @@ -0,0 +1,78 @@ +# Quick Start + +> **Language / 语言:** [English](./quick-start.md) | [简体中文](./quick-start.zh-CN.md) + +LeRobot Studio has two entry points: + +- the hosted application at [lerobot.studio](https://lerobot.studio); and +- the browser-only React package, after `@ioai/lerobot-studio` is published. + +## Use the hosted viewer + +Open the live application and choose a local folder, local archive, or remote +archive. Local content is processed in the browser. Remote archives must meet +the documented [CORS and HTTP Range requirements](./cors.md). + +## Embed the React viewer + +::: warning Publication status +`@ioai/lerobot-studio@1.0.0` is not published yet. Installation and the stable +contract below become available only after the [1.0 release gates](./compatibility.md#_1-0-acceptance-gates) +pass. +::: + +```bash +npm install @ioai/lerobot-studio +``` + +```tsx +import { LeRobotViewer } from '@ioai/lerobot-studio'; +import '@ioai/lerobot-studio/style.css'; + +export function DatasetViewer() { + return ( +
+ +
+ ); +} +``` + +The host must give the viewer an explicit height. The component fills its +container and does not choose a page height. + +## Runtime contract + +- React and React DOM `^19.0.0` +- ESM only +- browser and client-side rendering only +- no Node.js, CommonJS, React Server Components, SSR, or hydration support + +In Next.js, load the viewer from a Client Component with SSR disabled: + +```tsx +'use client'; + +import dynamic from 'next/dynamic'; + +const Viewer = dynamic( + () => import('@ioai/lerobot-studio').then((module) => module.LeRobotViewer), + { ssr: false }, +); +``` + +See the [stable API](./api.md) for custom data sources, callbacks, and errors. + +## Develop this repository + +```bash +npm ci +npm run fixtures:generate +npm run dev +``` + +To work on this documentation site: + +```bash +npm run docs:dev +``` diff --git a/docs/quick-start.zh-CN.md b/docs/quick-start.zh-CN.md new file mode 100644 index 0000000..1f5999e --- /dev/null +++ b/docs/quick-start.zh-CN.md @@ -0,0 +1,73 @@ +# 快速开始 + +> **Language / 语言:** [English](./quick-start.md) | [简体中文](./quick-start.zh-CN.md) + +LeRobot Studio 有两个入口: + +- 托管应用 [lerobot.studio](https://lerobot.studio);以及 +- 浏览器专用 React 包(`@ioai/lerobot-studio` 发布后)。 + +## 使用托管查看器 + +打开在线应用,选择本地文件夹、本地归档或远程归档。本地内容在浏览器中处理。远程归档须满足 [CORS 与 HTTP Range 要求](./cors) 中的约定。 + +## 嵌入 React 查看器 + +::: warning 发布状态 +`@ioai/lerobot-studio@1.0.0` 尚未发布。安装与下方稳定契约仅在 [1.0 发布门禁](./compatibility#_1-0-acceptance-gates) 通过后可用。 +::: + +```bash +npm install @ioai/lerobot-studio +``` + +```tsx +import { LeRobotViewer } from '@ioai/lerobot-studio'; +import '@ioai/lerobot-studio/style.css'; + +export function DatasetViewer() { + return ( +
+ +
+ ); +} +``` + +宿主必须为查看器提供明确高度。组件会填满容器,不会自行决定页面高度。 + +## 运行时契约 + +- React 与 React DOM `^19.0.0` +- 仅 ESM +- 仅浏览器与客户端渲染 +- 不支持 Node.js、CommonJS、React Server Components、SSR 或 hydration + +在 Next.js 中,从 Client Component 加载查看器并禁用 SSR: + +```tsx +'use client'; + +import dynamic from 'next/dynamic'; + +const Viewer = dynamic( + () => import('@ioai/lerobot-studio').then((module) => module.LeRobotViewer), + { ssr: false }, +); +``` + +自定义数据源、回调与错误处理见 [稳定 API](./api)。 + +## 开发本仓库 + +```bash +npm ci +npm run fixtures:generate +npm run dev +``` + +开发本文档站点: + +```bash +npm run docs:dev +``` diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..0479146 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,60 @@ +# Troubleshooting + +> **Language / 语言:** [English](./troubleshooting.md) | [简体中文](./troubleshooting.zh-CN.md) + +## The viewer has zero height + +`LeRobotViewer` fills its container. Give the host element an explicit height +or a layout constraint that resolves to a non-zero height. + +```tsx +
+ +
+``` + +## A remote archive does not open + +1. Confirm the final URL is reachable with `GET` from the viewer origin. +2. Inspect redirects; every final response must preserve the required CORS + headers. +3. Send a small byte request and verify a `206 Partial Content` response with a + valid `Content-Range`. +4. Expose `Accept-Ranges`, `Content-Length`, and `Content-Range` to browser + code. +5. Check whether a signed URL expired or a CDN removed the `Range` header. + +See [CORS and HTTP Range](./cors.md) for example headers. + +## An archive type is rejected + +The public archive factory accepts ZIP, TAR, and TAR.GZ files. For remote +sources, use a URL and response that allow the archive kind to be identified. +Use `onFatalError` to distinguish `UNSUPPORTED_ARCHIVE` from network and +dataset failures. + +## A dataset opens with a warning + +Only exact declared versions `v2.1` and `v3.0` are intended for 1.0 support. +Any other or missing `codebase_version` is unknown. Safe inspection may remain +available in warning-marked read-only mode, but export must stay disabled. + +## Video is unavailable + +Playback depends on the browser and codec used by the dataset. Test the same +asset in the target browser and inspect its media errors. A viewer smoke test +does not establish support for every codec. Non-media data should remain +inspectable where safe. + +## Next.js fails during server rendering + +Render the viewer from a Client Component and load it dynamically with +`ssr: false`. The package is browser-only and does not support React Server +Components or server rendering. See [Quick Start](./quick-start.md). + +## A custom data source leaks memory + +Revoke generated object URLs in `invalidateObjectUrl()` or `clear()`, release +archive handles, and ensure repeated reads do not retain full buffers without a +bounded cache. The host owns the lifecycle of a custom +[`DataSource`](./api.md). diff --git a/docs/troubleshooting.zh-CN.md b/docs/troubleshooting.zh-CN.md new file mode 100644 index 0000000..2bb23d8 --- /dev/null +++ b/docs/troubleshooting.zh-CN.md @@ -0,0 +1,43 @@ +# 故障排查 + +> **Language / 语言:** [English](./troubleshooting.md) | [简体中文](./troubleshooting.zh-CN.md) + +## 查看器高度为零 + +`LeRobotViewer` 会填满其容器。为宿主元素提供明确高度,或提供可解析为非零高度的布局约束。 + +```tsx +
+ +
+``` + +## 远程归档无法打开 + +1. 确认最终 URL 可从查看器源用 `GET` 访问。 +2. 检查重定向;每个最终响应须保留必需的 CORS 头。 +3. 发送小字节请求,验证 `206 Partial Content` 与有效的 `Content-Range`。 +4. 向浏览器代码暴露 `Accept-Ranges`、`Content-Length` 与 `Content-Range`。 +5. 检查签名 URL 是否过期,或 CDN 是否移除了 `Range` 头。 + +示例头见 [CORS 与 HTTP Range](./cors)。 + +## 归档类型被拒绝 + +公开归档工厂接受 ZIP、TAR 与 TAR.GZ 文件。对于远程源,请使用允许识别归档种类的 URL 与响应。使用 `onFatalError` 区分 `UNSUPPORTED_ARCHIVE` 与网络及数据集失败。 + +## 数据集打开但带警告 + +仅精确声明的版本 `v2.1` 与 `v3.0` 为 1.0 目标支持范围。任何其他或缺失的 `codebase_version` 均为未知。在安全时可能仍以带警告的只读模式提供检查,但导出须保持禁用。 + +## 视频不可用 + +播放取决于浏览器与数据集使用的编解码器。在目标浏览器中测试相同资源并检查媒体错误。查看器冒烟测试并不建立对每个编解码器的支持。在安全时非媒体数据仍应可检查。 + +## Next.js 在服务端渲染时失败 + +从 Client Component 渲染查看器,并用 `ssr: false` 动态加载。包仅适用于浏览器,不支持 React Server Components 或服务端渲染。见 [快速开始](./quick-start)。 + +## 自定义数据源泄漏内存 + +在 `invalidateObjectUrl()` 或 `clear()` 中撤销生成的 object URL,释放归档句柄,并确保重复读取不会在无界缓存中保留完整 buffer。自定义 [`DataSource`](./api) 的生命周期由宿主拥有。 diff --git a/eslint.config.js b/eslint.config.js index b7d2d7f..41b81bc 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -13,8 +13,6 @@ export default defineConfig([ 'node_modules/**', 'tests/fixtures/datasets/**', '**/*.d.ts', - // Pre-monorepo leftover tree; packages/* + apps/* are authoritative. - 'src/**', ]), { files: ['**/*.{ts,tsx}'], @@ -44,8 +42,26 @@ export default defineConfig([ '@typescript-eslint/no-explicit-any': 'off', // ESLint 10 新增:既有 Error 包装模式未统一 attach cause 'preserve-caught-error': 'off', - // shadcn / Context 模式会同时导出组件与 hook/variants - 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + // Context hooks and shadcn variants intentionally share their component module. + 'react-refresh/only-export-components': [ + 'warn', + { + allowConstantExport: true, + allowExportNames: [ + 'useDockviewApi', + 'useLoading', + 'useToast', + 'useI18nController', + 'useTranslationBridge', + 'reportIntlError', + 'badgeVariants', + 'buttonVariants', + 'tabsListVariants', + 'usePortalContainer', + 'metadata', + ], + }, + ], }, }, ]); diff --git a/etc/lerobot-studio.api.md b/etc/lerobot-studio.api.md new file mode 100644 index 0000000..b3ec763 --- /dev/null +++ b/etc/lerobot-studio.api.md @@ -0,0 +1,68 @@ +## API Report File for "@ioai/lerobot-studio" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { default as React_2 } from 'react'; + +// @public +export function createArchiveDataSourceFromFile(file: File): DataSource; + +// @public +export function createArchiveDataSourceFromUrl(url: string): DataSource; + +// @public +export interface DataSource { + clear(): void | Promise; + exists(path: string): Promise; + getObjectUrl(path: string, mimeType?: string, onProgress?: ProgressHandler): Promise; + invalidateObjectUrl?(path: string): void | Promise; + listPaths?(): Promise; + readBytes(path: string, onProgress?: ProgressHandler): Promise; + readText(path: string, onProgress?: ProgressHandler): Promise; +} + +// @public (undocumented) +export const LeRobotViewer: React_2.FC; + +// @public +export interface LeRobotViewerError { + cause?: unknown; + code: LeRobotViewerErrorCode; + message: string; + recoverable: boolean; +} + +// @public +export type LeRobotViewerErrorCode = 'INVALID_DATA_SOURCE' | 'REMOTE_SOURCE_UNAVAILABLE' | 'UNSUPPORTED_ARCHIVE' | 'DATASET_LOAD_FAILED'; + +// @public +export interface LeRobotViewerProps { + className?: string; + dataSource: string | DataSource; + enableKeyboardShortcuts?: boolean; + language?: string; + onBack?: () => void; + onExport?: () => void; + onFatalError?: (error: LeRobotViewerError) => void; + showPlaybackBar?: boolean; + showSidebar?: boolean; + theme?: 'light' | 'dark' | 'system'; +} + +// @public +export type LoadingPhase = 'download' | 'index' | 'gunzip' | 'read'; + +// @public +export type ProgressHandler = (info: ProgressInfo) => void; + +// @public +export interface ProgressInfo { + loaded?: number; + message?: string; + phase: LoadingPhase; + total?: number; +} + +``` diff --git a/examples/next-app-router/app/layout.tsx b/examples/next-app-router/app/layout.tsx new file mode 100644 index 0000000..f221ae2 --- /dev/null +++ b/examples/next-app-router/app/layout.tsx @@ -0,0 +1,17 @@ +import type { Metadata } from 'next'; +import type { ReactNode } from 'react'; +import '@ioai/lerobot-studio/style.css'; +import './styles.css'; + +// Next.js requires route metadata to be exported from the layout module. +export const metadata: Metadata = { + title: 'LeRobot Studio Next.js consumer', +}; + +export default function RootLayout({ children }: Readonly<{ children: ReactNode }>) { + return ( + + {children} + + ); +} diff --git a/examples/next-app-router/app/page.tsx b/examples/next-app-router/app/page.tsx new file mode 100644 index 0000000..a8f34bd --- /dev/null +++ b/examples/next-app-router/app/page.tsx @@ -0,0 +1,5 @@ +import { ViewerClient } from '../components/ViewerClient'; + +export default function Home() { + return ; +} diff --git a/examples/next-app-router/app/styles.css b/examples/next-app-router/app/styles.css new file mode 100644 index 0000000..11e5d2d --- /dev/null +++ b/examples/next-app-router/app/styles.css @@ -0,0 +1,7 @@ +html, +body, +main { + width: 100%; + height: 100%; + margin: 0; +} diff --git a/examples/next-app-router/components/ViewerCanvas.tsx b/examples/next-app-router/components/ViewerCanvas.tsx new file mode 100644 index 0000000..d3d6217 --- /dev/null +++ b/examples/next-app-router/components/ViewerCanvas.tsx @@ -0,0 +1,55 @@ +'use client'; + +import { LeRobotViewer, type DataSource } from '@ioai/lerobot-studio'; + +const files: Record = { + 'meta/info.json': JSON.stringify({ + codebase_version: 'v2.1', + robot_type: 'next-consumer-fixture', + total_episodes: 1, + total_frames: 1, + total_tasks: 1, + total_videos: 0, + chunks_size: 1000, + fps: 30, + features: { + timestamp: { dtype: 'float32', shape: [1], names: null }, + }, + }), + 'meta/episodes.jsonl': '{"episode_index":0,"length":1,"tasks":["smoke test"]}\n', + 'meta/tasks.jsonl': '{"task_index":0,"task":"smoke test"}\n', +}; + +const dataSource: DataSource = { + async exists(filePath) { + return filePath in files; + }, + async readText(filePath) { + const value = files[filePath]; + if (value === undefined) throw new Error(`Fixture path not found: ${filePath}`); + return value; + }, + async readBytes(filePath) { + return new TextEncoder().encode(await this.readText(filePath)); + }, + async getObjectUrl(filePath, mimeType = 'application/octet-stream') { + return URL.createObjectURL(new Blob([await this.readText(filePath)], { type: mimeType })); + }, + clear() {}, + async listPaths() { + return Object.keys(files); + }, +}; + +export default function ViewerCanvas() { + return ( +
+ +
+ ); +} diff --git a/examples/next-app-router/components/ViewerClient.tsx b/examples/next-app-router/components/ViewerClient.tsx new file mode 100644 index 0000000..2f017ba --- /dev/null +++ b/examples/next-app-router/components/ViewerClient.tsx @@ -0,0 +1,12 @@ +'use client'; + +import dynamic from 'next/dynamic'; + +const ViewerCanvas = dynamic(() => import('./ViewerCanvas'), { + ssr: false, + loading: () =>

Loading LeRobot Studio…

, +}); + +export function ViewerClient() { + return ; +} diff --git a/examples/next-app-router/next-env.d.ts b/examples/next-app-router/next-env.d.ts new file mode 100644 index 0000000..b45390a --- /dev/null +++ b/examples/next-app-router/next-env.d.ts @@ -0,0 +1,4 @@ +/// +/// + +// This file is generated by Next.js and is checked in so the example typechecks after install. diff --git a/examples/next-app-router/package.json b/examples/next-app-router/package.json new file mode 100644 index 0000000..43ba27b --- /dev/null +++ b/examples/next-app-router/package.json @@ -0,0 +1,23 @@ +{ + "name": "lerobot-studio-next-app-router-example", + "private": true, + "version": "0.0.0", + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@ioai/lerobot-studio": "file:../..", + "next": "latest", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/node": "latest", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "typescript": "latest" + } +} diff --git a/examples/next-app-router/tsconfig.json b/examples/next-app-router/tsconfig.json new file mode 100644 index 0000000..f9d454b --- /dev/null +++ b/examples/next-app-router/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": false, + "skipLibCheck": false, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [{ "name": "next" }] + }, + "include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/examples/vite-react/index.html b/examples/vite-react/index.html new file mode 100644 index 0000000..010197a --- /dev/null +++ b/examples/vite-react/index.html @@ -0,0 +1,12 @@ + + + + + + LeRobot Studio Vite consumer + + +
+ + + diff --git a/examples/vite-react/package.json b/examples/vite-react/package.json new file mode 100644 index 0000000..2863c7d --- /dev/null +++ b/examples/vite-react/package.json @@ -0,0 +1,24 @@ +{ + "name": "lerobot-studio-vite-react-example", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "typecheck": "tsc --noEmit", + "build": "npm run typecheck && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@ioai/lerobot-studio": "file:../..", + "@vitejs/plugin-react": "latest", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "vite": "latest" + }, + "devDependencies": { + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "typescript": "latest" + } +} diff --git a/examples/vite-react/src/App.tsx b/examples/vite-react/src/App.tsx new file mode 100644 index 0000000..8f34269 --- /dev/null +++ b/examples/vite-react/src/App.tsx @@ -0,0 +1,57 @@ +import { LeRobotViewer, type DataSource } from '@ioai/lerobot-studio'; + +const files = new Map([ + [ + 'meta/info.json', + JSON.stringify({ + codebase_version: 'v2.1', + robot_type: 'npm-consumer-fixture', + total_episodes: 1, + total_frames: 1, + total_tasks: 1, + total_videos: 0, + chunks_size: 1000, + fps: 30, + features: { + timestamp: { dtype: 'float32', shape: [1], names: null }, + }, + }), + ], + ['meta/episodes.jsonl', '{"episode_index":0,"length":1,"tasks":["smoke test"]}\n'], + ['meta/tasks.jsonl', '{"task_index":0,"task":"smoke test"}\n'], +]); + +const dataSource: DataSource = { + async exists(filePath) { + return files.has(filePath); + }, + async readText(filePath) { + if (filePath === 'meta/info.json') window.__LEROBOT_CONSUMER__.infoRead = true; + const value = files.get(filePath); + if (value === undefined) throw new Error(`Fixture path not found: ${filePath}`); + return value; + }, + async readBytes(filePath) { + return new TextEncoder().encode(await this.readText(filePath)); + }, + async getObjectUrl(filePath, mimeType = 'application/octet-stream') { + return URL.createObjectURL(new Blob([await this.readText(filePath)], { type: mimeType })); + }, + clear() {}, + async listPaths() { + return [...files.keys()]; + }, +}; + +export function App() { + return ( +
+ +
+ ); +} diff --git a/examples/vite-react/src/main.tsx b/examples/vite-react/src/main.tsx new file mode 100644 index 0000000..d38cbd8 --- /dev/null +++ b/examples/vite-react/src/main.tsx @@ -0,0 +1,44 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import '@ioai/lerobot-studio/style.css'; +import { App } from './App'; +import './styles.css'; + +declare global { + interface Window { + __LEROBOT_CONSUMER__: { + infoRead: boolean; + wasmFetched: boolean; + workerConstructed: boolean; + }; + } +} + +window.__LEROBOT_CONSUMER__ = { + infoRead: false, + wasmFetched: false, + workerConstructed: false, +}; +const nativeFetch = window.fetch.bind(window); +window.fetch = async (...args) => { + const input = args[0]; + const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url; + const isWasm = url.startsWith('data:') + ? url.includes('wasm') + : new URL(url).pathname.endsWith('.wasm'); + const response = await nativeFetch(...args); + if (isWasm && response.ok) window.__LEROBOT_CONSUMER__.wasmFetched = true; + return response; +}; +window.Worker = new Proxy(window.Worker, { + construct(Target, args) { + window.__LEROBOT_CONSUMER__.workerConstructed = true; + return Reflect.construct(Target, args); + }, +}); + +createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/examples/vite-react/src/styles.css b/examples/vite-react/src/styles.css new file mode 100644 index 0000000..6410ea7 --- /dev/null +++ b/examples/vite-react/src/styles.css @@ -0,0 +1,8 @@ +html, +body, +#root, +main { + width: 100%; + height: 100%; + margin: 0; +} diff --git a/examples/vite-react/tsconfig.json b/examples/vite-react/tsconfig.json new file mode 100644 index 0000000..07a1611 --- /dev/null +++ b/examples/vite-react/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "skipLibCheck": false, + "types": ["vite/client"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/vite-react/vite.config.ts b/examples/vite-react/vite.config.ts new file mode 100644 index 0000000..ea3bc2e --- /dev/null +++ b/examples/vite-react/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + base: './', + plugins: [react()], +}); diff --git a/package-lock.json b/package-lock.json index 6eccd2e..51c0304 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@base-ui/react": "^1.6.0", + "@base-ui/react": "^1.7.0", "@fontsource-variable/geist": "^5.3.0", "@zip.js/zip.js": "^2.8.34", "apache-arrow": "^21.2.0", @@ -20,11 +20,11 @@ "fflate": "^0.8.3", "framer-motion": "^12.43.0", "lucide-react": "^1.28.0", - "mediabunny": "^1.51.0", + "mediabunny": "^1.52.3", "next-themes": "^0.4.6", "parquet-wasm": "^0.7.2", "radix-ui": "^1.6.7", - "react-intl": "^10.1.18", + "react-intl": "^10.1.20", "react-window": "^2.3.0", "sonner": "^2.0.7", "tailwind-merge": "^3.6.0", @@ -33,69 +33,160 @@ "use-sync-external-store": "^1.6.0" }, "devDependencies": { + "@arethetypeswrong/cli": "^0.18.5", + "@changesets/cli": "^2.31.1", "@eslint/js": "^10.0.1", - "@playwright/test": "^1.62.0", + "@microsoft/api-extractor": "^7.58.12", + "@playwright/test": "^1.62.1", "@tailwindcss/vite": "^4.3.3", "@types/node": "^26.1.2", - "@types/react": "^19.2.17", - "@types/react-dom": "^19.2.3", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", "@vitejs/plugin-react": "^6.0.4", "@vitest/browser": "^4.1.10", "@vitest/browser-playwright": "^4.1.10", "@vitest/coverage-v8": "^4.1.10", + "axe-core": "^4.12.1", "eslint": "^10.8.0", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.3", - "globals": "^17.8.0", + "globals": "^17.9.0", "lightningcss": "^1.33.0", - "playwright": "^1.62.0", + "playwright": "^1.62.1", "prettier": "^3.6.2", + "publint": "^0.3.23", "react": "^19.2.8", "react-dom": "^19.2.8", "shadcn": "^4.16.0", "tailwindcss": "^4.3.3", "typescript": "6.0.3", - "typescript-eslint": "^8.65.0", + "typescript-eslint": "^8.66.0", "vite": "^8.1.5", - "vite-plugin-dts": "^5.0.3", + "vitepress": "2.0.0-alpha.19", "vitest": "^4.1.10" }, + "peerDependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, + "node_modules/@andrewbranch/untar.js": { + "version": "1.0.3", + "dev": true + }, + "node_modules/@arethetypeswrong/cli": { + "version": "0.18.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@arethetypeswrong/core": "0.18.5", + "chalk": "^4.1.2", + "cli-table3": "^0.6.3", + "commander": "^10.0.1", + "marked": "^9.1.2", + "marked-terminal": "^7.1.0", + "semver": "^7.5.4" + }, + "bin": { + "attw": "dist/index.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@arethetypeswrong/cli/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, "engines": { - "node": ">=24.0.0 <25" + "node": ">=10" }, - "peerDependencies": { - "react": "^19.2.8", - "react-dom": "^19.2.8" + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "apps/web": { - "name": "@-web", - "version": "1.0.0", - "extraneous": true, + "node_modules/@arethetypeswrong/cli/node_modules/commander": { + "version": "10.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@arethetypeswrong/cli/node_modules/semver": { + "version": "7.8.5", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@arethetypeswrong/core": { + "version": "0.18.5", + "dev": true, + "license": "MIT", "dependencies": { - "@": "*", - "@/core": "*", - "@/platform": "*", - "@/ui": "*", - "lucide-react": "^1.28.0", - "react": "^19.2.8", - "react-dom": "^19.2.8", - "react-intl": "^10.1.18" + "@andrewbranch/untar.js": "^1.0.3", + "@loaderkit/resolve": "^1.0.2", + "cjs-module-lexer": "^1.2.3", + "fflate": "^0.8.3", + "lru-cache": "^11.0.1", + "semver": "^7.5.4", + "typescript": "5.6.1-rc", + "validate-npm-package-name": "^5.0.0" }, - "devDependencies": { - "@tailwindcss/vite": "^4.3.3", - "@types/react": "^19.2.17", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^6.0.4", - "tailwindcss": "^4.3.3", - "typescript": "6.0.3", - "vite": "^8.1.5" + "engines": { + "node": ">=20" + } + }, + "node_modules/@arethetypeswrong/core/node_modules/lru-cache": { + "version": "11.5.2", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@arethetypeswrong/core/node_modules/semver": { + "version": "7.8.5", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@arethetypeswrong/core/node_modules/typescript": { + "version": "5.6.1-rc", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@arethetypeswrong/core/node_modules/validate-npm-package-name": { + "version": "5.0.1", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/@babel/code-frame": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", - "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { @@ -109,8 +200,6 @@ }, "node_modules/@babel/compat-data": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", - "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { @@ -119,8 +208,6 @@ }, "node_modules/@babel/core": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", - "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", "dependencies": { @@ -150,8 +237,6 @@ }, "node_modules/@babel/generator": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", - "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "dev": true, "license": "MIT", "dependencies": { @@ -167,8 +252,6 @@ }, "node_modules/@babel/helper-annotate-as-pure": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", - "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", "dev": true, "license": "MIT", "dependencies": { @@ -180,8 +263,6 @@ }, "node_modules/@babel/helper-compilation-targets": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", - "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", "dependencies": { @@ -197,8 +278,6 @@ }, "node_modules/@babel/helper-create-class-features-plugin": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", - "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", "dev": true, "license": "MIT", "dependencies": { @@ -219,8 +298,6 @@ }, "node_modules/@babel/helper-globals": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", - "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", "engines": { @@ -229,8 +306,6 @@ }, "node_modules/@babel/helper-member-expression-to-functions": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", - "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", "dev": true, "license": "MIT", "dependencies": { @@ -243,8 +318,6 @@ }, "node_modules/@babel/helper-module-imports": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", - "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { @@ -257,8 +330,6 @@ }, "node_modules/@babel/helper-module-transforms": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", - "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { @@ -275,8 +346,6 @@ }, "node_modules/@babel/helper-optimise-call-expression": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", - "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", "dev": true, "license": "MIT", "dependencies": { @@ -288,8 +357,6 @@ }, "node_modules/@babel/helper-plugin-utils": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", - "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", "dev": true, "license": "MIT", "engines": { @@ -298,8 +365,6 @@ }, "node_modules/@babel/helper-replace-supers": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", - "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", "dev": true, "license": "MIT", "dependencies": { @@ -316,8 +381,6 @@ }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", - "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", "dev": true, "license": "MIT", "dependencies": { @@ -330,8 +393,6 @@ }, "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==", "dev": true, "license": "MIT", "engines": { @@ -340,8 +401,6 @@ }, "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==", "dev": true, "license": "MIT", "engines": { @@ -350,8 +409,6 @@ }, "node_modules/@babel/helper-validator-option": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", - "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", "engines": { @@ -360,8 +417,6 @@ }, "node_modules/@babel/helpers": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", - "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", "dependencies": { @@ -374,8 +429,6 @@ }, "node_modules/@babel/parser": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { @@ -390,8 +443,6 @@ }, "node_modules/@babel/plugin-syntax-jsx": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", - "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", "dev": true, "license": "MIT", "dependencies": { @@ -406,8 +457,6 @@ }, "node_modules/@babel/plugin-syntax-typescript": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", - "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", "dev": true, "license": "MIT", "dependencies": { @@ -422,8 +471,6 @@ }, "node_modules/@babel/plugin-transform-modules-commonjs": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", - "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", "dev": true, "license": "MIT", "dependencies": { @@ -439,8 +486,6 @@ }, "node_modules/@babel/plugin-transform-typescript": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", - "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==", "dev": true, "license": "MIT", "dependencies": { @@ -459,8 +504,6 @@ }, "node_modules/@babel/preset-typescript": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.29.7.tgz", - "integrity": "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==", "dev": true, "license": "MIT", "dependencies": { @@ -479,8 +522,6 @@ }, "node_modules/@babel/runtime": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", - "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -488,8 +529,6 @@ }, "node_modules/@babel/template": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", - "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { @@ -503,8 +542,6 @@ }, "node_modules/@babel/traverse": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", - "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dev": true, "license": "MIT", "dependencies": { @@ -522,8 +559,6 @@ }, "node_modules/@babel/types": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { @@ -535,15 +570,13 @@ } }, "node_modules/@base-ui/react": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@base-ui/react/-/react-1.6.0.tgz", - "integrity": "sha512-/jzjTWJYXhRFO45Bev9lc3cHbmjzCMpUqbMZ2AgKy/z25mY9B6shGSNcXcjQar9n5doM0KYW1W8fcFv2jZBuMw==", + "version": "1.7.0", "license": "MIT", "dependencies": { "@babel/runtime": "^7.29.2", - "@base-ui/utils": "0.3.1", - "@floating-ui/react-dom": "^2.1.8", - "@floating-ui/utils": "^0.2.11", + "@base-ui/utils": "0.3.2", + "@floating-ui/react-dom": "^2.1.9", + "@floating-ui/utils": "^0.2.12", "use-sync-external-store": "^1.6.0" }, "engines": { @@ -573,13 +606,11 @@ } }, "node_modules/@base-ui/utils": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@base-ui/utils/-/utils-0.3.1.tgz", - "integrity": "sha512-gFFiltORVmW/N6IILTGxizP3PBpVpysqML1ALY5Vk0mH+7faVkCknOU31goYHN5Aoek2dkjxva1XOD2Ce9WuIg==", + "version": "0.3.2", "license": "MIT", "dependencies": { "@babel/runtime": "^7.29.2", - "@floating-ui/utils": "^0.2.11", + "@floating-ui/utils": "^0.2.12", "reselect": "^5.2.0", "use-sync-external-store": "^1.6.0" }, @@ -596,8 +627,6 @@ }, "node_modules/@bcoe/v8-coverage": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", - "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", "dev": true, "license": "MIT", "engines": { @@ -606,270 +635,509 @@ }, "node_modules/@blazediff/core": { "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@blazediff/core/-/core-1.9.1.tgz", - "integrity": "sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==", "dev": true, "license": "MIT" }, - "node_modules/@dotenvx/dotenvx": { - "version": "1.75.1", - "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.75.1.tgz", - "integrity": "sha512-/BITOC9dmS/edY2zQwZNicQ059O6RKabtQfyEafV0nGtfYRNHYy1DIPiYVcov40+tob9hfmBnbR963dS+EQ1DQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@dotenvx/primitives": "^0.8.0", - "commander": "^11.1.0", - "conf": "^10.2.0", - "dotenv": "^17.2.1", - "enquirer": "^2.4.1", - "env-paths": "^2.2.1", - "execa": "^5.1.1", - "fdir": "^6.2.0", - "ignore": "^5.3.0", - "object-treeify": "1.1.33", - "open": "^8.4.2", - "picomatch": "^4.0.4", - "systeminformation": "^5.22.11", - "undici": "^7.11.0", - "which": "^4.0.0", - "yocto-spinner": "^1.1.0" - }, - "bin": { - "dotenvx": "src/cli/dotenvx.js" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/@dotenvx/dotenvx/node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "node_modules/@braidai/lang": { + "version": "1.1.2", "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - } + "license": "ISC" }, - "node_modules/@dotenvx/dotenvx/node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "node_modules/@changesets/apply-release-plan": { + "version": "7.1.1", "dev": true, "license": "MIT", - "engines": { - "node": ">=8" + "dependencies": { + "@changesets/config": "^3.1.4", + "@changesets/get-version-range-type": "^0.4.0", + "@changesets/git": "^3.0.4", + "@changesets/should-skip-package": "^0.1.2", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "detect-indent": "^6.0.0", + "fs-extra": "^7.0.1", + "lodash.startcase": "^4.4.0", + "outdent": "^0.5.0", + "prettier": "^2.7.1", + "resolve-from": "^5.0.0", + "semver": "^7.5.3" } }, - "node_modules/@dotenvx/dotenvx/node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "node_modules/@changesets/apply-release-plan/node_modules/prettier": { + "version": "2.8.8", "dev": true, "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" + "bin": { + "prettier": "bin-prettier.js" }, "engines": { - "node": ">=10" + "node": ">=10.13.0" }, "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" + "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/@dotenvx/dotenvx/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "node_modules/@changesets/apply-release-plan/node_modules/semver": { + "version": "7.8.5", "dev": true, - "license": "MIT", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, "engines": { "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@dotenvx/dotenvx/node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "node_modules/@changesets/assemble-release-plan": { + "version": "6.0.10", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" + "license": "MIT", + "dependencies": { + "@changesets/errors": "^0.2.0", + "@changesets/get-dependents-graph": "^2.1.4", + "@changesets/should-skip-package": "^0.1.2", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "semver": "^7.5.3" } }, - "node_modules/@dotenvx/dotenvx/node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "node_modules/@changesets/assemble-release-plan/node_modules/semver": { + "version": "7.8.5", "dev": true, - "license": "MIT", + "license": "ISC", "bin": { - "is-docker": "cli.js" + "semver": "bin/semver.js" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=10" } }, - "node_modules/@dotenvx/dotenvx/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "node_modules/@changesets/changelog-git": { + "version": "0.2.1", "dev": true, "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "@changesets/types": "^6.1.0" } }, - "node_modules/@dotenvx/dotenvx/node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "node_modules/@changesets/cli": { + "version": "2.31.1", "dev": true, "license": "MIT", "dependencies": { - "is-docker": "^2.0.0" + "@changesets/apply-release-plan": "^7.1.1", + "@changesets/assemble-release-plan": "^6.0.10", + "@changesets/changelog-git": "^0.2.1", + "@changesets/config": "^3.1.4", + "@changesets/errors": "^0.2.0", + "@changesets/get-dependents-graph": "^2.1.4", + "@changesets/get-release-plan": "^4.0.16", + "@changesets/git": "^3.0.4", + "@changesets/logger": "^0.1.1", + "@changesets/pre": "^2.0.2", + "@changesets/read": "^0.6.7", + "@changesets/should-skip-package": "^0.1.2", + "@changesets/types": "^6.1.0", + "@changesets/write": "^0.4.0", + "@inquirer/external-editor": "^1.0.2", + "@manypkg/get-packages": "^1.1.3", + "ansi-colors": "^4.1.3", + "enquirer": "^2.4.1", + "fs-extra": "^7.0.1", + "mri": "^1.2.0", + "package-manager-detector": "^0.2.0", + "picocolors": "^1.1.0", + "resolve-from": "^5.0.0", + "semver": "^7.5.3", + "spawndamnit": "^3.0.1", + "term-size": "^2.1.0" }, - "engines": { - "node": ">=8" + "bin": { + "changeset": "bin.js" } }, - "node_modules/@dotenvx/dotenvx/node_modules/isexe": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", - "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "node_modules/@changesets/cli/node_modules/semver": { + "version": "7.8.5", "dev": true, - "license": "BlueOak-1.0.0", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, "engines": { - "node": ">=18" + "node": ">=10" } }, - "node_modules/@dotenvx/dotenvx/node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "node_modules/@changesets/config": { + "version": "3.1.4", "dev": true, "license": "MIT", "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" + "@changesets/errors": "^0.2.0", + "@changesets/get-dependents-graph": "^2.1.4", + "@changesets/logger": "^0.1.1", + "@changesets/should-skip-package": "^0.1.2", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "fs-extra": "^7.0.1", + "micromatch": "^4.0.8" } }, - "node_modules/@dotenvx/dotenvx/node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "node_modules/@changesets/errors": { + "version": "0.2.0", "dev": true, "license": "MIT", "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "extendable-error": "^0.1.5" } }, - "node_modules/@dotenvx/dotenvx/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/@dotenvx/dotenvx/node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "node_modules/@changesets/get-dependents-graph": { + "version": "2.1.4", "dev": true, "license": "MIT", - "engines": { - "node": ">=6" + "dependencies": { + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "picocolors": "^1.1.0", + "semver": "^7.5.3" } }, - "node_modules/@dotenvx/dotenvx/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "node_modules/@changesets/get-dependents-graph/node_modules/semver": { + "version": "7.8.5", "dev": true, "license": "ISC", - "dependencies": { - "isexe": "^3.1.1" - }, "bin": { - "node-which": "bin/which.js" + "semver": "bin/semver.js" }, "engines": { - "node": "^16.13.0 || >=18.0.0" + "node": ">=10" } }, - "node_modules/@dotenvx/primitives": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@dotenvx/primitives/-/primitives-0.8.0.tgz", - "integrity": "sha512-VYJy0uhFm9zTJ1TxBaW/pA8bjbOM/OttaNMwZ1RHG4JKyRG7DhSdiqD1ipQoAyoD22olUtxbP78W9xY3Wd11bg==", - "dev": true, - "license": "BSD-3-Clause" - }, - "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==", + "node_modules/@changesets/get-release-plan": { + "version": "4.0.16", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" + "@changesets/assemble-release-plan": "^6.0.10", + "@changesets/config": "^3.1.4", + "@changesets/pre": "^2.0.2", + "@changesets/read": "^0.6.7", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3" } }, - "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==", + "node_modules/@changesets/get-version-range-type": { + "version": "0.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@changesets/git": { + "version": "3.0.4", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "tslib": "^2.4.0" + "@changesets/errors": "^0.2.0", + "@manypkg/get-packages": "^1.1.3", + "is-subdir": "^1.1.1", + "micromatch": "^4.0.8", + "spawndamnit": "^3.0.1" } }, - "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==", + "node_modules/@changesets/logger": { + "version": "0.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.0" + } + }, + "node_modules/@changesets/parse": { + "version": "0.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^6.1.0", + "js-yaml": "^4.1.1" + } + }, + "node_modules/@changesets/pre": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/errors": "^0.2.0", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "fs-extra": "^7.0.1" + } + }, + "node_modules/@changesets/read": { + "version": "0.6.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/git": "^3.0.4", + "@changesets/logger": "^0.1.1", + "@changesets/parse": "^0.4.3", + "@changesets/types": "^6.1.0", + "fs-extra": "^7.0.1", + "p-filter": "^2.1.0", + "picocolors": "^1.1.0" + } + }, + "node_modules/@changesets/should-skip-package": { + "version": "0.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3" + } + }, + "node_modules/@changesets/types": { + "version": "6.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@changesets/write": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^6.1.0", + "fs-extra": "^7.0.1", + "human-id": "^4.1.1", + "prettier": "^2.7.1" + } + }, + "node_modules/@changesets/write/node_modules/prettier": { + "version": "2.8.8", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", "dev": true, "license": "MIT", "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@docsearch/css": { + "version": "4.7.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@docsearch/js": { + "version": "4.7.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@docsearch/sidepanel-js": { + "version": "4.7.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@dotenvx/dotenvx": { + "version": "1.75.1", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "tslib": "^2.4.0" + "@dotenvx/primitives": "^0.8.0", + "commander": "^11.1.0", + "conf": "^10.2.0", + "dotenv": "^17.2.1", + "enquirer": "^2.4.1", + "env-paths": "^2.2.1", + "execa": "^5.1.1", + "fdir": "^6.2.0", + "ignore": "^5.3.0", + "object-treeify": "1.1.33", + "open": "^8.4.2", + "picomatch": "^4.0.4", + "systeminformation": "^5.22.11", + "undici": "^7.11.0", + "which": "^4.0.0", + "yocto-spinner": "^1.1.0" + }, + "bin": { + "dotenvx": "src/cli/dotenvx.js" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/commander": { + "version": "11.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/define-lazy-prop": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/execa": { + "version": "5.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/get-stream": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/human-signals": { + "version": "2.1.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/is-docker": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/is-stream": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/is-wsl": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/isexe": { + "version": "3.1.5", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/npm-run-path": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/open": { + "version": "8.4.2", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/signal-exit": { + "version": "3.0.7", + "dev": true, + "license": "ISC" + }, + "node_modules/@dotenvx/dotenvx/node_modules/strip-final-newline": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" } }, + "node_modules/@dotenvx/dotenvx/node_modules/which": { + "version": "4.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@dotenvx/primitives": { + "version": "0.8.0", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -887,8 +1155,6 @@ }, "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "license": "Apache-2.0", "engines": { @@ -900,8 +1166,6 @@ }, "node_modules/@eslint-community/regexpp": { "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -910,8 +1174,6 @@ }, "node_modules/@eslint/config-array": { "version": "0.23.5", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", - "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -925,8 +1187,6 @@ }, "node_modules/@eslint/config-helpers": { "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", - "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -938,8 +1198,6 @@ }, "node_modules/@eslint/core": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", - "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -951,8 +1209,6 @@ }, "node_modules/@eslint/js": { "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", - "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", "dev": true, "license": "MIT", "engines": { @@ -972,8 +1228,6 @@ }, "node_modules/@eslint/object-schema": { "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", - "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -982,8 +1236,6 @@ }, "node_modules/@eslint/plugin-kit": { "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", - "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -996,8 +1248,6 @@ }, "node_modules/@floating-ui/core": { "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", - "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", "license": "MIT", "dependencies": { "@floating-ui/utils": "^0.2.12" @@ -1005,8 +1255,6 @@ }, "node_modules/@floating-ui/dom": { "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", - "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", "license": "MIT", "dependencies": { "@floating-ui/core": "^1.8.0", @@ -1015,8 +1263,6 @@ }, "node_modules/@floating-ui/react-dom": { "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", - "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", "license": "MIT", "dependencies": { "@floating-ui/dom": "^1.8.0" @@ -1028,14 +1274,10 @@ }, "node_modules/@floating-ui/utils": { "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", - "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", "license": "MIT" }, "node_modules/@fontsource-variable/geist": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@fontsource-variable/geist/-/geist-5.3.0.tgz", - "integrity": "sha512-j0m+vLQuG5XAYoHtGCVu0spvlGreR3EzpECUVzkFmI1mTVnAO38l/NEPDCFgZ177JxzYJCLSmTQibIiYPilGrA==", "license": "OFL-1.1", "funding": { "url": "https://github.com/sponsors/ayuhito" @@ -1043,14 +1285,10 @@ }, "node_modules/@formatjs/fast-memoize": { "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.7.tgz", - "integrity": "sha512-zXfhLpvA6T7+efdt9JLbBwZ00tT7NsBMDVnDu8rpHeNNv8KfRZAMo2gkG0k9lK/Nzc//3kJ9pImsfuJxk3KhUA==", "license": "MIT" }, "node_modules/@formatjs/icu-messageformat-parser": { - "version": "3.5.15", - "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.15.tgz", - "integrity": "sha512-5o4grXKotAB3JqQuisLApHG43g17N+paoRTa92Jiz35Zvfemq0cVf4EDvuxyHAzmsJji7igaEowicLO/VmfJ8Q==", + "version": "3.5.16", "license": "MIT", "dependencies": { "@formatjs/icu-skeleton-parser": "2.1.11" @@ -1058,25 +1296,19 @@ }, "node_modules/@formatjs/icu-skeleton-parser": { "version": "2.1.11", - "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.11.tgz", - "integrity": "sha512-j8cUmOJzVgkHuS0QiQ6ga76UIoLOFSAMWhs7aZJztH3aAdCOAE6vpC8KVvFB4cU10ON0y2/5oOVmPJ43s2lTwA==", "license": "MIT" }, "node_modules/@formatjs/intl": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-4.1.17.tgz", - "integrity": "sha512-IxluRkzOZH8ahaDip5g8YH9kJi1munUZi9i1BJs1wVFQIY2pROoCSiOlybZegyiPsZlMaYfADkD99zP2xmBfjQ==", + "version": "4.1.18", "license": "MIT", "dependencies": { "@formatjs/fast-memoize": "3.1.7", - "@formatjs/icu-messageformat-parser": "3.5.15", - "intl-messageformat": "11.2.12" + "@formatjs/icu-messageformat-parser": "3.5.16", + "intl-messageformat": "11.2.13" } }, "node_modules/@hono/node-server": { "version": "2.0.12", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.0.12.tgz", - "integrity": "sha512-eWpQYr67tqJLeaSUl0Q+TquuYfUdTibpOJlUMV2FfUP7+KqCC5TufnwnlXL6mobZBJbGAYRd7ZvEBDCbLInjhg==", "dev": true, "license": "MIT", "engines": { @@ -1088,8 +1320,6 @@ }, "node_modules/@humanfs/core": { "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", - "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1101,8 +1331,6 @@ }, "node_modules/@humanfs/node": { "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", - "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1116,8 +1344,6 @@ }, "node_modules/@humanfs/types": { "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", - "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1126,8 +1352,6 @@ }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1140,8 +1364,6 @@ }, "node_modules/@humanwhocodes/retry": { "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1152,10 +1374,41 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@iconify-json/simple-icons": { + "version": "1.2.93", + "dev": true, + "license": "CC0-1.0", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", "dependencies": { @@ -1165,8 +1418,6 @@ }, "node_modules/@jridgewell/remapping": { "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1176,36 +1427,250 @@ }, "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.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@loaderkit/resolve": { + "version": "1.0.6", + "dev": true, + "license": "ISC", + "dependencies": { + "@braidai/lang": "^1.0.0" + } + }, + "node_modules/@manypkg/find-root": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.5.5", + "@types/node": "^12.7.1", + "find-up": "^4.1.0", + "fs-extra": "^8.1.0" + } + }, + "node_modules/@manypkg/find-root/node_modules/@types/node": { + "version": "12.20.55", + "dev": true, + "license": "MIT" + }, + "node_modules/@manypkg/find-root/node_modules/find-up": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@manypkg/find-root/node_modules/fs-extra": { + "version": "8.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@manypkg/find-root/node_modules/locate-path": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@manypkg/find-root/node_modules/p-limit": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@manypkg/find-root/node_modules/p-locate": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@manypkg/get-packages": { + "version": "1.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.5.5", + "@changesets/types": "^4.0.1", + "@manypkg/find-root": "^1.1.0", + "fs-extra": "^8.1.0", + "globby": "^11.0.0", + "read-yaml-file": "^1.1.0" + } + }, + "node_modules/@manypkg/get-packages/node_modules/@changesets/types": { + "version": "4.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@manypkg/get-packages/node_modules/fs-extra": { + "version": "8.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@microsoft/api-extractor": { + "version": "7.58.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/api-extractor-model": "7.33.10", + "@microsoft/tsdoc": "~0.16.0", + "@microsoft/tsdoc-config": "~0.18.1", + "@rushstack/node-core-library": "5.23.3", + "@rushstack/rig-package": "0.7.3", + "@rushstack/terminal": "0.24.2", + "@rushstack/ts-command-line": "5.3.12", + "diff": "~8.0.2", + "minimatch": "10.2.3", + "resolve": "~1.22.1", + "semver": "~7.7.4", + "source-map": "~0.6.1", + "typescript": "5.9.3" + }, + "bin": { + "api-extractor": "bin/api-extractor" + } + }, + "node_modules/@microsoft/api-extractor-model": { + "version": "7.33.10", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "~0.16.0", + "@microsoft/tsdoc-config": "~0.18.1", + "@rushstack/node-core-library": "5.23.3" + } + }, + "node_modules/@microsoft/api-extractor/node_modules/minimatch": { + "version": "10.2.3", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@microsoft/api-extractor/node_modules/semver": { + "version": "7.7.4", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@microsoft/api-extractor/node_modules/typescript": { + "version": "5.9.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@microsoft/tsdoc": { + "version": "0.16.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@microsoft/tsdoc-config": { + "version": "0.18.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.16.0", + "ajv": "~8.18.0", + "jju": "~1.4.0", + "resolve": "~1.22.2" + } + }, + "node_modules/@microsoft/tsdoc-config/node_modules/ajv": { + "version": "8.18.0", + "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/@microsoft/tsdoc-config/node_modules/json-schema-traverse": { + "version": "1.0.0", "dev": true, "license": "MIT" }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.30.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", - "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", "dev": true, "license": "MIT", "dependencies": { @@ -1245,8 +1710,6 @@ }, "node_modules/@modelcontextprotocol/sdk/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": { @@ -1262,37 +1725,11 @@ }, "node_modules/@modelcontextprotocol/sdk/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/@napi-rs/wasm-runtime": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.1.tgz", - "integrity": "sha512-KjZdi8Q1wh89gsVmghvbrMgWl6ZWmRmHV6wjB7/g4Zf0dyO+hH3neZUtuDNPO00qq5YE5RITVWvrIZKRaAmzGQ==", - "dev": true, - "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.3", - "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, "license": "MIT", "dependencies": { @@ -1305,8 +1742,6 @@ }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, "license": "MIT", "engines": { @@ -1315,8 +1750,6 @@ }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "license": "MIT", "dependencies": { @@ -1328,9 +1761,7 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.139.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", - "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "version": "0.142.0", "dev": true, "license": "MIT", "funding": { @@ -1338,13 +1769,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.0.tgz", - "integrity": "sha512-9zOJ6ZQRAena31MpOH9VSzIz8Ou3YJ/wtY/eQm5T2uhfhG7/U3COrMS8xOtUrZrp9OgdmzEnIYODye3nY1VqzA==", + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz", + "integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.62.0" + "playwright": "1.62.1" }, "bin": { "playwright": "cli.js" @@ -1355,27 +1786,33 @@ }, "node_modules/@polka/url": { "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", "dev": true, "license": "MIT" }, + "node_modules/@publint/pack": { + "version": "0.1.6", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyexec": "^1.2.4" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://bjornlu.com/sponsor" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.3.tgz", - "integrity": "sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA==", "license": "MIT" }, "node_modules/@radix-ui/primitive": { "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.7.tgz", - "integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==", "license": "MIT" }, "node_modules/@radix-ui/react-accessible-icon": { "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.15.tgz", - "integrity": "sha512-WTQwcAvQf5sOcuUyi90lKPbhwcvQ+j55cjrSmeaN+L2vKU3DooOvlKw2MDeiJ5IkV5N905KW0/fGojKOBhD11A==", "license": "MIT", "dependencies": { "@radix-ui/react-visually-hidden": "1.2.11" @@ -1397,8 +1834,6 @@ }, "node_modules/@radix-ui/react-accordion": { "version": "1.2.20", - "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.20.tgz", - "integrity": "sha512-jDhG9FvAEnlhnjrsINbNXcUa4G+L1KqSkJSunkbKEzFRcAb52jvM0PjPxPRvhe1HNc5F5yc0yzzWeeqlH4yBIg==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.7", @@ -1428,8 +1863,6 @@ }, "node_modules/@radix-ui/react-alert-dialog": { "version": "1.1.23", - "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.23.tgz", - "integrity": "sha512-VAYOiQRqj3GPpYJE0I9J+X8Ip05cyVlNdKOFeiGS2Ou1HHGfpl0BxOyZm6nmVDyU+W+NF3/XLzmjHmVGydhwgA==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.7", @@ -1455,8 +1888,6 @@ }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.15.tgz", - "integrity": "sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA==", "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.1.10" @@ -1478,8 +1909,6 @@ }, "node_modules/@radix-ui/react-aspect-ratio": { "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.15.tgz", - "integrity": "sha512-fy+dyVR+90nelK8rqIznFlxzx7uPcGbhxH8Nfr2bHb4UfSe+e3hklOC0luK0hDwVwnRX7xTRySpsrQVeW+/oNQ==", "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.1.10" @@ -1501,8 +1930,6 @@ }, "node_modules/@radix-ui/react-avatar": { "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.2.6.tgz", - "integrity": "sha512-4ULOTJ/mqy2hT9GlWa/MFHxHSvH3nJzHnZM1waNsc5Bonv7i70aNenghXmD97S6OJ81ekXONGGt4nT1r0PfEdA==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.7", @@ -1529,8 +1956,6 @@ }, "node_modules/@radix-ui/react-checkbox": { "version": "1.3.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.11.tgz", - "integrity": "sha512-Gnptr9pDDQxD3hgq2dtPbtrp/c2qH1mBwIzw3X/ivrMb2e1t0jMTi606fVEqFPaQR1ggXIVQWKj3P2WW9v7zGQ==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.7", @@ -1558,8 +1983,6 @@ }, "node_modules/@radix-ui/react-collapsible": { "version": "1.1.20", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.20.tgz", - "integrity": "sha512-mcGesGplBnzN2sbvJETzpCNfSMyPnb29q1GRLU+Ib7bJrpIG2ywmRoh2V5VbA2uNvKikKUlVbAPks7JDjz4A8Q==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.7", @@ -1588,8 +2011,6 @@ }, "node_modules/@radix-ui/react-collection": { "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.15.tgz", - "integrity": "sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.5", @@ -1614,8 +2035,6 @@ }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz", - "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -1629,8 +2048,6 @@ }, "node_modules/@radix-ui/react-context": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.2.tgz", - "integrity": "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -1644,8 +2061,6 @@ }, "node_modules/@radix-ui/react-context-menu": { "version": "2.3.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.3.7.tgz", - "integrity": "sha512-CtXP35dxaB5T3zXSd+E3uHe/QpXcpYnZmxp6OaIbfthtfW4wyb77M23BG+bwIJDtsMwEP/YssdsmNyZu7jhWew==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.7", @@ -1671,8 +2086,6 @@ }, "node_modules/@radix-ui/react-dialog": { "version": "1.1.23", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.23.tgz", - "integrity": "sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.7", @@ -1708,8 +2121,6 @@ }, "node_modules/@radix-ui/react-direction": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.4.tgz", - "integrity": "sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -1723,8 +2134,6 @@ }, "node_modules/@radix-ui/react-dismissable-layer": { "version": "1.1.19", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.19.tgz", - "integrity": "sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.7", @@ -1750,8 +2159,6 @@ }, "node_modules/@radix-ui/react-dropdown-menu": { "version": "2.1.24", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.24.tgz", - "integrity": "sha512-geq8l2rJkxvkXsT9RMgtUE3P8pITFpTsvYpbySi1IH4fZEABD/Gp85myayFgxk0ktljGMJnCbeFkyTusvSvv7g==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.7", @@ -1779,8 +2186,6 @@ }, "node_modules/@radix-ui/react-focus-guards": { "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.6.tgz", - "integrity": "sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -1794,8 +2199,6 @@ }, "node_modules/@radix-ui/react-focus-scope": { "version": "1.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.16.tgz", - "integrity": "sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.5", @@ -1819,8 +2222,6 @@ }, "node_modules/@radix-ui/react-form": { "version": "0.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.16.tgz", - "integrity": "sha512-Q4TLEn2A7TAypxwmd6R9EwrlXDvkfYSDMrq9/887AXAGh+G1rH+kYJKSTv+Si9Y0JPKTwKYv6PviAJosysNimA==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.7", @@ -1847,8 +2248,6 @@ }, "node_modules/@radix-ui/react-hover-card": { "version": "1.1.23", - "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.23.tgz", - "integrity": "sha512-H8qONfZd3ltrU3+jHCIgITbWo6e1iTKvP9DHdrvYbX48ooRM5FjEDTn16AMwdfuOGkWdZEhpl3PLL/Wk/AnHDQ==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.7", @@ -1878,8 +2277,6 @@ }, "node_modules/@radix-ui/react-id": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.4.tgz", - "integrity": "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==", "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" @@ -1896,8 +2293,6 @@ }, "node_modules/@radix-ui/react-label": { "version": "2.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.15.tgz", - "integrity": "sha512-o/rdYEwZTTo5tjknnPeyQFU45kUC4i/XyeDPP+HGyi6XqpOP6Zf5Ya5vh/Yfe9Id5JiuWnnAx2XqIeD3UYZt0g==", "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.1.10" @@ -1919,8 +2314,6 @@ }, "node_modules/@radix-ui/react-menu": { "version": "2.1.24", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.24.tgz", - "integrity": "sha512-uW7RVuU6Lp/ZtfeY4b3kL32zccgEWvPv1+cf17ubYzHa9cL8AHokmk36cG/XEiH/smbQvumnieXX9j/e9RqJWA==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.7", @@ -1959,8 +2352,6 @@ }, "node_modules/@radix-ui/react-menubar": { "version": "1.1.24", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.24.tgz", - "integrity": "sha512-eeVs0vf7cuqXaM0qLQCPcufImiJNVBXdJDLu7ZGYl2732UH23Qat/foNGrr6vYV3/DdTsBqASoggUFgH14OcZA==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.7", @@ -1991,8 +2382,6 @@ }, "node_modules/@radix-ui/react-navigation-menu": { "version": "1.2.22", - "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.22.tgz", - "integrity": "sha512-ou7iLEJ+yrhQndkkA4U21XIdS/CS45F4iXIkTZcb6/Ne9EMsOuDudVmCwmDnfFZZ+y1FZqXRNSIgBy+YMvZVZg==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.7", @@ -2027,8 +2416,6 @@ }, "node_modules/@radix-ui/react-one-time-password-field": { "version": "0.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.16.tgz", - "integrity": "sha512-Tj9P6ntAJEw52oq/F0AGknXR4XncxEt7XU47O3xJQOiWfLzEy3d9gtgKfvjSzGxzHkfL+VzvxGu2KTFsloJqXw==", "license": "MIT", "dependencies": { "@radix-ui/number": "1.1.3", @@ -2061,8 +2448,6 @@ }, "node_modules/@radix-ui/react-password-toggle-field": { "version": "0.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.11.tgz", - "integrity": "sha512-4gvFnmDXu3dgj21CqsufzIameRvlRd4SBqaWhcrlrNhRo0Y5i/49AmRJYe1fdAM3G2VNBbmin4b0D6cdQocwgw==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.7", @@ -2091,8 +2476,6 @@ }, "node_modules/@radix-ui/react-popover": { "version": "1.1.23", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.23.tgz", - "integrity": "sha512-mw58MrBlyHWFisTOYignD0vf/3gdcgAR+9of1s9G/38CbFiUwH1nCDkc0AUM9IrXFgN5Ue8n45j9WCgyM1sbiQ==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.7", @@ -2128,8 +2511,6 @@ }, "node_modules/@radix-ui/react-popper": { "version": "1.3.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.7.tgz", - "integrity": "sha512-UsJrrd7w4wuKKTdvd/DNERVlwSlUcyXzjhyDwBk+3aPOsCjOY6ZSbxuw8E6lZTjjfP8Cpd0J8VVkrYUWyGYXyg==", "license": "MIT", "dependencies": { "@floating-ui/react-dom": "^2.0.0", @@ -2160,8 +2541,6 @@ }, "node_modules/@radix-ui/react-portal": { "version": "1.1.17", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.17.tgz", - "integrity": "sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==", "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.1.10", @@ -2184,8 +2563,6 @@ }, "node_modules/@radix-ui/react-presence": { "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.10.tgz", - "integrity": "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==", "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" @@ -2207,8 +2584,6 @@ }, "node_modules/@radix-ui/react-primitive": { "version": "2.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.10.tgz", - "integrity": "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==", "license": "MIT", "dependencies": { "@radix-ui/react-slot": "1.3.3" @@ -2230,8 +2605,6 @@ }, "node_modules/@radix-ui/react-progress": { "version": "1.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.16.tgz", - "integrity": "sha512-5XnomAsoZZCY+KNTxbIghpGqPruZvKFNlvcAljVAOdDRDsH4/OZQxhtwo5wdtoDM5R6MhJBb2sPnDuRFep3lzg==", "license": "MIT", "dependencies": { "@radix-ui/react-context": "1.2.2", @@ -2254,8 +2627,6 @@ }, "node_modules/@radix-ui/react-radio-group": { "version": "1.4.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.4.7.tgz", - "integrity": "sha512-cgYFEkntCxppHZgtSZ+7vh0wbZQ+IC7PPMw8DSnRG27B6kDd32/Zw0OJt7dGDigCoprMuWHjg2PvUn3PYvPFoQ==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.7", @@ -2285,8 +2656,6 @@ }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.19", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.19.tgz", - "integrity": "sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.7", @@ -2318,8 +2687,6 @@ }, "node_modules/@radix-ui/react-scroll-area": { "version": "1.2.18", - "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.18.tgz", - "integrity": "sha512-Zn5Cd171wxsO3Dfg8HaW6RifTb9CYTKQJHs/G4+LN1GfmJpaQMZQyQxMprVPHpaz7QY4l9BxK2JwQuzHsXC8nA==", "license": "MIT", "dependencies": { "@radix-ui/number": "1.1.3", @@ -2349,8 +2716,6 @@ }, "node_modules/@radix-ui/react-select": { "version": "2.3.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.3.7.tgz", - "integrity": "sha512-WFGImkmbzcfxeIwq/+4HvRN0pizBwbwQUED4I13ezQsDdfl38ZntN6TmR8XaSzPBqoCToe8rF75j6NPNDSzhbg==", "license": "MIT", "dependencies": { "@radix-ui/number": "1.1.3", @@ -2393,8 +2758,6 @@ }, "node_modules/@radix-ui/react-separator": { "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.15.tgz", - "integrity": "sha512-jOLO4lssEzWpoDu7G+Ze4VjwMRUBt291pnZD0gmalREZipnTX3wadQo7Fy48GCTfe14/YRN6rw/rOJqrE85Wxw==", "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.1.10" @@ -2416,8 +2779,6 @@ }, "node_modules/@radix-ui/react-slider": { "version": "1.4.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.4.7.tgz", - "integrity": "sha512-mTSLf1GC/C0moWjTbvCM6Qn/gBjvlFt1azuWF2v7MN5C3Zq2U2J2lN3ZEYkpujuOU5Ro7A28wkviSxaKnG0BYg==", "license": "MIT", "dependencies": { "@radix-ui/number": "1.1.3", @@ -2449,8 +2810,6 @@ }, "node_modules/@radix-ui/react-slot": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.3.tgz", - "integrity": "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.5" @@ -2467,8 +2826,6 @@ }, "node_modules/@radix-ui/react-switch": { "version": "1.3.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.3.7.tgz", - "integrity": "sha512-48tB/4dn2UVLBCYhTu9AuR63IHl73l/qLbLgxd86noTUor4/K4LFDAcYjK+isP5313qxaFpjPVogE7+Y0/V3Kw==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.7", @@ -2495,8 +2852,6 @@ }, "node_modules/@radix-ui/react-tabs": { "version": "1.1.21", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.21.tgz", - "integrity": "sha512-UKxJlZid7FVtsk/WTxj4i4uSEgj2Au+KBbS7SQyTlzMhhn+86Cz3tISZdTa87bfEfcuvZezf2ZsxD4xuEKtkog==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.7", @@ -2525,8 +2880,6 @@ }, "node_modules/@radix-ui/react-toast": { "version": "1.2.23", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.23.tgz", - "integrity": "sha512-ofhyAsYaocRGOs/n0XWdUOSVzEAG6BfrMVM8z0c0kLEWY38w/0WuMFPTJP/HVaZPYkMvHZoKIIhNcjbTCBILPg==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.7", @@ -2559,8 +2912,6 @@ }, "node_modules/@radix-ui/react-toggle": { "version": "1.1.18", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.18.tgz", - "integrity": "sha512-7lonPlKfSacd20GlOBx2ltuVKz9oqWYZz+oMQyOltw6t1y2nyftj2ZmwwUHYn49kqfDWcp8dNZm5NgV+5Z+mug==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.7", @@ -2584,8 +2935,6 @@ }, "node_modules/@radix-ui/react-toggle-group": { "version": "1.1.19", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.19.tgz", - "integrity": "sha512-OtnwuSVjd1Ofi+AdnvhsjQdyuhCDwYs1w9RyB5BN/OavXOVQo42SYqQjwUnbPnaiPFBpQ9aX70dWeee+v2oBLA==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.7", @@ -2613,8 +2962,6 @@ }, "node_modules/@radix-ui/react-toolbar": { "version": "1.1.19", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.19.tgz", - "integrity": "sha512-Ph0IvtYw4VB12ZnZg+YtrGs8yJQsnizwo/zu0R4Y/nWugtJzA7Pg1eWeuDR9+LSqn+xjamss+UOSOJJJ4gx8jw==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.7", @@ -2642,8 +2989,6 @@ }, "node_modules/@radix-ui/react-tooltip": { "version": "1.2.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.16.tgz", - "integrity": "sha512-6EamKFRRnlpdadndbZ6LMwycfwkwPte1B42hs6QA0gYhjaOKqW4PZ4pjaW9UrlDX5eVt/OjncE7BFTPL5nmZhg==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.7", @@ -2677,8 +3022,6 @@ }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.4.tgz", - "integrity": "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -2692,8 +3035,6 @@ }, "node_modules/@radix-ui/react-use-controllable-state": { "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.6.tgz", - "integrity": "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.7", @@ -2712,8 +3053,6 @@ }, "node_modules/@radix-ui/react-use-effect-event": { "version": "0.0.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.5.tgz", - "integrity": "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==", "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" @@ -2730,8 +3069,6 @@ }, "node_modules/@radix-ui/react-use-escape-keydown": { "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.5.tgz", - "integrity": "sha512-ge3ipobwSXTj4JyVtswQ7qZj0ZHdtbGuOno/LrgAAeSxtsJ6Vs4Gz5IkPH2bmqpjcLUFoqGhA/mueuIf63UXlA==", "license": "MIT", "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.4" @@ -2748,8 +3085,6 @@ }, "node_modules/@radix-ui/react-use-is-hydrated": { "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.3.tgz", - "integrity": "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -2763,8 +3098,6 @@ }, "node_modules/@radix-ui/react-use-layout-effect": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz", - "integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -2778,8 +3111,6 @@ }, "node_modules/@radix-ui/react-use-previous": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.4.tgz", - "integrity": "sha512-XoSLhbRbqxFtgJoi2fNHA3C6pDlY34x508vUpUGoFZfvePfHXHbE1lC4FYFMnJWgiCRroSTw6fOsXQoVS9RwZg==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -2793,8 +3124,6 @@ }, "node_modules/@radix-ui/react-use-rect": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.4.tgz", - "integrity": "sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==", "license": "MIT", "dependencies": { "@radix-ui/rect": "1.1.3" @@ -2811,8 +3140,6 @@ }, "node_modules/@radix-ui/react-use-size": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.4.tgz", - "integrity": "sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw==", "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" @@ -2829,8 +3156,6 @@ }, "node_modules/@radix-ui/react-visually-hidden": { "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.11.tgz", - "integrity": "sha512-NFS86RYYZb4/exihaESBGOpMJFz8MGLAfu3mOBSGByVnVPC9JPASfYubxd/8KbkQK0sYAv8lVQDEQukDX/qXvQ==", "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.1.10" @@ -2852,14 +3177,12 @@ }, "node_modules/@radix-ui/rect": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.3.tgz", - "integrity": "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==", "license": "MIT" }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", - "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.2.tgz", + "integrity": "sha512-l7x215OGvo1s52JWmR8U/DAVzEDWBCIbTm28aeJV/WDTSHgcKXaZTuBT0hJMs5NggilfJTW3clZVvd24yfKJxA==", "cpu": [ "arm64" ], @@ -2874,9 +3197,7 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", - "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "version": "1.2.2", "cpu": [ "arm64" ], @@ -2891,9 +3212,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", - "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.2.tgz", + "integrity": "sha512-9W1mbGZAfW3oqd85bhBkmpyHCCzL1TeG/zFFP3vg7b0rlly8cxOcre5nXwz+LHazCwac2MNWgPdPCHndABjpWQ==", "cpu": [ "x64" ], @@ -2908,9 +3229,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", - "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.2.tgz", + "integrity": "sha512-0p1lhiCSCyaerFwtrdZQUx7NqGk6LQnaRKWX7tFQqwQgvX0rjM15cIkm3pax1UpEakK14C4mOxx/jSqCBdBRqQ==", "cpu": [ "x64" ], @@ -2925,9 +3246,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", - "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.2.tgz", + "integrity": "sha512-e+cOJXrJ2L3zx6YzqPg+f6Wbk3V1cKB8bOhbaYdVYN3DdquzNdRAmrbETz1qnt5yp/c7JNlNjmITiA2cVneQ7w==", "cpu": [ "arm" ], @@ -2942,9 +3263,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", - "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.2.tgz", + "integrity": "sha512-JsSMsj6sNat/MuhG5fnBD7QgbtpHKVe30x5/bAVirDHdhoQRXJkF6xc0Jqk8O4fiCUQAzMOoH9wZi3m60c8wtg==", "cpu": [ "arm64" ], @@ -2959,9 +3280,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", - "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.2.tgz", + "integrity": "sha512-B5G/zJdHaoJn9vD50eGHWkiWfmq8Uhi3IiLPJTzmZTrAalk1bztUikSXo0qga18ibE0IXboyeMUnhPjhAJ45wQ==", "cpu": [ "arm64" ], @@ -2976,9 +3297,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", - "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.2.tgz", + "integrity": "sha512-6mC/awzKka8W6EoekjegpfGkjz8jXWDX63pqu/HYVpyKtZfu65Jsh4QAH3Kej3CAv/c1oGX7psTmFEbr0mDxLA==", "cpu": [ "ppc64" ], @@ -2993,9 +3314,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", - "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.2.tgz", + "integrity": "sha512-412MX9fJLdA1IK28EZnc8jYv2HRTleOZgfLQumJ5zy7OeJLZlg/CETwFaXjNmGVxG51cFHpKLqb5LKvBC+HsHA==", "cpu": [ "s390x" ], @@ -3010,9 +3331,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", - "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.2.tgz", + "integrity": "sha512-Q/+HI/ToJafZ1iCqGgVQXUEkIjufHCTF0gBQ2a5o3cg7GJ2h0qyq3nvvSmU+bGda2/7ygXpTY4TM6gO9OhQ0ZA==", "cpu": [ "x64" ], @@ -3027,9 +3348,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", - "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.2.tgz", + "integrity": "sha512-ZKp/w41n6wCvxzxQHtQSbuphfX3Y4cCvbjkKHusrLx4lh+JWLTU7StSltO/DKARISzbj368d+qUaCjI8K2wzXw==", "cpu": [ "x64" ], @@ -3044,9 +3365,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", - "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.2.tgz", + "integrity": "sha512-pxE6xD4KS3eAROkKK5yrhB9/3+vhlhVGMvlQLbdpzrBGDbKrnzx3RLwPaHvasLo6jgaiBLn7e04Df9C4tYhjmA==", "cpu": [ "arm64" ], @@ -3060,107 +3381,339 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", - "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.2.tgz", + "integrity": "sha512-4MqEue5re+xIZzAWsB8sj0P1kqZySWqIuN4t6QaIO/YA6SFwySOLruvWQFzfmqk8LBK2P30KCSJwf6mJCZZ5/A==", "cpu": [ - "wasm32" + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.2.tgz", + "integrity": "sha512-NweNxxD0Nf9t8v7kodun45Ijp3EIwYY+uydPP6qBEYvfBqhIjN6dZMzlQja3tqX/aLs3F3Uz+AxDpKgRhpOZQg==", + "cpu": [ + "x64" ], "dev": true, "license": "MIT", "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@rushstack/node-core-library": { + "version": "5.23.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "~8.20.0", + "ajv-draft-04": "~1.0.0", + "ajv-formats": "~3.0.1", + "fs-extra": "~11.3.0", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.22.1", + "semver": "~7.7.4" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@rushstack/node-core-library/node_modules/ajv": { + "version": "8.20.0", + "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/@rushstack/node-core-library/node_modules/ajv-draft-04": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@rushstack/node-core-library/node_modules/fs-extra": { + "version": "11.3.6", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/json-schema-traverse": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@rushstack/node-core-library/node_modules/jsonfile": { + "version": "6.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/semver": { + "version": "7.7.4", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/universalify": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@rushstack/problem-matcher": { + "version": "0.2.1", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@rushstack/rig-package": { + "version": "0.7.3", + "dev": true, + "license": "MIT", + "dependencies": { + "jju": "~1.4.0", + "resolve": "~1.22.1" + } + }, + "node_modules/@rushstack/terminal": { + "version": "0.24.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@rushstack/node-core-library": "5.23.3", + "@rushstack/problem-matcher": "0.2.1", + "supports-color": "~8.1.1" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@rushstack/terminal/node_modules/supports-color": { + "version": "8.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/@rushstack/ts-command-line": { + "version": "5.3.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@rushstack/terminal": "0.24.2", + "@types/argparse": "1.0.38", + "argparse": "~1.0.9", + "string-argv": "~0.3.1" + } + }, + "node_modules/@rushstack/ts-command-line/node_modules/argparse": { + "version": "1.0.10", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@shikijs/core": { + "version": "4.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/primitive": "4.4.1", + "@shikijs/types": "4.4.1", + "@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.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.1", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.6" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "4.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.1", + "@shikijs/vscode-textmate": "^10.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/langs": { + "version": "4.4.1", + "dev": true, + "license": "MIT", "dependencies": { - "@emnapi/core": "1.11.1", - "@emnapi/runtime": "1.11.1", - "@napi-rs/wasm-runtime": "^1.1.6" + "@shikijs/types": "4.4.1" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=20" } }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", - "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", - "cpu": [ - "arm64" - ], + "node_modules/@shikijs/primitive": { + "version": "4.4.1", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "@shikijs/types": "4.4.1", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=20" } }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", - "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", - "cpu": [ - "x64" - ], + "node_modules/@shikijs/themes": { + "version": "4.4.1", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "@shikijs/types": "4.4.1" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=20" } }, - "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==", + "node_modules/@shikijs/transformers": { + "version": "4.4.1", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@shikijs/core": "4.4.1", + "@shikijs/types": "4.4.1" + }, + "engines": { + "node": ">=20" + } }, - "node_modules/@rollup/pluginutils": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", - "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "node_modules/@shikijs/types": { + "version": "4.4.1", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" }, "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } + "node": ">=20" } }, - "node_modules/@rollup/pluginutils/node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", "dev": true, "license": "MIT" }, - "node_modules/@sec-ant/readable-stream": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", - "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "node_modules/@sindresorhus/is": { + "version": "4.6.0", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } }, "node_modules/@sindresorhus/merge-streams": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", - "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", "dev": true, "license": "MIT", "engines": { @@ -3172,15 +3725,11 @@ }, "node_modules/@standard-schema/spec": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "dev": true, "license": "MIT" }, "node_modules/@tailwindcss/node": { "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", - "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", "dev": true, "license": "MIT", "dependencies": { @@ -3195,8 +3744,6 @@ }, "node_modules/@tailwindcss/node/node_modules/lightningcss": { "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -3246,8 +3793,6 @@ }, "node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-arm64": { "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", "cpu": [ "arm64" ], @@ -3456,8 +4001,6 @@ }, "node_modules/@tailwindcss/oxide": { "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", - "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", "dev": true, "license": "MIT", "engines": { @@ -3497,8 +4040,6 @@ }, "node_modules/@tailwindcss/oxide-darwin-arm64": { "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", - "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", "cpu": [ "arm64" ], @@ -3697,8 +4238,6 @@ }, "node_modules/@tailwindcss/vite": { "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", - "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", "dev": true, "license": "MIT", "dependencies": { @@ -3712,8 +4251,6 @@ }, "node_modules/@ts-morph/common": { "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.27.0.tgz", - "integrity": "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3722,21 +4259,13 @@ "path-browserify": "^1.0.1" } }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", - "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "node_modules/@types/argparse": { + "version": "1.0.38", "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } + "license": "MIT" }, "node_modules/@types/chai": { "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", "dependencies": { @@ -3746,15 +4275,11 @@ }, "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/dom-mediacapture-transform": { "version": "0.1.11", - "resolved": "https://registry.npmjs.org/@types/dom-mediacapture-transform/-/dom-mediacapture-transform-0.1.11.tgz", - "integrity": "sha512-Y2p+nGf1bF2XMttBnsVPHUWzRRZzqUoJAKmiP10b5umnO6DDrWI0BrGDJy1pOHoOULVmGSfFNkQrAlC5dcj6nQ==", "license": "MIT", "dependencies": { "@types/dom-webcodecs": "*" @@ -3762,35 +4287,60 @@ }, "node_modules/@types/dom-webcodecs": { "version": "0.1.13", - "resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.13.tgz", - "integrity": "sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ==", "license": "MIT" }, "node_modules/@types/esrecurse": { "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", - "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", "dev": true, "license": "MIT" }, "node_modules/@types/estree": { "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, + "node_modules/@types/hast": { + "version": "3.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", "dev": true, "license": "MIT" }, "node_modules/@types/node": { "version": "26.1.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz", - "integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==", "dev": true, "license": "MIT", "dependencies": { @@ -3798,43 +4348,51 @@ } }, "node_modules/@types/react": { - "version": "19.2.17", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", - "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", "license": "MIT", "dependencies": { "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" } }, + "node_modules/@types/unist": { + "version": "3.0.3", + "dev": true, + "license": "MIT" + }, "node_modules/@types/validate-npm-package-name": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz", - "integrity": "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", "dev": true, "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", - "integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.66.0.tgz", + "integrity": "sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.65.0", - "@typescript-eslint/type-utils": "8.65.0", - "@typescript-eslint/utils": "8.65.0", - "@typescript-eslint/visitor-keys": "8.65.0", + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/type-utils": "8.66.0", + "@typescript-eslint/utils": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -3847,7 +4405,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.65.0", + "@typescript-eslint/parser": "^8.66.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -3863,16 +4421,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz", - "integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.66.0.tgz", + "integrity": "sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.65.0", - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/typescript-estree": "8.65.0", - "@typescript-eslint/visitor-keys": "8.65.0", + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", "debug": "^4.4.3" }, "engines": { @@ -3888,14 +4446,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", - "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.66.0.tgz", + "integrity": "sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.65.0", - "@typescript-eslint/types": "^8.65.0", + "@typescript-eslint/tsconfig-utils": "^8.66.0", + "@typescript-eslint/types": "^8.66.0", "debug": "^4.4.3" }, "engines": { @@ -3910,14 +4468,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz", - "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.66.0.tgz", + "integrity": "sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/visitor-keys": "8.65.0" + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3928,9 +4486,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", - "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.66.0.tgz", + "integrity": "sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==", "dev": true, "license": "MIT", "engines": { @@ -3945,15 +4503,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz", - "integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.66.0.tgz", + "integrity": "sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/typescript-estree": "8.65.0", - "@typescript-eslint/utils": "8.65.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/utils": "8.66.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -3970,9 +4528,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", - "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.66.0.tgz", + "integrity": "sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==", "dev": true, "license": "MIT", "engines": { @@ -3984,16 +4542,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", - "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.66.0.tgz", + "integrity": "sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.65.0", - "@typescript-eslint/tsconfig-utils": "8.65.0", - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/visitor-keys": "8.65.0", + "@typescript-eslint/project-service": "8.66.0", + "@typescript-eslint/tsconfig-utils": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -4025,16 +4583,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz", - "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.66.0.tgz", + "integrity": "sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.65.0", - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/typescript-estree": "8.65.0" + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4049,13 +4607,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz", - "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.66.0.tgz", + "integrity": "sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/types": "8.66.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -4066,10 +4624,13 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.3", + "dev": true, + "license": "ISC" + }, "node_modules/@vitejs/plugin-react": { "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.4.tgz", - "integrity": "sha512-XcCQz0TBpBgljhj0gMuuDj49i6Ytqh5q1osT/Gp5uAVJUCTWxyskk/l1jwYYiu2xcNHHipdMz40EGfM1VdamVg==", "dev": true, "license": "MIT", "dependencies": { @@ -4092,10 +4653,23 @@ } } }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, "node_modules/@vitest/browser": { "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.1.10.tgz", - "integrity": "sha512-UDwuWGwXj646CBx/bQHOaJSX7np0I8JL/UKQYa1e4QrVHH8VdWtx8eaOuf8sy0ShwDgR6NjJAsp5eF6vjF6qng==", "dev": true, "license": "MIT", "dependencies": { @@ -4117,8 +4691,6 @@ }, "node_modules/@vitest/browser-playwright": { "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.1.10.tgz", - "integrity": "sha512-nMoXGEiRpT7m3W7NsbvrM2aKNwiNHZf+zEpUCvMteGjZFvfT96Q9fh7QyB98dvDWXiKvrLxA7bJ1mCOOv+JQPw==", "dev": true, "license": "MIT", "dependencies": { @@ -4141,8 +4713,6 @@ }, "node_modules/@vitest/coverage-v8": { "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz", - "integrity": "sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==", "dev": true, "license": "MIT", "dependencies": { @@ -4172,8 +4742,6 @@ }, "node_modules/@vitest/expect": { "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", - "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", "dev": true, "license": "MIT", "dependencies": { @@ -4190,8 +4758,6 @@ }, "node_modules/@vitest/mocker": { "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", - "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", "dev": true, "license": "MIT", "dependencies": { @@ -4217,8 +4783,6 @@ }, "node_modules/@vitest/pretty-format": { "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", - "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", "dev": true, "license": "MIT", "dependencies": { @@ -4230,8 +4794,6 @@ }, "node_modules/@vitest/runner": { "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", - "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", "dev": true, "license": "MIT", "dependencies": { @@ -4244,8 +4806,6 @@ }, "node_modules/@vitest/snapshot": { "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", - "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", "dev": true, "license": "MIT", "dependencies": { @@ -4260,8 +4820,6 @@ }, "node_modules/@vitest/spy": { "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", - "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", "dev": true, "license": "MIT", "funding": { @@ -4270,8 +4828,6 @@ }, "node_modules/@vitest/utils": { "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", - "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", "dev": true, "license": "MIT", "dependencies": { @@ -4283,39 +4839,231 @@ "url": "https://opencollective.com/vitest" } }, - "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==", + "node_modules/@vue/compiler-core": { + "version": "3.5.40", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.40", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-core/node_modules/estree-walker": { + "version": "2.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.40", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.40", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.40", + "@vue/compiler-dom": "3.5.40", + "@vue/compiler-ssr": "3.5.40", + "@vue/shared": "3.5.40", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.19", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-sfc/node_modules/estree-walker": { + "version": "2.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.40", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/devtools-api": { + "version": "8.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^8.2.1" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "8.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^8.2.1", + "birpc": "^2.6.1", + "hookable": "^5.5.3", + "perfect-debounce": "^2.0.0" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "8.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.40", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.40", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.40", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.40", + "@vue/runtime-core": "3.5.40", + "@vue/shared": "3.5.40", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.40", "dev": true, "license": "MIT", "dependencies": { - "@volar/source-map": "2.4.28" + "@vue/compiler-ssr": "3.5.40", + "@vue/runtime-dom": "3.5.40", + "@vue/shared": "3.5.40" } }, - "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==", + "node_modules/@vue/shared": { + "version": "3.5.40", "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==", + "node_modules/@vueuse/core": { + "version": "14.4.0", "dev": true, "license": "MIT", "dependencies": { - "@volar/language-core": "2.4.28", - "path-browserify": "^1.0.1", - "vscode-uri": "^3.0.8" + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.4.0", + "@vueuse/shared": "14.4.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/integrations": { + "version": "14.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@vueuse/core": "14.4.0", + "@vueuse/shared": "14.4.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "^4", + "axios": "^1", + "change-case": "^5", + "drauu": "^0.4", + "focus-trap": "^7 || ^8", + "fuse.js": "^7", + "idb-keyval": "^6", + "jwt-decode": "^4", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^7 || ^8", + "vue": "^3.5.0" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "14.4.0", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "14.4.0", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" } }, "node_modules/@zip.js/zip.js": { "version": "2.8.34", - "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.34.tgz", - "integrity": "sha512-+6a3lyqq69rpseLbvDPiVIWsZ/HdTGAAD6afFtug6ECPDGttb2dHnPC6cJgdPofYkzL9OvXizegq+DQVfL2rnA==", "license": "BSD-3-Clause", "engines": { "bun": ">=0.7.0", @@ -4325,8 +5073,6 @@ }, "node_modules/accepts": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "dev": true, "license": "MIT", "dependencies": { @@ -4339,8 +5085,6 @@ }, "node_modules/acorn": { "version": "8.18.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", - "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "dev": true, "license": "MIT", "bin": { @@ -4352,8 +5096,6 @@ }, "node_modules/acorn-jsx": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -4362,8 +5104,6 @@ }, "node_modules/ajv": { "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -4379,8 +5119,6 @@ }, "node_modules/ajv-formats": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4397,8 +5135,6 @@ }, "node_modules/ajv-formats/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": { @@ -4414,35 +5150,60 @@ }, "node_modules/ajv-formats/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/ansi-colors": { "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "dev": true, + "license": "MIT" + }, "node_modules/apache-arrow": { "version": "21.2.0", - "resolved": "https://registry.npmjs.org/apache-arrow/-/apache-arrow-21.2.0.tgz", - "integrity": "sha512-Hxe6Agq26gQOM954qpzYSllJBPJl+e16U5CkfuMUhLrNba+5nKkttIVlflaovN6oaTratqMGAO8H5u/aNhmHWQ==", "license": "Apache-2.0", "dependencies": { "@types/node": "^25.2.0", @@ -4456,8 +5217,6 @@ }, "node_modules/apache-arrow/node_modules/@types/node": { "version": "25.9.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.5.tgz", - "integrity": "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==", "license": "MIT", "dependencies": { "undici-types": ">=7.24.0 <7.24.7" @@ -4465,21 +5224,15 @@ }, "node_modules/apache-arrow/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/argparse": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, "license": "Python-2.0" }, "node_modules/aria-hidden": { "version": "1.2.6", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", - "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", "license": "MIT", "dependencies": { "tslib": "^2.0.0" @@ -4488,10 +5241,16 @@ "node": ">=10" } }, + "node_modules/array-union": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "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": { @@ -4500,8 +5259,6 @@ }, "node_modules/ast-types": { "version": "0.16.1", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", - "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", "dev": true, "license": "MIT", "dependencies": { @@ -4513,8 +5270,6 @@ }, "node_modules/ast-v8-to-istanbul": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.5.tgz", - "integrity": "sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==", "dev": true, "license": "MIT", "dependencies": { @@ -4525,25 +5280,27 @@ }, "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { "version": "10.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", - "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", "dev": true, "license": "MIT" }, "node_modules/atomically": { "version": "1.7.0", - "resolved": "https://registry.npmjs.org/atomically/-/atomically-1.7.0.tgz", - "integrity": "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==", "dev": true, "license": "MIT", "engines": { "node": ">=10.12.0" } }, + "node_modules/axe-core": { + "version": "4.12.1", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, "node_modules/balanced-match": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", "engines": { @@ -4552,8 +5309,6 @@ }, "node_modules/baseline-browser-mapping": { "version": "2.10.38", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz", - "integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -4563,10 +5318,27 @@ "node": ">=6.0.0" } }, + "node_modules/better-path-resolve": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-windows": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/body-parser": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", - "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", "dev": true, "license": "MIT", "dependencies": { @@ -4590,8 +5362,6 @@ }, "node_modules/body-parser/node_modules/content-type": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", "dev": true, "license": "MIT", "engines": { @@ -4603,9 +5373,7 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", - "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", + "version": "5.0.9", "dev": true, "license": "MIT", "dependencies": { @@ -4617,8 +5385,6 @@ }, "node_modules/braces": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "license": "MIT", "dependencies": { @@ -4630,8 +5396,6 @@ }, "node_modules/browserslist": { "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "dev": true, "funding": [ { @@ -4664,8 +5428,6 @@ }, "node_modules/bundle-name": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", - "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", "dev": true, "license": "MIT", "dependencies": { @@ -4680,8 +5442,6 @@ }, "node_modules/bytes": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true, "license": "MIT", "engines": { @@ -4690,8 +5450,6 @@ }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4704,8 +5462,6 @@ }, "node_modules/call-bound": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "dev": true, "license": "MIT", "dependencies": { @@ -4721,8 +5477,6 @@ }, "node_modules/callsites": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, "license": "MIT", "engines": { @@ -4731,8 +5485,6 @@ }, "node_modules/caniuse-lite": { "version": "1.0.30001799", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", - "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", "dev": true, "funding": [ { @@ -4750,10 +5502,17 @@ ], "license": "CC-BY-4.0" }, + "node_modules/ccount": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chai": { "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", "engines": { @@ -4762,8 +5521,6 @@ }, "node_modules/chalk": { "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "dev": true, "license": "MIT", "engines": { @@ -4773,10 +5530,44 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/char-regex": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chardet": { + "version": "2.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "dev": true, + "license": "MIT" + }, "node_modules/class-variance-authority": { "version": "0.7.1", - "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", - "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", "license": "Apache-2.0", "dependencies": { "clsx": "^2.1.1" @@ -4787,8 +5578,6 @@ }, "node_modules/cli-cursor": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "dev": true, "license": "MIT", "dependencies": { @@ -4801,10 +5590,43 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-highlight": { + "version": "2.1.11", + "dev": true, + "license": "ISC", + "dependencies": { + "chalk": "^4.0.0", + "highlight.js": "^10.7.1", + "mz": "^2.4.0", + "parse5": "^5.1.1", + "parse5-htmlparser2-tree-adapter": "^6.0.0", + "yargs": "^16.0.0" + }, + "bin": { + "highlight": "bin/highlight" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/cli-highlight/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/cli-spinners": { "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", "dev": true, "license": "MIT", "engines": { @@ -4814,10 +5636,68 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-table3": { + "version": "0.6.5", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-table3/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-table3/node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "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" @@ -4825,38 +5705,48 @@ }, "node_modules/code-block-writer": { "version": "13.0.3", - "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", - "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", "dev": true, "license": "MIT" }, "node_modules/comlink": { "version": "4.4.2", - "resolved": "https://registry.npmjs.org/comlink/-/comlink-4.4.2.tgz", - "integrity": "sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g==", "license": "Apache-2.0" }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/commander": { "version": "14.0.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", - "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", "dev": true, "license": "MIT", "engines": { "node": ">=20" } }, - "node_modules/compare-versions": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz", - "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==", - "dev": true, - "license": "MIT" - }, "node_modules/conf": { "version": "10.2.0", - "resolved": "https://registry.npmjs.org/conf/-/conf-10.2.0.tgz", - "integrity": "sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg==", "dev": true, "license": "MIT", "dependencies": { @@ -4880,8 +5770,6 @@ }, "node_modules/conf/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": { @@ -4897,8 +5785,6 @@ }, "node_modules/conf/node_modules/ajv-formats": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, "license": "MIT", "dependencies": { @@ -4915,22 +5801,16 @@ }, "node_modules/conf/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/conf/node_modules/json-schema-typed": { "version": "7.0.3", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-7.0.3.tgz", - "integrity": "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==", "dev": true, "license": "BSD-2-Clause" }, "node_modules/conf/node_modules/semver": { "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", "bin": { @@ -4940,17 +5820,8 @@ "node": ">=10" } }, - "node_modules/confbox": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", - "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", - "dev": true, - "license": "MIT" - }, "node_modules/content-disposition": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", - "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", "dev": true, "license": "MIT", "engines": { @@ -4963,8 +5834,6 @@ }, "node_modules/content-type": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true, "license": "MIT", "engines": { @@ -4973,15 +5842,11 @@ }, "node_modules/convert-source-map": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, "license": "MIT" }, "node_modules/cookie": { "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "dev": true, "license": "MIT", "engines": { @@ -4990,8 +5855,6 @@ }, "node_modules/cookie-signature": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "dev": true, "license": "MIT", "engines": { @@ -5000,8 +5863,6 @@ }, "node_modules/cors": { "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", "dev": true, "license": "MIT", "dependencies": { @@ -5018,8 +5879,6 @@ }, "node_modules/cosmiconfig": { "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", - "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", "dev": true, "license": "MIT", "dependencies": { @@ -5045,8 +5904,6 @@ }, "node_modules/cross-spawn": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -5060,8 +5917,6 @@ }, "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==", "dev": true, "license": "MIT", "bin": { @@ -5073,14 +5928,10 @@ }, "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/debounce-fn": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz", - "integrity": "sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5095,8 +5946,6 @@ }, "node_modules/debug": { "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -5113,8 +5962,6 @@ }, "node_modules/dedent": { "version": "1.7.2", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", - "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -5128,15 +5975,11 @@ }, "node_modules/deep-is": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, "license": "MIT" }, "node_modules/deepmerge": { "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true, "license": "MIT", "engines": { @@ -5145,8 +5988,6 @@ }, "node_modules/default-browser": { "version": "5.5.0", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", - "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", "dev": true, "license": "MIT", "dependencies": { @@ -5162,8 +6003,6 @@ }, "node_modules/default-browser-id": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", - "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", "dev": true, "license": "MIT", "engines": { @@ -5175,8 +6014,6 @@ }, "node_modules/define-lazy-prop": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", "dev": true, "license": "MIT", "engines": { @@ -5188,18 +6025,30 @@ }, "node_modules/depd": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" } }, + "node_modules/dequal": { + "version": "2.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-indent": { + "version": "6.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-libc": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -5208,24 +6057,41 @@ }, "node_modules/detect-node-es": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, + "node_modules/devlop": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/diff": { "version": "8.0.4", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", - "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } }, + "node_modules/dir-glob": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/dockview": { "version": "7.0.4", - "resolved": "https://registry.npmjs.org/dockview/-/dockview-7.0.4.tgz", - "integrity": "sha512-n6n9WpYZgp/WY8SgvP4hr9qh01ZXhSGIRmlhoJXxRU3f34bwxCbFyOhBArV4W8quolX8OQe1yPfAUwUUjdnZPA==", "license": "MIT", "dependencies": { "dockview-core": "^7.0.4" @@ -5233,14 +6099,10 @@ }, "node_modules/dockview-core": { "version": "7.0.4", - "resolved": "https://registry.npmjs.org/dockview-core/-/dockview-core-7.0.4.tgz", - "integrity": "sha512-AiIzD6ov153L/VuhqVBg5KD5oSAgJGH7L1xvzV/X+ghIEOTFfEQYEBGNd/ys+ZjQfdGRogHSeQ0v9JF/L6JrPg==", "license": "MIT" }, "node_modules/dockview-react": { "version": "7.0.4", - "resolved": "https://registry.npmjs.org/dockview-react/-/dockview-react-7.0.4.tgz", - "integrity": "sha512-eUhR3J3prpuFcaPJwQ4rzcRAghO3p4JVXlj5zfdQTlb3Qwa8VUpy3KvyV6BCsIjNk1toTdd8/NyADcZXMvpmtA==", "license": "MIT", "dependencies": { "dockview": "^7.0.4" @@ -5252,8 +6114,6 @@ }, "node_modules/dot-prop": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", - "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", "dev": true, "license": "MIT", "dependencies": { @@ -5268,8 +6128,6 @@ }, "node_modules/dotenv": { "version": "17.4.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", - "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -5281,8 +6139,6 @@ }, "node_modules/dunder-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "dev": true, "license": "MIT", "dependencies": { @@ -5296,29 +6152,26 @@ }, "node_modules/ee-first": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "dev": true, "license": "MIT" }, "node_modules/electron-to-chromium": { "version": "1.5.375", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.375.tgz", - "integrity": "sha512-ZWP5eB4BVPW/ZYo9252hQZHZ5XavtsTgpbhcmMmRwymavC5AsLWQWBPaKMeNd2LW0KGby5HPXvj7+sr4ta5j/Q==", "dev": true, "license": "ISC" }, "node_modules/emoji-regex": { "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/emojilib": { + "version": "2.4.0", "dev": true, "license": "MIT" }, "node_modules/encodeurl": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true, "license": "MIT", "engines": { @@ -5327,8 +6180,6 @@ }, "node_modules/enhanced-resolve": { "version": "5.24.4", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.4.tgz", - "integrity": "sha512-GVoi+ICHocoOIU7qVVM48wOJziRsqrsyqlI0Ce0LdowRn6v3bcH2zUa9kp85ncx0nwIb9/HOCOLS3fdThDG/XQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5341,8 +6192,6 @@ }, "node_modules/enquirer": { "version": "2.4.1", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", - "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5353,20 +6202,38 @@ "node": ">=8.6" } }, + "node_modules/entities": { + "version": "7.0.1", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/env-paths": { "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/environment": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/error-ex": { "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5375,8 +6242,6 @@ }, "node_modules/es-define-property": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, "license": "MIT", "engines": { @@ -5385,8 +6250,6 @@ }, "node_modules/es-errors": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, "license": "MIT", "engines": { @@ -5395,15 +6258,11 @@ }, "node_modules/es-module-lexer": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", - "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", "dev": true, "license": "MIT" }, "node_modules/es-object-atoms": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "dev": true, "license": "MIT", "dependencies": { @@ -5415,8 +6274,6 @@ }, "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": { @@ -5425,15 +6282,11 @@ }, "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==", "dev": true, "license": "MIT" }, "node_modules/escape-string-regexp": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", "engines": { @@ -5445,8 +6298,6 @@ }, "node_modules/eslint": { "version": "10.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz", - "integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==", "dev": true, "license": "MIT", "workspaces": [ @@ -5504,8 +6355,6 @@ }, "node_modules/eslint-plugin-react-hooks": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", - "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", "dev": true, "license": "MIT", "dependencies": { @@ -5524,8 +6373,6 @@ }, "node_modules/eslint-plugin-react-refresh": { "version": "0.5.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.3.tgz", - "integrity": "sha512-5EMmLCV98Pi4o/f/3DP/v/tNqLHMIc9I8LKClNDWhZ9JTho89/kQcitCXQBMG7sAfVRK0Ie3T2EDOzp1YXYiVA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -5534,8 +6381,6 @@ }, "node_modules/eslint-scope": { "version": "9.1.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", - "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5553,8 +6398,6 @@ }, "node_modules/eslint-visitor-keys": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -5566,8 +6409,6 @@ }, "node_modules/espree": { "version": "11.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", - "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5584,8 +6425,6 @@ }, "node_modules/esprima": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true, "license": "BSD-2-Clause", "bin": { @@ -5598,8 +6437,6 @@ }, "node_modules/esquery": { "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -5611,8 +6448,6 @@ }, "node_modules/esrecurse": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5624,8 +6459,6 @@ }, "node_modules/estraverse": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -5634,8 +6467,6 @@ }, "node_modules/estree-walker": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", "dependencies": { @@ -5644,8 +6475,6 @@ }, "node_modules/esutils": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -5654,8 +6483,6 @@ }, "node_modules/etag": { "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "dev": true, "license": "MIT", "engines": { @@ -5664,8 +6491,6 @@ }, "node_modules/eventsource": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", "dev": true, "license": "MIT", "dependencies": { @@ -5677,8 +6502,6 @@ }, "node_modules/eventsource-parser": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", - "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", "dev": true, "license": "MIT", "engines": { @@ -5687,8 +6510,6 @@ }, "node_modules/execa": { "version": "9.6.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", - "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", "dev": true, "license": "MIT", "dependencies": { @@ -5714,8 +6535,6 @@ }, "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": { @@ -5724,8 +6543,6 @@ }, "node_modules/express": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "dev": true, "license": "MIT", "dependencies": { @@ -5768,8 +6585,6 @@ }, "node_modules/express-rate-limit": { "version": "8.6.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.1.tgz", - "integrity": "sha512-0D493aP61w0TJ2A0wy27riRsO7FMQ7FK+KUHOKCSfPvYo0R55aiC6emCVgFUeShH0fq0ICPVzNcgoS+BsbXQCA==", "dev": true, "license": "MIT", "dependencies": { @@ -5786,24 +6601,18 @@ "express": ">= 4.11" } }, - "node_modules/exsolve": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.1.tgz", - "integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==", + "node_modules/extendable-error": { + "version": "0.1.7", "dev": true, "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-glob": { "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, "license": "MIT", "dependencies": { @@ -5819,8 +6628,6 @@ }, "node_modules/fast-glob/node_modules/glob-parent": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "license": "ISC", "dependencies": { @@ -5832,22 +6639,16 @@ }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, "license": "MIT" }, "node_modules/fast-uri": { "version": "3.1.5", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", - "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", "dev": true, "funding": [ { @@ -5863,8 +6664,6 @@ }, "node_modules/fastq": { "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, "license": "ISC", "dependencies": { @@ -5873,8 +6672,6 @@ }, "node_modules/fdir": { "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", "engines": { @@ -5891,14 +6688,10 @@ }, "node_modules/fflate": { "version": "0.8.3", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", - "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", "license": "MIT" }, "node_modules/figures": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", - "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", "dev": true, "license": "MIT", "dependencies": { @@ -5913,8 +6706,6 @@ }, "node_modules/file-entry-cache": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5926,8 +6717,6 @@ }, "node_modules/fill-range": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "license": "MIT", "dependencies": { @@ -5939,8 +6728,6 @@ }, "node_modules/finalhandler": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "dev": true, "license": "MIT", "dependencies": { @@ -5961,8 +6748,6 @@ }, "node_modules/find-up": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", "dependencies": { @@ -5978,8 +6763,6 @@ }, "node_modules/flat-cache": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", "dependencies": { @@ -5992,21 +6775,23 @@ }, "node_modules/flatbuffers": { "version": "25.9.23", - "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz", - "integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==", "license": "Apache-2.0" }, "node_modules/flatted": { "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, + "node_modules/focus-trap": { + "version": "8.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "tabbable": "^6.5.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "dev": true, "license": "MIT", "engines": { @@ -6015,8 +6800,6 @@ }, "node_modules/framer-motion": { "version": "12.43.0", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.43.0.tgz", - "integrity": "sha512-1eaL3RvR/kAlbG7UYcpMptEyzPoENO0c6w7ZnB3/hh2vSAz/6uGAFn6fdoqTBguNstf3MsFhJHsD/0DHiclG+g==", "license": "MIT", "dependencies": { "motion-dom": "^12.43.0", @@ -6042,20 +6825,28 @@ }, "node_modules/fresh": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" } }, + "node_modules/fs-extra": { + "version": "7.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, "node_modules/fsevents": { "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, - "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ @@ -6067,8 +6858,6 @@ }, "node_modules/function-bind": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, "license": "MIT", "funding": { @@ -6077,25 +6866,27 @@ }, "node_modules/fuzzysort": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-3.1.0.tgz", - "integrity": "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==", "dev": true, "license": "MIT" }, "node_modules/gensync": { "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-east-asian-width": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", - "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", "dev": true, "license": "MIT", "engines": { @@ -6107,8 +6898,6 @@ }, "node_modules/get-intrinsic": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6132,8 +6921,6 @@ }, "node_modules/get-nonce": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", "license": "MIT", "engines": { "node": ">=6" @@ -6141,8 +6928,6 @@ }, "node_modules/get-own-enumerable-keys": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-own-enumerable-keys/-/get-own-enumerable-keys-1.0.0.tgz", - "integrity": "sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA==", "dev": true, "license": "MIT", "engines": { @@ -6154,8 +6939,6 @@ }, "node_modules/get-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, "license": "MIT", "dependencies": { @@ -6168,8 +6951,6 @@ }, "node_modules/get-stream": { "version": "9.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", - "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", "dev": true, "license": "MIT", "dependencies": { @@ -6185,8 +6966,6 @@ }, "node_modules/glob-parent": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", "dependencies": { @@ -6197,9 +6976,9 @@ } }, "node_modules/globals": { - "version": "17.8.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.8.0.tgz", - "integrity": "sha512-Zz/LMDZScFmkakeL2cTHzf+PbWKdpU3uclqkZT7TjDG58j5WPt0PpA+n9uPI24fZtlw07q0OtEi84K+umsRzqQ==", + "version": "17.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.9.0.tgz", + "integrity": "sha512-m/MvAW61QVU5VDNF1Vj8axt016h8w7L5TU1e9zlab7XIttAT2YAlCwl75K1fOqvMM9apmD7lbCIRhpfkhmxhCg==", "dev": true, "license": "MIT", "engines": { @@ -6209,10 +6988,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globby": { + "version": "11.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, "license": "MIT", "engines": { @@ -6224,15 +7020,11 @@ }, "node_modules/graceful-fs": { "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true, "license": "ISC" }, "node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", "engines": { @@ -6241,8 +7033,6 @@ }, "node_modules/has-symbols": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, "license": "MIT", "engines": { @@ -6254,8 +7044,6 @@ }, "node_modules/hasown": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "dev": true, "license": "MIT", "dependencies": { @@ -6265,44 +7053,90 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "dev": true, + "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-whitespace": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hermes-estree": { "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", - "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", "dev": true, "license": "MIT" }, "node_modules/hermes-parser": { "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", - "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", "dev": true, "license": "MIT", "dependencies": { "hermes-estree": "0.25.1" } }, + "node_modules/highlight.js": { + "version": "10.7.3", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, "node_modules/hono": { - "version": "4.12.33", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.33.tgz", - "integrity": "sha512-+SwvkaiJtxsiPjhy9LivY/1m7UsNqCJetM1BrZl9A5DkQhlbHQDU730mMiDPWjnoCYOM8Chf3WrCJw27kNTPFQ==", + "version": "4.13.0", "dev": true, "license": "MIT", "engines": { "node": ">=16.9.0" } }, + "node_modules/hookable": { + "version": "5.5.3", + "dev": true, + "license": "MIT" + }, "node_modules/html-escaper": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true, "license": "MIT" }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/http-errors": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6320,10 +7154,16 @@ "url": "https://opencollective.com/express" } }, + "node_modules/human-id": { + "version": "4.2.0", + "dev": true, + "license": "MIT", + "bin": { + "human-id": "dist/cli.js" + } + }, "node_modules/human-signals": { "version": "8.0.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", - "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -6332,8 +7172,6 @@ }, "node_modules/iconv-lite": { "version": "0.7.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", - "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6349,8 +7187,6 @@ }, "node_modules/ignore": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", "engines": { @@ -6359,8 +7195,6 @@ }, "node_modules/import-fresh": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6376,18 +7210,22 @@ }, "node_modules/import-fresh/node_modules/resolve-from": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "license": "MIT", "engines": { "node": ">=4" } }, + "node_modules/import-lazy": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", "engines": { @@ -6396,25 +7234,19 @@ }, "node_modules/inherits": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true, "license": "ISC" }, "node_modules/intl-messageformat": { - "version": "11.2.12", - "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.2.12.tgz", - "integrity": "sha512-KW70Xxfcvy7vV3qODfvShWkFDPMqKDAa4N+hSyVBWGNtVhTUFYaqlD/l88DaYPKiVcPP4rPQ3qnH7i5K82Mg7g==", + "version": "11.2.13", "license": "BSD-3-Clause", "dependencies": { "@formatjs/fast-memoize": "3.1.7", - "@formatjs/icu-messageformat-parser": "3.5.15" + "@formatjs/icu-messageformat-parser": "3.5.16" } }, "node_modules/ip-address": { "version": "10.4.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.4.0.tgz", - "integrity": "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==", "dev": true, "license": "MIT", "engines": { @@ -6423,8 +7255,6 @@ }, "node_modules/ipaddr.js": { "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "dev": true, "license": "MIT", "engines": { @@ -6433,15 +7263,25 @@ }, "node_modules/is-arrayish": { "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true, "license": "MIT" }, + "node_modules/is-core-module": { + "version": "2.16.2", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-docker": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", "dev": true, "license": "MIT", "bin": { @@ -6456,18 +7296,22 @@ }, "node_modules/is-extglob": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", "dependencies": { @@ -6479,8 +7323,6 @@ }, "node_modules/is-in-ssh": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", - "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", "dev": true, "license": "MIT", "engines": { @@ -6492,8 +7334,6 @@ }, "node_modules/is-inside-container": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", "dev": true, "license": "MIT", "dependencies": { @@ -6511,8 +7351,6 @@ }, "node_modules/is-interactive": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", - "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", "dev": true, "license": "MIT", "engines": { @@ -6524,8 +7362,6 @@ }, "node_modules/is-number": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, "license": "MIT", "engines": { @@ -6534,8 +7370,6 @@ }, "node_modules/is-obj": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", "dev": true, "license": "MIT", "engines": { @@ -6544,8 +7378,6 @@ }, "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==", "dev": true, "license": "MIT", "engines": { @@ -6557,15 +7389,11 @@ }, "node_modules/is-promise": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "dev": true, "license": "MIT" }, "node_modules/is-regexp": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz", - "integrity": "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==", "dev": true, "license": "MIT", "engines": { @@ -6577,8 +7405,6 @@ }, "node_modules/is-stream": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", - "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", "dev": true, "license": "MIT", "engines": { @@ -6588,10 +7414,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-subdir": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "better-path-resolve": "1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/is-unicode-supported": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", "dev": true, "license": "MIT", "engines": { @@ -6601,10 +7436,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-windows": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-wsl": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", - "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", "dev": true, "license": "MIT", "dependencies": { @@ -6619,15 +7460,11 @@ }, "node_modules/isexe": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -6636,8 +7473,6 @@ }, "node_modules/istanbul-lib-report": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -6651,8 +7486,6 @@ }, "node_modules/istanbul-reports": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -6665,18 +7498,19 @@ }, "node_modules/jiti": { "version": "2.7.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", - "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "dev": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jju": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, "node_modules/jose": { "version": "6.2.7", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.7.tgz", - "integrity": "sha512-hq1OB1bALKfydZNoViyg6hPVGV4i93ny9Op+n4zP5RSf7SCZEXa/TsG2O3IEr7+WlHRTPnpqDmHfMH6qXAD60w==", "dev": true, "license": "MIT", "funding": { @@ -6685,15 +7519,11 @@ }, "node_modules/js-tokens": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true, "license": "MIT" }, "node_modules/js-yaml": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", - "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "dev": true, "funding": [ { @@ -6715,8 +7545,6 @@ }, "node_modules/jsesc": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, "license": "MIT", "bin": { @@ -6728,49 +7556,35 @@ }, "node_modules/json-buffer": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, "license": "MIT" }, "node_modules/json-schema-typed": { "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", "dev": true, "license": "BSD-2-Clause" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, "license": "MIT" }, "node_modules/json-with-bigint": { "version": "3.5.10", - "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.10.tgz", - "integrity": "sha512-Vcx+JVNEBts/xfcoCS69sKrOhOk/3TVlvlT+XzUOefVKnnrbYSCKpDCm10pohsJFtsJVYnwa/cXRZ4eElzaM6w==", "license": "MIT" }, "node_modules/json5": { "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "license": "MIT", "bin": { @@ -6780,10 +7594,16 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/keyv": { "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", "dependencies": { @@ -6792,25 +7612,14 @@ }, "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/kolorist": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", - "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", - "dev": true, - "license": "MIT" - }, "node_modules/levn": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6823,8 +7632,6 @@ }, "node_modules/lightningcss": { "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", - "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -6874,8 +7681,6 @@ }, "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" ], @@ -7084,33 +7889,11 @@ }, "node_modules/lines-and-columns": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true, "license": "MIT" }, - "node_modules/local-pkg": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.2.1.tgz", - "integrity": "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "mlly": "^1.7.4", - "pkg-types": "^2.3.0", - "quansync": "^0.2.11" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/locate-path": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", "dependencies": { @@ -7123,10 +7906,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.startcase": { + "version": "4.4.0", + "dev": true, + "license": "MIT" + }, "node_modules/log-symbols": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", - "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", "dev": true, "license": "MIT", "dependencies": { @@ -7142,8 +7928,6 @@ }, "node_modules/log-symbols/node_modules/is-unicode-supported": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", "dev": true, "license": "MIT", "engines": { @@ -7155,8 +7939,6 @@ }, "node_modules/lru-cache": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, "license": "ISC", "dependencies": { @@ -7165,8 +7947,6 @@ }, "node_modules/lucide-react": { "version": "1.28.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.28.0.tgz", - "integrity": "sha512-fARAFJULsGuDDydjp6+6blekG/sBIM29TerzLjc9bQUKAcEfrSc4ZQKb25KRz4OMKd87cZTb5dgq0w/T6KufVg==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -7174,8 +7954,6 @@ }, "node_modules/magic-string": { "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7184,8 +7962,6 @@ }, "node_modules/magicast": { "version": "0.5.3", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", - "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", "dev": true, "license": "MIT", "dependencies": { @@ -7196,8 +7972,6 @@ }, "node_modules/make-dir": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, "license": "MIT", "dependencies": { @@ -7212,8 +7986,6 @@ }, "node_modules/make-dir/node_modules/semver": { "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", "bin": { @@ -7223,20 +7995,83 @@ "node": ">=10" } }, + "node_modules/mark.js": { + "version": "8.11.1", + "dev": true, + "license": "MIT" + }, + "node_modules/marked": { + "version": "9.1.6", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 16" + } + }, + "node_modules/marked-terminal": { + "version": "7.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "ansi-regex": "^6.1.0", + "chalk": "^5.4.1", + "cli-highlight": "^2.1.11", + "cli-table3": "^0.6.5", + "node-emoji": "^2.2.0", + "supports-hyperlinks": "^3.1.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "marked": ">=1 <16" + } + }, + "node_modules/marked-terminal/node_modules/ansi-regex": { + "version": "6.2.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" } }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "dev": true, + "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/media-typer": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", - "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", "dev": true, "license": "MIT", "engines": { @@ -7248,9 +8083,7 @@ } }, "node_modules/mediabunny": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.51.0.tgz", - "integrity": "sha512-u327374xU8Ho0gCaMII7fUK8t0PnqkabCox1k8uUwvgvGb9o6YQGZEG2Qr4DTe7nTMpzfL7ukgnHDvDROySZ+Q==", + "version": "1.52.3", "license": "MPL-2.0", "workspaces": [ ".", @@ -7267,8 +8100,6 @@ }, "node_modules/merge-descriptors": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "dev": true, "license": "MIT", "engines": { @@ -7280,25 +8111,103 @@ }, "node_modules/merge-stream": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true, "license": "MIT" }, "node_modules/merge2": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, "license": "MIT", "engines": { "node": ">= 8" } }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "dev": true, + "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", + "dev": true, + "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", + "dev": true, + "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", + "dev": true, + "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.2", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", "dependencies": { @@ -7311,8 +8220,6 @@ }, "node_modules/micromatch/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==", "dev": true, "license": "MIT", "engines": { @@ -7324,8 +8231,6 @@ }, "node_modules/mime-db": { "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "dev": true, "license": "MIT", "engines": { @@ -7334,8 +8239,6 @@ }, "node_modules/mime-types": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "dev": true, "license": "MIT", "dependencies": { @@ -7351,8 +8254,6 @@ }, "node_modules/mimic-fn": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", - "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", "dev": true, "license": "MIT", "engines": { @@ -7361,8 +8262,6 @@ }, "node_modules/mimic-function": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", "dev": true, "license": "MIT", "engines": { @@ -7374,8 +8273,6 @@ }, "node_modules/minimatch": { "version": "10.2.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", - "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -7390,50 +8287,19 @@ }, "node_modules/minimist": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/mlly": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", - "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.16.0", - "pathe": "^2.0.3", - "pkg-types": "^1.3.1", - "ufo": "^1.6.3" - } - }, - "node_modules/mlly/node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "node_modules/minisearch": { + "version": "7.2.0", "dev": true, "license": "MIT" }, - "node_modules/mlly/node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, "node_modules/motion-dom": { "version": "12.43.0", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.43.0.tgz", - "integrity": "sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag==", "license": "MIT", "dependencies": { "motion-utils": "^12.39.0" @@ -7441,14 +8307,18 @@ }, "node_modules/motion-utils": { "version": "12.39.0", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz", - "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==", "license": "MIT" }, + "node_modules/mri": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/mrmime": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", - "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", "dev": true, "license": "MIT", "engines": { @@ -7457,15 +8327,21 @@ }, "node_modules/ms": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, "license": "MIT" }, + "node_modules/mz": { + "version": "2.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "dev": true, "funding": [ { @@ -7483,15 +8359,11 @@ }, "node_modules/natural-compare": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, "license": "MIT" }, "node_modules/negotiator": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "dev": true, "license": "MIT", "engines": { @@ -7500,18 +8372,28 @@ }, "node_modules/next-themes": { "version": "0.4.6", - "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", - "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", "license": "MIT", "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, + "node_modules/node-emoji": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.6.0", + "char-regex": "^1.0.2", + "emojilib": "^2.4.0", + "skin-tone": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/node-releases": { "version": "2.0.48", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.48.tgz", - "integrity": "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==", "dev": true, "license": "MIT", "engines": { @@ -7520,8 +8402,6 @@ }, "node_modules/npm-run-path": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", - "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", "dev": true, "license": "MIT", "dependencies": { @@ -7537,8 +8417,6 @@ }, "node_modules/npm-run-path/node_modules/path-key": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", "dev": true, "license": "MIT", "engines": { @@ -7550,8 +8428,6 @@ }, "node_modules/object-assign": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true, "license": "MIT", "engines": { @@ -7560,8 +8436,6 @@ }, "node_modules/object-inspect": { "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, "license": "MIT", "engines": { @@ -7573,8 +8447,6 @@ }, "node_modules/object-treeify": { "version": "1.1.33", - "resolved": "https://registry.npmjs.org/object-treeify/-/object-treeify-1.1.33.tgz", - "integrity": "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==", "dev": true, "license": "MIT", "engines": { @@ -7583,8 +8455,6 @@ }, "node_modules/obug": { "version": "2.1.4", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", - "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", @@ -7597,8 +8467,6 @@ }, "node_modules/on-finished": { "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "dev": true, "license": "MIT", "dependencies": { @@ -7610,8 +8478,6 @@ }, "node_modules/once": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "license": "ISC", "dependencies": { @@ -7620,8 +8486,6 @@ }, "node_modules/onetime": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, "license": "MIT", "dependencies": { @@ -7636,18 +8500,29 @@ }, "node_modules/onetime/node_modules/mimic-fn": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/oniguruma-parser": { + "version": "0.12.2", + "dev": true, + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "4.3.6", + "dev": true, + "license": "MIT", + "dependencies": { + "oniguruma-parser": "^0.12.2", + "regex": "^6.1.0", + "regex-recursion": "^6.0.2" + } + }, "node_modules/open": { "version": "11.0.0", - "resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz", - "integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==", "dev": true, "license": "MIT", "dependencies": { @@ -7667,8 +8542,6 @@ }, "node_modules/optionator": { "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", "dependencies": { @@ -7685,8 +8558,6 @@ }, "node_modules/ora": { "version": "8.2.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", - "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", "dev": true, "license": "MIT", "dependencies": { @@ -7709,8 +8580,6 @@ }, "node_modules/ora/node_modules/ansi-regex": { "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", "engines": { @@ -7722,8 +8591,6 @@ }, "node_modules/ora/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": { @@ -7736,10 +8603,24 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/outdent": { + "version": "0.5.0", + "dev": true, + "license": "MIT" + }, + "node_modules/p-filter": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-map": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/p-limit": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7754,8 +8635,6 @@ }, "node_modules/p-locate": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", "dependencies": { @@ -7768,20 +8647,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/p-try": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/package-manager-detector": { + "version": "0.2.11", + "dev": true, + "license": "MIT", + "dependencies": { + "quansync": "^0.2.7" + } + }, "node_modules/parent-module": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "license": "MIT", "dependencies": { @@ -7793,14 +8684,10 @@ }, "node_modules/parquet-wasm": { "version": "0.7.2", - "resolved": "https://registry.npmjs.org/parquet-wasm/-/parquet-wasm-0.7.2.tgz", - "integrity": "sha512-XfzVFW7REEYzt/MF5pG2fVjF0lyArXkk4x7m4wuE7R5OEoUfXq5wdI5DQi/UsB8wWW3ncx2z6xMjq5dM9qTYgg==", "license": "MIT OR Apache-2.0" }, "node_modules/parse-json": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dev": true, "license": "MIT", "dependencies": { @@ -7818,8 +8705,6 @@ }, "node_modules/parse-ms": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", - "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", "dev": true, "license": "MIT", "engines": { @@ -7829,10 +8714,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "5.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^6.0.1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "6.0.1", + "dev": true, + "license": "MIT" + }, "node_modules/parseurl": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "dev": true, "license": "MIT", "engines": { @@ -7841,15 +8742,11 @@ }, "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-exists": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "license": "MIT", "engines": { @@ -7858,18 +8755,19 @@ }, "node_modules/path-key": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "dev": true, + "license": "MIT" + }, "node_modules/path-to-regexp": { "version": "8.4.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", - "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "dev": true, "license": "MIT", "funding": { @@ -7877,24 +8775,31 @@ "url": "https://opencollective.com/express" } }, + "node_modules/path-type": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/pathe": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "2.1.0", "dev": true, "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { @@ -7904,32 +8809,24 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pkce-challenge": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", - "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "node_modules/pify": { + "version": "4.0.1", "dev": true, "license": "MIT", "engines": { - "node": ">=16.20.0" + "node": ">=6" } }, - "node_modules/pkg-types": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", - "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "node_modules/pkce-challenge": { + "version": "5.0.1", "dev": true, "license": "MIT", - "dependencies": { - "confbox": "^0.2.4", - "exsolve": "^1.0.8", - "pathe": "^2.0.3" + "engines": { + "node": ">=16.20.0" } }, "node_modules/pkg-up": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", - "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", "dev": true, "license": "MIT", "dependencies": { @@ -7941,8 +8838,6 @@ }, "node_modules/pkg-up/node_modules/find-up": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", "dev": true, "license": "MIT", "dependencies": { @@ -7954,8 +8849,6 @@ }, "node_modules/pkg-up/node_modules/locate-path": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", "dev": true, "license": "MIT", "dependencies": { @@ -7968,8 +8861,6 @@ }, "node_modules/pkg-up/node_modules/p-limit": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "license": "MIT", "dependencies": { @@ -7984,8 +8875,6 @@ }, "node_modules/pkg-up/node_modules/p-locate": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7997,8 +8886,6 @@ }, "node_modules/pkg-up/node_modules/path-exists": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", "dev": true, "license": "MIT", "engines": { @@ -8006,13 +8893,13 @@ } }, "node_modules/playwright": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.0.tgz", - "integrity": "sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==", + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", + "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.62.0" + "playwright-core": "1.62.1" }, "bin": { "playwright": "cli.js" @@ -8025,9 +8912,9 @@ } }, "node_modules/playwright-core": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.0.tgz", - "integrity": "sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==", + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -8039,8 +8926,6 @@ }, "node_modules/pngjs": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", - "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", "dev": true, "license": "MIT", "engines": { @@ -8049,8 +8934,6 @@ }, "node_modules/postcss": { "version": "8.5.25", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", - "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", "dev": true, "funding": [ { @@ -8078,8 +8961,6 @@ }, "node_modules/postcss-selector-parser": { "version": "7.1.4", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", - "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", "dev": true, "license": "MIT", "dependencies": { @@ -8092,8 +8973,6 @@ }, "node_modules/powershell-utils": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", - "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", "dev": true, "license": "MIT", "engines": { @@ -8105,8 +8984,6 @@ }, "node_modules/prelude-ls": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", "engines": { @@ -8115,8 +8992,6 @@ }, "node_modules/prettier": { "version": "3.9.6", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", - "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", "dev": true, "license": "MIT", "bin": { @@ -8131,8 +9006,6 @@ }, "node_modules/pretty-ms": { "version": "9.3.0", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", - "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8147,8 +9020,6 @@ }, "node_modules/prompts": { "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -8161,18 +9032,23 @@ }, "node_modules/prompts/node_modules/kleur": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/property-information": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "dev": true, "license": "MIT", "dependencies": { @@ -8183,10 +9059,33 @@ "node": ">= 0.10" } }, + "node_modules/publint": { + "version": "0.3.23", + "dev": true, + "license": "MIT", + "dependencies": { + "@publint/pack": "^0.1.6", + "package-manager-detector": "^1.7.0", + "picocolors": "^1.1.1", + "sade": "^1.8.1" + }, + "bin": { + "publint": "src/cli.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://bjornlu.com/sponsor" + } + }, + "node_modules/publint/node_modules/package-manager-detector": { + "version": "1.8.0", + "dev": true, + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "license": "MIT", "engines": { @@ -8195,8 +9094,6 @@ }, "node_modules/qs": { "version": "6.15.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", - "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -8212,8 +9109,6 @@ }, "node_modules/quansync": { "version": "0.2.11", - "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", - "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", "dev": true, "funding": [ { @@ -8229,8 +9124,6 @@ }, "node_modules/queue-microtask": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true, "funding": [ { @@ -8250,8 +9143,6 @@ }, "node_modules/radix-ui": { "version": "1.6.7", - "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.6.7.tgz", - "integrity": "sha512-QBdhh1arIEUvPC0dQ5+nwWAxt7+N+oP/9jPwjJkGFoSk/sqxg32gJtSXGtFh8frAIcS6oC9cx2Q+7KYCQLOAeA==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.7", @@ -8327,8 +9218,6 @@ }, "node_modules/range-parser": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", - "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", "dev": true, "license": "MIT", "engines": { @@ -8341,8 +9230,6 @@ }, "node_modules/raw-body": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "dev": true, "license": "MIT", "dependencies": { @@ -8357,8 +9244,6 @@ }, "node_modules/react": { "version": "19.2.8", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", - "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8366,8 +9251,6 @@ }, "node_modules/react-dom": { "version": "19.2.8", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", - "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" @@ -8377,14 +9260,12 @@ } }, "node_modules/react-intl": { - "version": "10.1.18", - "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-10.1.18.tgz", - "integrity": "sha512-4JMOYakqVn/IajJt8Ad341Iwd6RDtiP1WQZiHRcFpHDBhVW4E9T0vkypSOk69Adg3dEbIYOE4HKTehH/1sSwnw==", + "version": "10.1.20", "license": "BSD-3-Clause", "dependencies": { - "@formatjs/icu-messageformat-parser": "3.5.15", - "@formatjs/intl": "4.1.17", - "intl-messageformat": "11.2.12" + "@formatjs/icu-messageformat-parser": "3.5.16", + "@formatjs/intl": "4.1.18", + "intl-messageformat": "11.2.13" }, "peerDependencies": { "@types/react": ">=18.0.0", @@ -8393,8 +9274,6 @@ }, "node_modules/react-remove-scroll": { "version": "2.7.2", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", - "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", "license": "MIT", "dependencies": { "react-remove-scroll-bar": "^2.3.7", @@ -8418,8 +9297,6 @@ }, "node_modules/react-remove-scroll-bar": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", - "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", "license": "MIT", "dependencies": { "react-style-singleton": "^2.2.2", @@ -8440,8 +9317,6 @@ }, "node_modules/react-style-singleton": { "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", - "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", "license": "MIT", "dependencies": { "get-nonce": "^1.0.0", @@ -8462,18 +9337,48 @@ }, "node_modules/react-window": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/react-window/-/react-window-2.3.0.tgz", - "integrity": "sha512-FW6TIpaOH646k51X7yE+LSCWGkt5Pfsnc1fVyq/sCI9h0pTqmMiBXM04pzFKg3Bt7NGkeV6kqbU8d/QjmFS7Ug==", "license": "MIT", "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, + "node_modules/read-yaml-file": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.5", + "js-yaml": "^3.6.1", + "pify": "^4.0.1", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/read-yaml-file/node_modules/argparse": { + "version": "1.0.10", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/read-yaml-file/node_modules/js-yaml": { + "version": "3.15.1", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/recast": { "version": "0.23.19", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.19.tgz", - "integrity": "sha512-T98lym7kH+pnZmRaD8yDRdaNqyUbwnbEBx0MuchrzMFOEMray4AO3ZJoTUZ5r78Ao78X/OhzW0DL8GB85w/I2w==", "dev": true, "license": "MIT", "dependencies": { @@ -8487,10 +9392,37 @@ "node": ">= 4" } }, + "node_modules/regex": { + "version": "6.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, "license": "MIT", "engines": { @@ -8499,14 +9431,38 @@ }, "node_modules/reselect": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz", - "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==", "license": "MIT" }, + "node_modules/resolve": { + "version": "1.22.12", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/restore-cursor": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "dev": true, "license": "MIT", "dependencies": { @@ -8522,8 +9478,6 @@ }, "node_modules/restore-cursor/node_modules/onetime": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8538,8 +9492,6 @@ }, "node_modules/reusify": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "license": "MIT", "engines": { @@ -8548,13 +9500,11 @@ } }, "node_modules/rolldown": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", - "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "version": "1.2.2", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.139.0", + "@oxc-project/types": "=0.142.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -8564,27 +9514,24 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.1.5", - "@rolldown/binding-darwin-arm64": "1.1.5", - "@rolldown/binding-darwin-x64": "1.1.5", - "@rolldown/binding-freebsd-x64": "1.1.5", - "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", - "@rolldown/binding-linux-arm64-gnu": "1.1.5", - "@rolldown/binding-linux-arm64-musl": "1.1.5", - "@rolldown/binding-linux-ppc64-gnu": "1.1.5", - "@rolldown/binding-linux-s390x-gnu": "1.1.5", - "@rolldown/binding-linux-x64-gnu": "1.1.5", - "@rolldown/binding-linux-x64-musl": "1.1.5", - "@rolldown/binding-openharmony-arm64": "1.1.5", - "@rolldown/binding-wasm32-wasi": "1.1.5", - "@rolldown/binding-win32-arm64-msvc": "1.1.5", - "@rolldown/binding-win32-x64-msvc": "1.1.5" + "@rolldown/binding-android-arm64": "1.2.2", + "@rolldown/binding-darwin-arm64": "1.2.2", + "@rolldown/binding-darwin-x64": "1.2.2", + "@rolldown/binding-freebsd-x64": "1.2.2", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.2", + "@rolldown/binding-linux-arm64-gnu": "1.2.2", + "@rolldown/binding-linux-arm64-musl": "1.2.2", + "@rolldown/binding-linux-ppc64-gnu": "1.2.2", + "@rolldown/binding-linux-s390x-gnu": "1.2.2", + "@rolldown/binding-linux-x64-gnu": "1.2.2", + "@rolldown/binding-linux-x64-musl": "1.2.2", + "@rolldown/binding-openharmony-arm64": "1.2.2", + "@rolldown/binding-win32-arm64-msvc": "1.2.2", + "@rolldown/binding-win32-x64-msvc": "1.2.2" } }, "node_modules/router": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8600,8 +9547,6 @@ }, "node_modules/run-applescript": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", - "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", "dev": true, "license": "MIT", "engines": { @@ -8613,8 +9558,6 @@ }, "node_modules/run-parallel": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, "funding": [ { @@ -8635,23 +9578,28 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/sade": { + "version": "1.8.1", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true, "license": "MIT" }, "node_modules/scheduler": { "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, "node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -8660,8 +9608,6 @@ }, "node_modules/send": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8687,8 +9633,6 @@ }, "node_modules/serve-static": { "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "dev": true, "license": "MIT", "dependencies": { @@ -8707,15 +9651,11 @@ }, "node_modules/setprototypeof": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "dev": true, "license": "ISC" }, "node_modules/shadcn": { "version": "4.16.1", - "resolved": "https://registry.npmjs.org/shadcn/-/shadcn-4.16.1.tgz", - "integrity": "sha512-XLFzfNNIUPlUlyheFEzj0H4Vnhi9nI0nl3Nfgg8HYXW1FkUVhVT1X+mgmOUW8aWL5SeG0A+yJIV5fm3Hr9MVkQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8761,8 +9701,6 @@ }, "node_modules/shadcn/node_modules/fs-extra": { "version": "11.4.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz", - "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==", "dev": true, "license": "MIT", "dependencies": { @@ -8776,8 +9714,6 @@ }, "node_modules/shadcn/node_modules/jsonfile": { "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -8789,8 +9725,6 @@ }, "node_modules/shadcn/node_modules/universalify": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", "engines": { @@ -8799,8 +9733,6 @@ }, "node_modules/shadcn/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": { @@ -8809,8 +9741,6 @@ }, "node_modules/shebang-command": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", "dependencies": { @@ -8822,18 +9752,32 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/shiki": { + "version": "4.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "4.4.1", + "@shikijs/engine-javascript": "4.4.1", + "@shikijs/engine-oniguruma": "4.4.1", + "@shikijs/langs": "4.4.1", + "@shikijs/themes": "4.4.1", + "@shikijs/types": "4.4.1", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/side-channel": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", - "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8852,8 +9796,6 @@ }, "node_modules/side-channel-list": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "dev": true, "license": "MIT", "dependencies": { @@ -8869,8 +9811,6 @@ }, "node_modules/side-channel-map": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "dev": true, "license": "MIT", "dependencies": { @@ -8888,8 +9828,6 @@ }, "node_modules/side-channel-weakmap": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dev": true, "license": "MIT", "dependencies": { @@ -8908,15 +9846,11 @@ }, "node_modules/siginfo": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true, "license": "ISC" }, "node_modules/signal-exit": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, "license": "ISC", "engines": { @@ -8928,8 +9862,6 @@ }, "node_modules/sirv": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", - "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", "dev": true, "license": "MIT", "dependencies": { @@ -8943,15 +9875,30 @@ }, "node_modules/sisteransi": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "dev": true, "license": "MIT" }, + "node_modules/skin-tone": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-emoji-modifier-base": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/sonner": { "version": "2.0.7", - "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", - "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", "license": "MIT", "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", @@ -8960,8 +9907,6 @@ }, "node_modules/source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -8970,25 +9915,42 @@ }, "node_modules/source-map-js": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/spawndamnit": { + "version": "3.0.1", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "cross-spawn": "^7.0.5", + "signal-exit": "^4.0.1" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/stackback": { "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true, "license": "MIT" }, "node_modules/statuses": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "dev": true, "license": "MIT", "engines": { @@ -8997,15 +9959,11 @@ }, "node_modules/std-env": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", - "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", "dev": true, "license": "MIT" }, "node_modules/stdin-discarder": { "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", - "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", "dev": true, "license": "MIT", "engines": { @@ -9015,10 +9973,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, "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": { @@ -9035,8 +9999,6 @@ }, "node_modules/string-width/node_modules/ansi-regex": { "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", "engines": { @@ -9048,8 +10010,6 @@ }, "node_modules/string-width/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": { @@ -9062,10 +10022,21 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "dev": true, + "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/stringify-object": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-5.0.0.tgz", - "integrity": "sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -9082,8 +10053,6 @@ }, "node_modules/stringify-object/node_modules/is-obj": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-3.0.0.tgz", - "integrity": "sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==", "dev": true, "license": "MIT", "engines": { @@ -9095,8 +10064,6 @@ }, "node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -9108,8 +10075,6 @@ }, "node_modules/strip-bom": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, "license": "MIT", "engines": { @@ -9118,8 +10083,6 @@ }, "node_modules/strip-final-newline": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", - "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", "dev": true, "license": "MIT", "engines": { @@ -9131,8 +10094,6 @@ }, "node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { @@ -9142,10 +10103,34 @@ "node": ">=8" } }, + "node_modules/supports-hyperlinks": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/systeminformation": { "version": "5.33.1", - "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.33.1.tgz", - "integrity": "sha512-DEN6ICHk3Tk0Uf/hrAHh7xlt7iL5CJFBtPZinA0H62DrGG/KPKqq/Nzj6lCXPS4Ay/sf/14zNnk9LpqKzBIc+w==", "dev": true, "license": "MIT", "os": [ @@ -9169,10 +10154,13 @@ "url": "https://www.buymeacoffee.com/systeminfo" } }, + "node_modules/tabbable": { + "version": "6.5.0", + "dev": true, + "license": "MIT" + }, "node_modules/tailwind-merge": { "version": "3.6.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", - "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", "license": "MIT", "funding": { "type": "github", @@ -9181,15 +10169,11 @@ }, "node_modules/tailwindcss": { "version": "4.3.3", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", - "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", "dev": true, "license": "MIT" }, "node_modules/tapable": { "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", - "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "dev": true, "license": "MIT", "engines": { @@ -9200,24 +10184,48 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/term-size": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/tiny-invariant": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "dev": true, "license": "MIT" }, "node_modules/tinybench": { "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", "dev": true, "license": "MIT" }, "node_modules/tinyexec": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", - "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", "dev": true, "license": "MIT", "engines": { @@ -9226,8 +10234,6 @@ }, "node_modules/tinyglobby": { "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { @@ -9243,8 +10249,6 @@ }, "node_modules/tinyrainbow": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", - "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", "dev": true, "license": "MIT", "engines": { @@ -9253,8 +10257,6 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9266,8 +10268,6 @@ }, "node_modules/toidentifier": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true, "license": "MIT", "engines": { @@ -9276,14 +10276,21 @@ }, "node_modules/totalist": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -9299,8 +10306,6 @@ }, "node_modules/ts-morph": { "version": "26.0.0", - "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-26.0.0.tgz", - "integrity": "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==", "dev": true, "license": "MIT", "dependencies": { @@ -9310,8 +10315,6 @@ }, "node_modules/tsconfig-paths": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", - "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", "dev": true, "license": "MIT", "dependencies": { @@ -9325,14 +10328,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==", "license": "0BSD" }, "node_modules/tw-animate-css": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", - "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/Wombosvideo" @@ -9340,8 +10339,6 @@ }, "node_modules/type-check": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", "dependencies": { @@ -9353,8 +10350,6 @@ }, "node_modules/type-is": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", - "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", "dev": true, "license": "MIT", "dependencies": { @@ -9372,8 +10367,6 @@ }, "node_modules/type-is/node_modules/content-type": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", "dev": true, "license": "MIT", "engines": { @@ -9386,8 +10379,6 @@ }, "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": { @@ -9399,16 +10390,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.65.0.tgz", - "integrity": "sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.66.0.tgz", + "integrity": "sha512-QlEbBPz/RuJ1XUHj29nm3t0F/O/cSlEnntozqPOYHnnTGAXFamnMBu5i9Vn6vhUPHGAjR+Vl+5J8vPN/BMUrJw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.65.0", - "@typescript-eslint/parser": "8.65.0", - "@typescript-eslint/typescript-estree": "8.65.0", - "@typescript-eslint/utils": "8.65.0" + "@typescript-eslint/eslint-plugin": "8.66.0", + "@typescript-eslint/parser": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/utils": "8.66.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -9422,17 +10413,8 @@ "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/ufo": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", - "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", - "dev": true, - "license": "MIT" - }, "node_modules/undici": { "version": "7.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", - "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", "dev": true, "license": "MIT", "engines": { @@ -9441,15 +10423,19 @@ }, "node_modules/undici-types": { "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", - "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", "dev": true, "license": "MIT" }, + "node_modules/unicode-emoji-modifier-base": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/unicorn-magic": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", - "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", "dev": true, "license": "MIT", "engines": { @@ -9459,90 +10445,87 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "node_modules/unist-util-is": { + "version": "6.0.1", "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.8" + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/unplugin": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", - "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==", + "node_modules/unist-util-position": { + "version": "5.0.0", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "acorn": "^8.15.0", - "picomatch": "^4.0.3", - "webpack-virtual-modules": "^0.6.2" + "@types/unist": "^3.0.0" }, - "engines": { - "node": ">=18.12.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/unplugin-dts": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/unplugin-dts/-/unplugin-dts-1.0.3.tgz", - "integrity": "sha512-/GR887wfG4r1cWyt1UZsLRuMIjsmEbGkS9yJrz+0dsToHAYUD5CTyP3JMGVLv25j9K0mJcwAVvZno/aTuSUvNg==", + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", "dev": true, "license": "MIT", "dependencies": { - "@rollup/pluginutils": "^5.1.4", - "@volar/typescript": "^2.4.26", - "compare-versions": "^6.1.1", - "debug": "^4.4.0", - "kolorist": "^1.8.0", - "local-pkg": "^1.1.1", - "magic-string": "^0.30.17", - "unplugin": "^2.3.2" - }, - "peerDependencies": { - "@microsoft/api-extractor": ">=7", - "@rspack/core": "^1", - "@vue/language-core": "^3.1.5", - "esbuild": "*", - "rolldown": "*", - "rollup": ">=3", - "typescript": ">=4", - "vite": ">=3", - "webpack": "^4 || ^5" + "@types/unist": "^3.0.0" }, - "peerDependenciesMeta": { - "@microsoft/api-extractor": { - "optional": true - }, - "@rspack/core": { - "optional": true - }, - "@vue/language-core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "rolldown": { - "optional": true - }, - "rollup": { - "optional": true - }, - "vite": { - "optional": true - }, - "webpack": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "dev": true, + "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", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" } }, "node_modules/update-browserslist-db": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -9572,14 +10555,10 @@ }, "node_modules/uplot": { "version": "1.6.32", - "resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.32.tgz", - "integrity": "sha512-KIMVnG68zvu5XXUbC4LQEPnhwOxBuLyW1AHtpm6IKTXImkbLgkMy+jabjLgSLMasNuGGzQm/ep3tOkyTxpiQIw==", "license": "MIT" }, "node_modules/uri-js": { "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -9588,8 +10567,6 @@ }, "node_modules/use-callback-ref": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", - "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", "license": "MIT", "dependencies": { "tslib": "^2.0.0" @@ -9609,8 +10586,6 @@ }, "node_modules/use-sidecar": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", - "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", "license": "MIT", "dependencies": { "detect-node-es": "^1.1.0", @@ -9631,8 +10606,6 @@ }, "node_modules/use-sync-external-store": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -9640,15 +10613,11 @@ }, "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==", "dev": true, "license": "MIT" }, "node_modules/validate-npm-package-name": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", - "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", "dev": true, "license": "ISC", "engines": { @@ -9657,25 +10626,47 @@ }, "node_modules/vary": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" } }, + "node_modules/vfile": { + "version": "6.0.3", + "dev": true, + "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", + "dev": true, + "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.1.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", - "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "version": "8.2.0", "dev": true, "license": "MIT", "dependencies": { - "lightningcss": "^1.32.0", + "lightningcss": "^1.33.0", "picomatch": "^4.0.5", - "postcss": "^8.5.17", - "rolldown": "~1.1.5", + "postcss": "^8.5.23", + "rolldown": "~1.2.0", "tinyglobby": "^0.2.17" }, "bin": { @@ -9692,7 +10683,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.3.0", + "@vitejs/devtools": "^0.4.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -9743,51 +10734,61 @@ } } }, - "node_modules/vite-plugin-dts": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/vite-plugin-dts/-/vite-plugin-dts-5.0.3.tgz", - "integrity": "sha512-gIth6NdCEHWPiiRMCK3N6C8WjvdsrtEQrmsiG8h6Ov+lFP+b07Y+wcs9H0H7n146l0PDTYK4cQN1vgeG1pMdRQ==", + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/vitepress": { + "version": "2.0.0-alpha.19", "dev": true, "license": "MIT", "dependencies": { - "unplugin-dts": "1.0.3" + "@docsearch/css": "^4.7.0", + "@docsearch/js": "^4.7.0", + "@docsearch/sidepanel-js": "^4.7.0", + "@iconify-json/simple-icons": "^1.2.92", + "@shikijs/core": "^4.4.1", + "@shikijs/transformers": "^4.4.1", + "@shikijs/types": "^4.4.1", + "@types/markdown-it": "^14.1.2", + "@vitejs/plugin-vue": "^6.0.8", + "@vue/devtools-api": "^8.2.1", + "@vue/shared": "^3.5.40", + "@vueuse/core": "^14.4.0", + "@vueuse/integrations": "^14.4.0", + "focus-trap": "^8.2.2", + "mark.js": "8.11.1", + "minisearch": "^7.2.0", + "shiki": "^4.4.1", + "vite": "^8.2.0", + "vue": "^3.5.40" + }, + "bin": { + "vitepress": "bin/vitepress.js" }, "peerDependencies": { - "@microsoft/api-extractor": ">=7", - "rollup": ">=3", - "vite": ">=3" + "markdown-it-mathjax3": "^4", + "postcss": "^8" }, "peerDependenciesMeta": { - "@microsoft/api-extractor": { - "optional": true - }, - "rollup": { + "markdown-it-mathjax3": { "optional": true }, - "vite": { + "postcss": { "optional": true } } }, - "node_modules/vite/node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/vitest": { "version": "4.1.10", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", - "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", "dev": true, "license": "MIT", "dependencies": { @@ -9874,24 +10875,28 @@ } } }, - "node_modules/vscode-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", - "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/webpack-virtual-modules": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", - "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "node_modules/vue": { + "version": "3.5.40", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.40", + "@vue/compiler-sfc": "3.5.40", + "@vue/runtime-dom": "3.5.40", + "@vue/server-renderer": "3.5.40", + "@vue/shared": "3.5.40" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } }, "node_modules/which": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "license": "ISC", "dependencies": { @@ -9906,8 +10911,6 @@ }, "node_modules/why-is-node-running": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "license": "MIT", "dependencies": { @@ -9923,25 +10926,53 @@ }, "node_modules/word-wrap": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true, "license": "ISC" }, "node_modules/ws": { "version": "8.21.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", - "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "dev": true, "license": "MIT", "engines": { @@ -9962,8 +10993,6 @@ }, "node_modules/wsl-utils": { "version": "0.3.1", - "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz", - "integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==", "dev": true, "license": "MIT", "dependencies": { @@ -9977,17 +11006,64 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/y18n": { + "version": "5.0.8", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" }, + "node_modules/yargs": { + "version": "16.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", "engines": { @@ -9999,8 +11075,6 @@ }, "node_modules/yocto-spinner": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/yocto-spinner/-/yocto-spinner-1.2.2.tgz", - "integrity": "sha512-DODGl1wJjA/s5pnJFKau9lIYHT81lnhob1i3e1TjxZRxEhWRKl74nTbWE6H5KlkViQQTo/Z29YFdxzTZAMY3ng==", "dev": true, "license": "MIT", "dependencies": { @@ -10015,8 +11089,6 @@ }, "node_modules/yoctocolors": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz", - "integrity": "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==", "dev": true, "license": "MIT", "engines": { @@ -10028,8 +11100,6 @@ }, "node_modules/zod": { "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "dev": true, "license": "MIT", "funding": { @@ -10038,8 +11108,6 @@ }, "node_modules/zod-to-json-schema": { "version": "3.25.2", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", - "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", "dev": true, "license": "ISC", "peerDependencies": { @@ -10048,8 +11116,6 @@ }, "node_modules/zod-validation-error": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", - "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", "dev": true, "license": "MIT", "engines": { @@ -10059,86 +11125,13 @@ "zod": "^3.25.0 || ^4.0.0" } }, - "packages/core": { - "name": "@/core", - "version": "1.0.0", - "extraneous": true, - "dependencies": { - "apache-arrow": "^21.2.0" - } - }, - "packages/platform": { - "name": "@/platform", - "version": "1.0.0", - "extraneous": true, - "dependencies": { - "@/core": "*", - "@zip.js/zip.js": "^2.8.34", - "apache-arrow": "^21.2.0", - "comlink": "^4.4.2", - "fflate": "^0.8.3", - "mediabunny": "^1.51.0", - "parquet-wasm": "^0.7.2" - } - }, - "packages/react": { - "name": "@", - "version": "1.0.0", - "extraneous": true, + "node_modules/zwitch": { + "version": "2.0.4", + "dev": true, "license": "MIT", - "dependencies": { - "@zip.js/zip.js": "^2.8.34", - "apache-arrow": "^21.2.0", - "comlink": "^4.4.2", - "dockview-react": "^7.0.4", - "framer-motion": "^12.43.0", - "lucide-react": "^1.28.0", - "react-intl": "^10.1.18", - "react-window": "^2.3.0", - "sonner": "^2.0.7", - "uplot": "^1.6.32", - "use-sync-external-store": "^1.6.0" - }, - "devDependencies": { - "@types/react": "^19.2.17", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^6.0.4", - "typescript": "6.0.3", - "vite": "^8.1.5" - }, - "engines": { - "node": ">=24.0.0 <25" - }, - "peerDependencies": { - "react": "^19.2.8", - "react-dom": "^19.2.8" - } - }, - "packages/ui": { - "name": "@/ui", - "version": "1.0.0", - "extraneous": true, - "dependencies": { - "@base-ui/react": "^1.6.0", - "@fontsource-variable/geist": "^5.3.0", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "lucide-react": "^1.28.0", - "next-themes": "^0.4.6", - "radix-ui": "^1.6.7", - "sonner": "^2.0.7", - "tailwind-merge": "^3.6.0", - "tw-animate-css": "^1.4.0" - }, - "devDependencies": { - "@tailwindcss/vite": "^4.3.3", - "shadcn": "^4.16.0", - "tailwindcss": "^4.3.3", - "typescript": "6.0.3" - }, - "peerDependencies": { - "react": "^19.2.8", - "react-dom": "^19.2.8" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } } } diff --git a/package.json b/package.json index ad94612..7cb2718 100644 --- a/package.json +++ b/package.json @@ -4,62 +4,76 @@ "version": "1.0.0", "type": "module", "packageManager": "npm@11.7.0", - "engines": { - "node": ">=24.0.0 <25" - }, "scripts": { "dev": "vite", "build": "tsc -p tsconfig.app.json --noEmit && vite build", - "build:lib": "vite build --config vite.lib.config.ts", + "build:lib": "tsc -p tsconfig.lib.json --emitDeclarationOnly --outDir temp/types && vite build --config vite.lib.config.ts && api-extractor run", + "docs:dev": "vitepress dev docs", + "docs:build": "vitepress build docs", + "check:docs-i18n": "node scripts/check-docs-i18n.mjs", + "api:report": "api-extractor run --local", + "api:report:check": "api-extractor run", + "check:package": "npm run api:report:check && publint && attw --pack . --profile esm-only --exclude-entrypoints ./style.css ./index.css && npm run verify:npm-consumer", + "check:bundle-size": "node scripts/check-bundle-size.mjs", + "sbom": "npm sbom --sbom-format cyclonedx > sbom.cdx.json", + "changeset": "changeset", + "version-packages": "changeset version", "verify:npm-consumer": "node scripts/verify-npm-consumer.mjs", - "lint": "eslint .", + "test:npm-consumer:docker": "node tests/npm-consumer/run-docker-test.mjs", + "lint": "eslint . --max-warnings 0", "format": "prettier --write .", - "format:check": "prettier --check \"*.{md,json}\" \"docs/**/*.md\" \".github/**/*.{yml,yaml,md}\" \"src/**/*.{ts,tsx,js,mjs,css,json,md}\" \"vite/**/*.{ts,tsx}\" \"scripts/**/*.{ts,tsx,js,mjs,json}\" \"tests/**/*.{ts,tsx,js,mjs,json}\"", + "format:check": "prettier --check \"*.{md,json}\" \"docs/**/*.md\" \"*.zh-CN.md\" \".github/**/*.{yml,yaml,md}\" \"src/**/*.{ts,tsx,js,mjs,css,json,md}\" \"scripts/**/*.{ts,tsx,js,mjs,json}\" \"tests/**/*.{ts,tsx,js,mjs,json}\"", "fixtures:generate": "node scripts/generate-test-fixtures.mjs", "typecheck": "tsc -p tsconfig.app.json --noEmit && tsc -p tsconfig.lib.json --noEmit", "test": "vitest run", "test:unit": "vitest run", "test:browser": "vitest run --config vitest.browser.config.ts", + "test:browser:smoke": "vitest run --config vitest.browser.smoke.config.ts", "test:compat": "vitest run tests/compat", "test:coverage": "vitest run --coverage", "preview": "vite preview" }, "devDependencies": { + "@arethetypeswrong/cli": "^0.18.5", + "@changesets/cli": "^2.31.1", "@eslint/js": "^10.0.1", - "@playwright/test": "^1.62.0", + "@microsoft/api-extractor": "^7.58.12", + "@playwright/test": "^1.62.1", "@tailwindcss/vite": "^4.3.3", "@types/node": "^26.1.2", - "@types/react": "^19.2.17", - "@types/react-dom": "^19.2.3", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", "@vitejs/plugin-react": "^6.0.4", "@vitest/browser": "^4.1.10", "@vitest/browser-playwright": "^4.1.10", "@vitest/coverage-v8": "^4.1.10", + "axe-core": "^4.12.1", "eslint": "^10.8.0", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.3", - "globals": "^17.8.0", + "globals": "^17.9.0", "lightningcss": "^1.33.0", - "playwright": "^1.62.0", + "playwright": "^1.62.1", "prettier": "^3.6.2", + "publint": "^0.3.23", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "shadcn": "^4.16.0", "tailwindcss": "^4.3.3", "typescript": "6.0.3", - "typescript-eslint": "^8.65.0", + "typescript-eslint": "^8.66.0", "vite": "^8.1.5", - "vite-plugin-dts": "^5.0.3", - "vitest": "^4.1.10", - "react": "^19.2.8", - "react-dom": "^19.2.8", - "shadcn": "^4.16.0" + "vitepress": "2.0.0-alpha.19", + "vitest": "^4.1.10" }, "description": "Browser-based LeRobot dataset visualizer and React embeddable studio", "license": "MIT", "main": "./dist-lib/lerobot.es.js", "module": "./dist-lib/lerobot.es.js", - "types": "./dist-lib/react/index.d.ts", + "types": "./dist-lib/lerobot.d.ts", "exports": { ".": { - "types": "./dist-lib/react/index.d.ts", + "types": "./dist-lib/lerobot.d.ts", "import": "./dist-lib/lerobot.es.js", "default": "./dist-lib/lerobot.es.js" }, @@ -67,11 +81,14 @@ "./index.css": "./dist-lib/lerobot-studio.css" }, "files": [ - "dist-lib" + "dist-lib", + "CHANGELOG.md", + "LICENSE", + "NOTICE", + "README.md" ], "sideEffects": [ - "**/*.css", - "./src/platform/workers/**" + "**/*.css" ], "repository": { "type": "git", @@ -82,14 +99,14 @@ }, "homepage": "https://github.com/ioai-tech/lerobot-studio#readme", "peerDependencies": { - "react": "^19.2.8", - "react-dom": "^19.2.8" + "react": "^19.0.0", + "react-dom": "^19.0.0" }, "publishConfig": { "access": "public" }, "dependencies": { - "@base-ui/react": "^1.6.0", + "@base-ui/react": "^1.7.0", "@fontsource-variable/geist": "^5.3.0", "@zip.js/zip.js": "^2.8.34", "apache-arrow": "^21.2.0", @@ -100,11 +117,11 @@ "fflate": "^0.8.3", "framer-motion": "^12.43.0", "lucide-react": "^1.28.0", - "mediabunny": "^1.51.0", + "mediabunny": "^1.52.3", "next-themes": "^0.4.6", "parquet-wasm": "^0.7.2", "radix-ui": "^1.6.7", - "react-intl": "^10.1.18", + "react-intl": "^10.1.20", "react-window": "^2.3.0", "sonner": "^2.0.7", "tailwind-merge": "^3.6.0", diff --git a/scripts/check-bundle-size.mjs b/scripts/check-bundle-size.mjs new file mode 100644 index 0000000..b339a2c --- /dev/null +++ b/scripts/check-bundle-size.mjs @@ -0,0 +1,283 @@ +import { spawnSync } from 'node:child_process'; +import { readdir, readFile } from 'node:fs/promises'; +import path from 'node:path'; +import { gzipSync } from 'node:zlib'; + +const ROOT = process.cwd(); +const LIB_DIR = path.join(ROOT, 'dist-lib'); +const WEB_DIR = path.join(ROOT, 'dist'); +const scale = Number(process.env.BUNDLE_SIZE_BUDGET_SCALE ?? '1'); + +if (!Number.isFinite(scale) || scale <= 0 || scale > 1) { + console.error('BUNDLE_SIZE_BUDGET_SCALE must be greater than 0 and at most 1.'); + process.exit(2); +} + +// These hard limits are approximately 5% above the measured 2026-08-04 baseline. +// Raising one requires an explicit review of the generated artifacts. +const budgets = { + libEntry: { raw: 330_300, gzip: 107_300 }, + libCss: { raw: 354_200, gzip: 108_500 }, + webInitialJs: { raw: 2_664_000, gzip: 715_000 }, + webInitialCss: { raw: 192_000, gzip: 24_400 }, + libDataLoader: { raw: 272_100, gzip: 56_200 }, + webDataLoader: { raw: 33_500, gzip: 9_300 }, + libDockview: { raw: 670_900, gzip: 158_700 }, + webDockview: { raw: 91_500, gzip: 26_900 }, + libWorkerWasm: { raw: 9_100_000, gzip: 2_940_000 }, + libWorkers: { raw: 461_000, gzip: 106_000 }, + webWorkerWasm: { raw: 6_819_000, gzip: 2_014_000 }, + webWorkers: { raw: 461_000, gzip: 106_000 }, + largestLibJs: { raw: 670_900, gzip: 158_700 }, + largestWebJs: { raw: 1_108_200, gzip: 313_400 }, + npmTarball: { raw: 11_554_000, gzip: 3_444_000 }, +}; + +const failures = []; + +async function walkFiles(directory) { + let entries; + try { + entries = await readdir(directory, { withFileTypes: true }); + } catch (error) { + failures.push(`Missing build output directory: ${path.relative(ROOT, directory)}/`); + return []; + } + + const files = []; + for (const entry of entries) { + const absolute = path.join(directory, entry.name); + if (entry.isDirectory()) { + files.push(...(await walkFiles(absolute))); + } else if (entry.isFile()) { + files.push(absolute); + } + } + return files; +} + +function oneFile(files, pattern, description) { + const matches = files.filter((file) => pattern.test(path.basename(file))); + if (matches.length !== 1) { + failures.push( + `Expected exactly one ${description}; found ${matches.length}: ${ + matches.map((file) => path.relative(ROOT, file)).join(', ') || 'none' + }`, + ); + } + return matches; +} + +async function measureFiles(files) { + let raw = 0; + let gzip = 0; + for (const file of files) { + try { + const contents = await readFile(file); + raw += contents.byteLength; + gzip += gzipSync(contents, { level: 9 }).byteLength; + } catch (error) { + failures.push(`Cannot read artifact ${path.relative(ROOT, file)}: ${error.message}`); + } + } + return { raw, gzip }; +} + +function budgetFor(key) { + return { + raw: Math.floor(budgets[key].raw * scale), + gzip: Math.floor(budgets[key].gzip * scale), + }; +} + +function formatBytes(bytes) { + if (!Number.isFinite(bytes)) return '-'; + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 ** 2) return `${(bytes / 1024).toFixed(1)} KiB`; + return `${(bytes / 1024 ** 2).toFixed(2)} MiB`; +} + +const rows = []; + +async function checkGroup(label, key, files) { + const actual = await measureFiles(files); + const limit = budgetFor(key); + rows.push({ label, actual, limit }); + for (const encoding of ['raw', 'gzip']) { + if (actual[encoding] > limit[encoding]) { + failures.push( + `${label} ${encoding} is ${formatBytes(actual[encoding])}; limit is ${formatBytes(limit[encoding])}.`, + ); + } + } +} + +async function checkLargestChunk(label, key, files) { + const measured = await Promise.all( + files.map(async (file) => ({ file, ...(await measureFiles([file])) })), + ); + const largestRaw = measured.toSorted((a, b) => b.raw - a.raw)[0] ?? { raw: 0, gzip: 0 }; + const largestGzip = measured.toSorted((a, b) => b.gzip - a.gzip)[0] ?? { + raw: 0, + gzip: 0, + }; + const actual = { raw: largestRaw.raw, gzip: largestGzip.gzip }; + const limit = budgetFor(key); + rows.push({ label, actual, limit }); + + for (const item of measured) { + if (item.raw > limit.raw || item.gzip > limit.gzip) { + failures.push( + `Unexpected huge chunk ${path.relative(ROOT, item.file)} is ${formatBytes(item.raw)} raw / ${formatBytes(item.gzip)} gzip; per-chunk limit is ${formatBytes(limit.raw)} raw / ${formatBytes(limit.gzip)} gzip.`, + ); + } + } +} + +function htmlAssetPaths(html, relation) { + const matches = []; + const tagPattern = relation === 'script' ? /]*>/gi : /]*>/gi; + for (const tag of html.match(tagPattern) ?? []) { + const rel = tag.match(/\brel=["']([^"']+)["']/i)?.[1]; + const attribute = relation === 'script' ? 'src' : 'href'; + if ( + relation !== 'script' && + !rel + ?.split(/\s+/) + .map((value) => value.toLowerCase()) + .includes(relation) + ) { + continue; + } + const value = tag.match(new RegExp(`\\b${attribute}=["']([^"']+)["']`, 'i'))?.[1]; + if (value) matches.push(value); + } + return matches; +} + +function resolveWebAssets(references, description) { + if (references.length === 0) + failures.push(`No ${description} references found in dist/index.html.`); + return references.map((reference) => path.join(WEB_DIR, reference.replace(/^\/+/, ''))); +} + +function readPackMetadata() { + const result = spawnSync('npm', ['pack', '--dry-run', '--json'], { + cwd: ROOT, + encoding: 'utf8', + }); + if (result.status !== 0) { + failures.push(`npm pack --dry-run failed:\n${result.stderr || result.stdout}`); + return null; + } + try { + const metadata = JSON.parse(result.stdout); + if (!Array.isArray(metadata) || metadata.length !== 1) throw new Error('unexpected result'); + return metadata[0]; + } catch (error) { + failures.push(`Could not parse npm pack metadata: ${error.message}`); + return null; + } +} + +const [libFiles, webFiles] = await Promise.all([walkFiles(LIB_DIR), walkFiles(WEB_DIR)]); +const libEntry = oneFile(libFiles, /^lerobot\.es\.js$/, 'stable library entry'); +const libCss = oneFile(libFiles, /^lerobot-studio\.css$/, 'library CSS artifact'); +const libDataLoader = oneFile( + libFiles, + /^LeRobotDataLoader-[\w-]+\.js$/, + 'library DataLoader chunk', +); +const libDockview = oneFile(libFiles, /^DockviewLayout-[\w-]+\.js$/, 'library Dockview chunk'); +const libWorkers = libFiles.filter((file) => /\.worker-[\w-]+\.js$/i.test(path.basename(file))); +const libWorkerWasm = libFiles.filter( + (file) => /wasm/i.test(path.basename(file)) && !libWorkers.includes(file), +); + +if (libWorkerWasm.length === 0) failures.push('No library Worker/WASM artifacts found.'); +if (libWorkers.length !== 2) + failures.push(`Expected two library Workers; found ${libWorkers.length}.`); + +let webInitialJs = []; +let webInitialCss = []; +try { + const html = await readFile(path.join(WEB_DIR, 'index.html'), 'utf8'); + webInitialJs = resolveWebAssets( + [...htmlAssetPaths(html, 'script'), ...htmlAssetPaths(html, 'modulepreload')], + 'initial JS', + ); + webInitialCss = resolveWebAssets(htmlAssetPaths(html, 'stylesheet'), 'initial CSS'); +} catch (error) { + failures.push(`Missing or unreadable dist/index.html: ${error.message}`); +} + +const webDataLoader = oneFile(webFiles, /^LeRobotDataLoader-[\w-]+\.js$/, 'web DataLoader chunk'); +const webDockview = oneFile(webFiles, /^DockviewLayout-[\w-]+\.js$/, 'web Dockview chunk'); +const webWorkers = webFiles.filter((file) => /\.worker-[\w-]+\.js$/i.test(path.basename(file))); +const webWorkerWasm = webFiles.filter( + (file) => /wasm/i.test(path.basename(file)) && !webWorkers.includes(file), +); + +if (webWorkerWasm.length === 0) failures.push('No web Worker/WASM artifacts found.'); +if (webWorkers.length !== 2) failures.push(`Expected two web Workers; found ${webWorkers.length}.`); + +await checkGroup('Library stable entry', 'libEntry', libEntry); +await checkGroup('Library CSS', 'libCss', libCss); +await checkGroup('Web initial synchronous JS', 'webInitialJs', webInitialJs); +await checkGroup('Web initial CSS', 'webInitialCss', webInitialCss); +await checkGroup('Library lazy DataLoader', 'libDataLoader', libDataLoader); +await checkGroup('Web lazy DataLoader', 'webDataLoader', webDataLoader); +await checkGroup('Library lazy Dockview', 'libDockview', libDockview); +await checkGroup('Web lazy Dockview', 'webDockview', webDockview); +await checkGroup('Library Worker/WASM', 'libWorkerWasm', libWorkerWasm); +await checkGroup('Library external Workers', 'libWorkers', libWorkers); +await checkGroup('Web Worker/WASM', 'webWorkerWasm', webWorkerWasm); +await checkGroup('Web external Workers', 'webWorkers', webWorkers); +await checkLargestChunk( + 'Largest library JS chunk', + 'largestLibJs', + libFiles.filter((file) => file.endsWith('.js') && !libWorkerWasm.includes(file)), +); +await checkLargestChunk( + 'Largest web JS chunk', + 'largestWebJs', + webFiles.filter((file) => file.endsWith('.js') && !webWorkerWasm.includes(file)), +); + +const pack = readPackMetadata(); +if (pack) { + const actual = { raw: pack.unpackedSize, gzip: pack.size }; + const limit = budgetFor('npmTarball'); + rows.push({ label: 'npm tarball (unpacked / .tgz)', actual, limit }); + if (actual.raw > limit.raw) { + failures.push( + `npm tarball unpacked size is ${formatBytes(actual.raw)}; limit is ${formatBytes(limit.raw)}.`, + ); + } + if (actual.gzip > limit.gzip) { + failures.push( + `npm tarball .tgz size is ${formatBytes(actual.gzip)}; limit is ${formatBytes(limit.gzip)}.`, + ); + } +} + +const labelWidth = Math.max(...rows.map((row) => row.label.length), 24); +console.log('\nBundle size report (raw and gzip):'); +console.log( + `${'Artifact'.padEnd(labelWidth)} ${'Raw / limit'.padEnd(25)} ${'Gzip / limit'.padEnd(25)}`, +); +for (const row of rows) { + const raw = `${formatBytes(row.actual.raw)} / ${formatBytes(row.limit.raw)}`; + const gzip = `${formatBytes(row.actual.gzip)} / ${formatBytes(row.limit.gzip)}`; + console.log(`${row.label.padEnd(labelWidth)} ${raw.padEnd(25)} ${gzip.padEnd(25)}`); +} + +console.log('\nNote: library Workers are external and share one lazy package-local WASM payload.'); + +if (failures.length > 0) { + console.error(`\nBundle size gate failed with ${failures.length} problem(s):`); + for (const failure of failures) console.error(`- ${failure}`); + process.exit(1); +} + +console.log('\nBundle size gate passed.'); diff --git a/scripts/check-docs-i18n.mjs b/scripts/check-docs-i18n.mjs new file mode 100644 index 0000000..93d4b7b --- /dev/null +++ b/scripts/check-docs-i18n.mjs @@ -0,0 +1,115 @@ +#!/usr/bin/env node +/** + * Ensures every repository-owned English Markdown file has a `.zh-CN.md` pair + * or an explicit entry in scripts/docs-i18n-exceptions.json. + */ +import { readFileSync, existsSync } from 'node:fs'; +import { readdirSync, statSync } from 'node:fs'; +import { join, relative, sep } from 'node:path'; + +const repoRoot = join(import.meta.dirname, '..'); +const exceptionsPath = join(repoRoot, 'scripts/docs-i18n-exceptions.json'); + +const SKIP_DIRS = new Set([ + 'node_modules', + 'dist', + 'dist-lib', + '.git', + '.vitepress', + 'coverage', + 'temp', +]); + +/** @type {{ exceptions: Array<{ path: string; zhEntry?: string | null }>; skipScanDirs?: Array<{ dir: string }> }} */ +const manifest = JSON.parse(readFileSync(exceptionsPath, 'utf8')); +const { exceptions, skipScanDirs = [] } = manifest; +const exceptionByPath = new Map(exceptions.map((entry) => [entry.path, entry])); +const skipScanDirNames = new Set(skipScanDirs.map((entry) => entry.dir)); + +/** @param {string} dir */ +function walkMarkdown(dir) { + /** @type {string[]} */ + const files = []; + for (const name of readdirSync(dir)) { + const full = join(dir, name); + const rel = relative(repoRoot, full).split(sep).join('/'); + if (statSync(full).isDirectory()) { + if (SKIP_DIRS.has(name)) continue; + files.push(...walkMarkdown(full)); + continue; + } + if (name.endsWith('.md')) { + files.push(rel); + } + } + return files; +} + +/** @param {string} relPath */ +function zhPairPath(relPath) { + return relPath.replace(/\.md$/, '.zh-CN.md'); +} + +/** @param {string} relPath */ +function isUnderSkipScanDir(relPath) { + const top = relPath.split('/')[0]; + return skipScanDirNames.has(top); +} + +/** @type {string[]} */ +const errors = []; +const markdownFiles = walkMarkdown(repoRoot); + +for (const entry of exceptions) { + if (isUnderSkipScanDir(entry.path)) { + errors.push( + `Exception for ${entry.path} is redundant: paths under skipScanDirs are not scanned; remove the exception or stop skipping the directory.`, + ); + continue; + } + if (!markdownFiles.includes(entry.path)) { + errors.push(`Exception references missing Markdown file: ${entry.path}`); + } + if (entry.zhEntry && !existsSync(join(repoRoot, entry.zhEntry))) { + errors.push(`Exception for ${entry.path} references missing zhEntry: ${entry.zhEntry}`); + } +} + +for (const relPath of markdownFiles.sort()) { + if (relPath.endsWith('.zh-CN.md')) { + continue; + } + + const exception = exceptionByPath.get(relPath); + if (exception) { + continue; + } + + const pair = zhPairPath(relPath); + if (!existsSync(join(repoRoot, pair))) { + errors.push(`Missing Chinese pair: expected ${pair} for ${relPath}`); + } +} + +for (const relPath of markdownFiles.filter((file) => file.endsWith('.zh-CN.md'))) { + const source = relPath.replace(/\.zh-CN\.md$/, '.md'); + if (!existsSync(join(repoRoot, source)) && !exceptionByPath.has(source)) { + errors.push(`Orphan Chinese doc without English source: ${relPath}`); + } +} + +if (errors.length > 0) { + console.error('Documentation i18n check failed:\n'); + for (const error of errors) { + console.error(` - ${error}`); + } + console.error( + `\nAdd a sibling \`.zh-CN.md\` file or register an exception in scripts/docs-i18n-exceptions.json.`, + ); + process.exit(1); +} + +const englishCount = markdownFiles.filter((f) => !f.endsWith('.zh-CN.md')).length; +console.log( + `Documentation i18n check passed (${englishCount} English files, ${exceptions.length} exceptions, ${skipScanDirs.length} skip-scan dirs).`, +); diff --git a/scripts/docs-i18n-exceptions.json b/scripts/docs-i18n-exceptions.json new file mode 100644 index 0000000..91ab08a --- /dev/null +++ b/scripts/docs-i18n-exceptions.json @@ -0,0 +1,18 @@ +{ + "exceptions": [ + { + "path": "etc/lerobot-studio.api.md", + "reason": "api-extractor-generated", + "zhEntry": "docs/api.zh-CN.md", + "noteEn": "Machine-generated API Extractor report. Do not translate by hand; regenerate with `npm run api:report`. Chinese documentation wraps this report in docs/api.zh-CN.md.", + "noteZh": "由 API Extractor 自动生成的 API 报告,请勿手工翻译;使用 `npm run api:report` 重新生成。中文说明见 docs/api.zh-CN.md,正文仍引用本文件的英文报告。" + } + ], + "skipScanDirs": [ + { + "dir": "temp", + "reasonEn": "Ephemeral build output (for example local API Extractor copies). Not repository documentation; excluded from Markdown pairing scans.", + "reasonZh": "临时构建输出(例如本地 API Extractor 副本),不属于仓库文档,不参与 Markdown 配对扫描。" + } + ] +} diff --git a/scripts/generate-test-fixtures.mjs b/scripts/generate-test-fixtures.mjs index 3721b6c..d6e7c0e 100644 --- a/scripts/generate-test-fixtures.mjs +++ b/scripts/generate-test-fixtures.mjs @@ -11,7 +11,16 @@ import { promises as fs } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { spawnSync } from 'node:child_process'; -import { tableToIPC, vectorFromArray, Binary, Table } from 'apache-arrow'; +import { + tableToIPC, + vectorFromArray, + Binary, + Field, + Float64, + Int64, + List, + Table, +} from 'apache-arrow'; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const outRoot = path.join(root, 'tests/fixtures/datasets'); @@ -22,6 +31,34 @@ const FPS = 10; const FRAMES_PER_EP = 3; const EPISODE_DURATION_SEC = FRAMES_PER_EP / FPS; +function solidVisualEpisodeStats(rgb, count = FRAMES_PER_EP) { + const shaped = rgb.map((value) => [[value]]); + return { + [`stats/${VIDEO_KEY}/min`]: shaped, + [`stats/${VIDEO_KEY}/max`]: shaped, + [`stats/${VIDEO_KEY}/mean`]: shaped, + [`stats/${VIDEO_KEY}/std`]: rgb.map(() => [[0]]), + [`stats/${VIDEO_KEY}/q01`]: shaped, + [`stats/${VIDEO_KEY}/q10`]: shaped, + [`stats/${VIDEO_KEY}/q50`]: shaped, + [`stats/${VIDEO_KEY}/q90`]: shaped, + [`stats/${VIDEO_KEY}/q99`]: shaped, + [`stats/${VIDEO_KEY}/count`]: [count], + }; +} + +function solidVisualStatsObject(rgb, count = FRAMES_PER_EP) { + const prefix = `stats/${VIDEO_KEY}/`; + return { + [VIDEO_KEY]: Object.fromEntries( + Object.entries(solidVisualEpisodeStats(rgb, count)).map(([key, value]) => [ + key.slice(prefix.length), + value, + ]), + ), + }; +} + /** Minimal 64×64 PNG — Chromium's AVC encoder rejects tiny frames (e.g. 2×2). */ const TINY_PNG = Buffer.from( 'iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAAlC+aJAAAAeElEQVR4nO3PQQkAMAzAwIqdfwuriD2OQSACLjPn/p0XNKAFDWhBA1rQgBY0oAUNaEEDWtCAFjSgBQ1oQQNa0IAWNKAFDWhBA1rQgBY0oAUNaEEDWtCAFjSgBQ1oQQNa0IAWNKAFDWhBA1rQgBY0oAUNaEEDWvDWAp47MUuty3rOAAAAAElFTkSuQmCC', @@ -68,12 +105,45 @@ async function writeParquetFromColumns(filePath, columns) { Array.isArray(values) && values.length > 0 && values.every((v) => v instanceof Uint8Array || v instanceof Buffer); - vectors[name] = isBinary - ? vectorFromArray( - values.map((v) => (v instanceof Uint8Array ? v : new Uint8Array(v))), - new Binary(), - ) - : vectorFromArray(values); + if (isBinary) { + vectors[name] = vectorFromArray( + values.map((v) => (v instanceof Uint8Array ? v : new Uint8Array(v))), + new Binary(), + ); + continue; + } + const sample = values.find((value) => value != null); + if (Array.isArray(sample)) { + const leaves = []; + const collect = (value) => + Array.isArray(value) + ? value.forEach(collect) + : value == null + ? undefined + : leaves.push(value); + values.forEach(collect); + const integers = leaves.every( + (value) => typeof value === 'number' && Number.isInteger(value), + ); + let type = integers ? new Int64() : new Float64(); + let current = sample; + while (Array.isArray(current)) { + type = new List(new Field('item', type, true)); + current = current[0]; + } + const normalized = integers + ? values.map(function normalize(value) { + return Array.isArray(value) + ? value.map(normalize) + : value == null + ? null + : BigInt(value); + }) + : values; + vectors[name] = vectorFromArray(normalized, type); + continue; + } + vectors[name] = vectorFromArray(values); } const table = new Table(vectors); const ipc = tableToIPC(table, 'stream'); @@ -168,6 +238,17 @@ async function createV2(mp4Bytes) { JSON.stringify({ episode_index: 1, length: FRAMES_PER_EP, tasks: ['pick cube'] }), ].join('\n') + '\n', ); + await writeText( + path.join(base, 'meta/episodes_stats.jsonl'), + [0, 1] + .map((episode_index) => + JSON.stringify({ + episode_index, + stats: solidVisualStatsObject([0, 0, 1]), + }), + ) + .join('\n') + '\n', + ); await writeText( path.join(base, 'meta/tasks.jsonl'), `${JSON.stringify({ task_index: 0, task: 'pick cube' })}\n`, @@ -222,6 +303,12 @@ async function createV3(mp4Bytes) { [`videos/${VIDEO_KEY}/file_index`]: [0, 1], [`videos/${VIDEO_KEY}/from_timestamp`]: [0, 0], [`videos/${VIDEO_KEY}/to_timestamp`]: [EPISODE_DURATION_SEC, EPISODE_DURATION_SEC], + ...Object.fromEntries( + Object.entries(solidVisualEpisodeStats([0, 0, 1])).map(([key, value]) => [ + key, + [value, value], + ]), + ), }); await writeParquetFromColumns(path.join(base, 'meta/tasks.parquet'), { @@ -277,6 +364,17 @@ async function createV2Image() { JSON.stringify({ episode_index: 1, length: FRAMES_PER_EP, tasks: ['pick cube'] }), ].join('\n') + '\n', ); + await writeText( + path.join(base, 'meta/episodes_stats.jsonl'), + [0, 1] + .map((episode_index) => + JSON.stringify({ + episode_index, + stats: solidVisualStatsObject([0, 100 / 255, 1]), + }), + ) + .join('\n') + '\n', + ); await writeText( path.join(base, 'meta/tasks.jsonl'), `${JSON.stringify({ task_index: 0, task: 'pick cube' })}\n`, diff --git a/scripts/verify-npm-consumer.mjs b/scripts/verify-npm-consumer.mjs index 48d07ef..0216fec 100644 --- a/scripts/verify-npm-consumer.mjs +++ b/scripts/verify-npm-consumer.mjs @@ -1,41 +1,192 @@ import { execFile } from 'node:child_process'; -import { mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { createServer } from 'node:http'; +import { cp, mkdtemp, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import { promisify } from 'node:util'; +import { chromium } from 'playwright'; const execFileAsync = promisify(execFile); const root = path.resolve(import.meta.dirname, '..'); const packageDir = root; const tempDir = await mkdtemp(path.join(os.tmpdir(), 'lerobot-studio-consumer-')); +const runNext = process.env.VERIFY_NEXT_EXAMPLE !== '0'; + +async function run(command, args, options = {}) { + try { + return await execFileAsync(command, args, { + maxBuffer: 20 * 1024 * 1024, + ...options, + }); + } catch (error) { + const details = [error.stdout, error.stderr].filter(Boolean).join('\n'); + throw new Error(`${command} ${args.join(' ')} failed\n${details}`, { cause: error }); + } +} + +async function collectFiles(directory) { + const results = []; + for (const entry of await readdir(directory, { withFileTypes: true })) { + const filePath = path.join(directory, entry.name); + if (entry.isDirectory()) results.push(...(await collectFiles(filePath))); + else results.push(filePath); + } + return results; +} + +function runtimeExportsFromApiReport(report) { + return Array.from( + report.matchAll(/^export (?:abstract )?(?:class|const|enum|function|namespace|var) ([\w$]+)/gm), + (match) => match[1], + ).sort(); +} + +async function serve(directory, mountPath = '/') { + const contentTypes = { + '.css': 'text/css', + '.html': 'text/html', + '.js': 'text/javascript', + '.map': 'application/json', + '.svg': 'image/svg+xml', + '.wasm': 'application/wasm', + }; + const server = createServer(async (request, response) => { + try { + const urlPath = decodeURIComponent(new URL(request.url ?? '/', 'http://localhost').pathname); + const normalizedMount = `/${mountPath.replace(/^\/+|\/+$/g, '')}/`; + if (!urlPath.startsWith(normalizedMount)) throw new Error('Outside mount path'); + const mountedPath = urlPath.slice(normalizedMount.length); + const relativePath = mountedPath === '' ? 'index.html' : mountedPath; + const filePath = path.resolve(directory, relativePath); + if (!filePath.startsWith(`${path.resolve(directory)}${path.sep}`)) { + response.writeHead(403).end('Forbidden'); + return; + } + if (!(await stat(filePath)).isFile()) throw new Error('Not a file'); + response.setHeader( + 'content-type', + contentTypes[path.extname(filePath)] ?? 'application/octet-stream', + ); + response.end(await readFile(filePath)); + } catch { + response.writeHead(404).end('Not found'); + } + }); + await new Promise((resolve, reject) => { + server.once('error', reject); + server.listen(0, '127.0.0.1', resolve); + }); + const address = server.address(); + if (!address || typeof address === 'string') throw new Error('Failed to start consumer server'); + return { + url: `http://127.0.0.1:${address.port}`, + close: () => + new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ), + }; +} try { - const { stdout } = await execFileAsync('npm', ['pack', '--json', '--pack-destination', tempDir], { + const { stdout } = await run('npm', ['pack', '--json', '--pack-destination', tempDir], { cwd: packageDir, }); - const [{ filename }] = JSON.parse(stdout); + const [{ filename, files }] = JSON.parse(stdout); const tarball = path.join(tempDir, filename); + const packedPaths = new Set(files.map((file) => file.path)); + const requiredPaths = [ + 'package.json', + 'README.md', + 'LICENSE', + 'NOTICE', + 'CHANGELOG.md', + 'dist-lib/lerobot.es.js', + 'dist-lib/lerobot-studio.css', + 'dist-lib/lerobot.d.ts', + ]; + + for (const requiredPath of requiredPaths) { + if (!packedPaths.has(requiredPath)) { + throw new Error(`npm package is missing required file: ${requiredPath}`); + } + } + const workerPaths = files.filter((file) => + /^dist-lib\/assets\/.*\.worker-.*\.js$/.test(file.path), + ); + if (workerPaths.length !== 2) { + throw new Error( + `npm package must contain two external Worker assets; found ${workerPaths.length}`, + ); + } + const lazyWasmPaths = files.filter((file) => /^dist-lib\/wasmUrl-.*\.js$/.test(file.path)); + if (lazyWasmPaths.length !== 1) { + throw new Error(`npm package must contain one lazy WASM chunk; found ${lazyWasmPaths.length}`); + } + + const forbiddenPath = files.find( + (file) => + file.path.startsWith('src/') || + file.path.startsWith('tests/') || + file.path.startsWith('.github/'), + ); + if (forbiddenPath) { + throw new Error(`npm package contains repository-only file: ${forbiddenPath.path}`); + } + + const packedJavaScript = ( + await Promise.all( + files + .filter((file) => file.path.endsWith('.js')) + .map((file) => readFile(path.join(packageDir, file.path), 'utf8')), + ) + ).join('\n'); + if ( + /new Worker\s*\(\s*["'`]\/assets\//i.test(packedJavaScript) || + /["'`]\/assets\/[^"'`]*(?:worker|wasm)[^"'`]*["'`]/i.test(packedJavaScript) + ) { + throw new Error('npm package contains an absolute /assets worker or WASM URL'); + } + const packedWorkerJavaScript = ( + await Promise.all( + files + .filter((file) => /worker/i.test(file.path) && file.path.endsWith('.js')) + .map((file) => readFile(path.join(packageDir, file.path), 'utf8')), + ) + ).join('\n'); + if (/data:application\/wasm/i.test(packedWorkerJavaScript)) { + throw new Error('npm package Worker artifacts contain an inline WASM payload'); + } await writeFile( path.join(tempDir, 'package.json'), JSON.stringify({ private: true, type: 'module' }, null, 2), ); - await execFileAsync( - 'npm', - ['install', '--ignore-scripts', tarball, 'typescript', '@types/react', '@types/react-dom'], - { cwd: tempDir, maxBuffer: 10 * 1024 * 1024 }, - ); + const installPackages = [ + tarball, + 'typescript', + 'react@^19', + 'react-dom@^19', + '@types/react@^19', + '@types/react-dom@^19', + 'vite', + '@vitejs/plugin-react', + ]; + if (runNext) installPackages.push('next'); + await run('npm', ['install', '--ignore-scripts', '--no-audit', '--no-fund', ...installPackages], { + cwd: tempDir, + }); await writeFile( path.join(tempDir, 'index.ts'), [ - "import { DirectoryDataSource, LeRobotStudioProvider } from '@ioai/lerobot-studio';", - "import type { DataSource, EpisodeMetadata } from '@ioai/lerobot-studio';", + "import { LeRobotViewer, createArchiveDataSourceFromFile, createArchiveDataSourceFromUrl } from '@ioai/lerobot-studio';", + "import type { DataSource, LeRobotViewerProps } from '@ioai/lerobot-studio';", 'const source: DataSource | null = null;', - 'const episodes: EpisodeMetadata[] = [];', + "const props: LeRobotViewerProps = { dataSource: 'https://example.com/dataset.zip' };", 'void source;', - 'void episodes;', - 'void DirectoryDataSource;', - 'void LeRobotStudioProvider;', + 'void props;', + 'void LeRobotViewer;', + 'void createArchiveDataSourceFromFile;', + 'void createArchiveDataSourceFromUrl;', '', ].join('\n'), ); @@ -57,8 +208,135 @@ try { 2, ), ); - await execFileAsync('npx', ['tsc', '--project', 'tsconfig.json'], { cwd: tempDir }); - console.log('npm consumer typecheck passed'); + await run(path.join(tempDir, 'node_modules/.bin/tsc'), ['--project', 'tsconfig.json'], { + cwd: tempDir, + }); + const apiReport = await readFile(path.join(root, 'etc/lerobot-studio.api.md'), 'utf8'); + const expectedRuntimeExports = runtimeExportsFromApiReport(apiReport); + await writeFile( + path.join(tempDir, 'runtime.mjs'), + [ + "const module = await import('@ioai/lerobot-studio');", + `const expected = ${JSON.stringify(expectedRuntimeExports)};`, + 'const actual = Object.keys(module).sort();', + 'if (JSON.stringify(actual) !== JSON.stringify(expected)) {', + " throw new Error(`Runtime exports (${actual.join(', ')}) differ from API report values (${expected.join(', ')})`);", + '}', + '', + ].join('\n'), + ); + await run(process.execPath, ['runtime.mjs'], { cwd: tempDir }); + + const viteExample = path.join(tempDir, 'vite-react'); + await cp(path.join(root, 'examples/vite-react'), viteExample, { recursive: true }); + await run(path.join(tempDir, 'node_modules/.bin/tsc'), ['--project', 'tsconfig.json'], { + cwd: viteExample, + }); + await run(path.join(tempDir, 'node_modules/.bin/vite'), ['build'], { cwd: viteExample }); + + const builtAssets = await collectFiles(path.join(viteExample, 'dist')); + const builtJavaScript = ( + await Promise.all( + builtAssets.filter((file) => file.endsWith('.js')).map((file) => readFile(file, 'utf8')), + ) + ).join('\n'); + if ( + /new Worker\s*\(\s*["'`]\/assets\//i.test(builtJavaScript) || + /["'`]\/assets\/[^"'`]*(?:worker|wasm)[^"'`]*["'`]/i.test(builtJavaScript) + ) { + throw new Error('Vite consumer build regressed to an absolute /assets worker or WASM URL'); + } + + const consumerMount = '/arbitrary/root/npm-consumer/'; + const staticServer = await serve(path.join(viteExample, 'dist'), consumerMount); + const browser = await chromium.launch({ headless: true }); + try { + const page = await browser.newPage(); + const badResponses = []; + const requestFailures = []; + const requestedResources = []; + const pageErrors = []; + page.on('response', (response) => { + requestedResources.push(`${response.status()} ${response.url()}`); + if (response.status() === 404) badResponses.push(response.url()); + }); + page.on('requestfailed', (request) => { + requestFailures.push(`${request.url()}: ${request.failure()?.errorText ?? 'failed'}`); + }); + page.on('pageerror', (error) => pageErrors.push(error.message)); + await page.goto(`${staticServer.url}${consumerMount}`, { waitUntil: 'networkidle' }); + await page.locator('[data-npm-consumer-mounted="true"]').waitFor(); + try { + await page.waitForFunction( + () => + window.__LEROBOT_CONSUMER__?.infoRead === true && + window.__LEROBOT_CONSUMER__?.wasmFetched === true, + ); + } catch (error) { + const marker = await page.evaluate(() => window.__LEROBOT_CONSUMER__); + throw new Error( + `Chromium Worker/WASM readiness timed out: ${JSON.stringify({ + marker, + badResponses, + requestFailures, + pageErrors, + requestedResources, + builtAssets: builtAssets.map((file) => + path.relative(path.join(viteExample, 'dist'), file), + ), + })}`, + { cause: error }, + ); + } + const browserResult = await page.evaluate(() => { + const root = document.querySelector('.lerobot-root'); + const cssLoaded = Array.from(document.styleSheets).some((sheet) => { + try { + return (sheet.cssRules?.length ?? 0) > 0; + } catch { + return false; + } + }); + return { + cssLoaded, + hasViewerRoot: Boolean(root), + marker: window.__LEROBOT_CONSUMER__, + }; + }); + if (!browserResult.hasViewerRoot) throw new Error('Chromium did not mount LeRobotViewer'); + if (!browserResult.cssLoaded) throw new Error('Chromium loaded no consumer CSS rules'); + if (!browserResult.marker?.workerConstructed) { + throw new Error('Chromium did not construct the library Worker'); + } + if (!browserResult.marker?.wasmFetched) { + throw new Error('Chromium did not fetch the library WASM asset'); + } + if (badResponses.length || requestFailures.length) { + throw new Error( + `Chromium resource failures: ${[...badResponses, ...requestFailures].join(', ')}`, + ); + } + } finally { + await browser.close(); + await staticServer.close(); + } + + if (runNext) { + const nextExample = path.join(tempDir, 'next-app-router'); + await cp(path.join(root, 'examples/next-app-router'), nextExample, { recursive: true }); + await run(process.execPath, [path.join(tempDir, 'node_modules/next/dist/bin/next'), 'build'], { + cwd: nextExample, + env: { ...process.env, NEXT_TELEMETRY_DISABLED: '1' }, + }); + } else { + console.warn('Next App Router build skipped because VERIFY_NEXT_EXAMPLE=0'); + } + + console.log( + `npm tarball passed package, API, type, Vite build, Chromium, CSS, Worker/WASM, and ${ + runNext ? 'Next build' : 'Next static-only' + } checks (${files.length} packed files)`, + ); } finally { await rm(tempDir, { recursive: true, force: true }); } diff --git a/vite/cssScopePlugin.ts b/scripts/vite/cssScopePlugin.ts similarity index 100% rename from vite/cssScopePlugin.ts rename to scripts/vite/cssScopePlugin.ts diff --git a/src/core/analysis/SplitService.ts b/src/core/analysis/SplitService.ts index f593536..d97d5e6 100644 --- a/src/core/analysis/SplitService.ts +++ b/src/core/analysis/SplitService.ts @@ -25,13 +25,28 @@ export interface SplitConfig { /** Map split name -> episode indices (0-based in export order). */ export type SplitsIndices = Record; -/** Serialize splits to LeRobot v3 info.splits format: each value is comma-separated indices. */ +/** + * Serialize splits to the official LeRobot v3 half-open `start:end` format. + * A split must be one non-empty contiguous range in export order. + */ export function splitsIndicesToInfoSplits(splits: SplitsIndices): Record { const out: Record = {}; for (const [name, indices] of Object.entries(splits)) { - if (indices.length > 0) { - out[name] = indices.join(','); + if (indices.length === 0) continue; + for (let position = 0; position < indices.length; position++) { + const index = indices[position]; + if (!Number.isSafeInteger(index) || index < 0) { + throw new Error( + `Cannot export split "${name}": episode index ${String(index)} is not a non-negative safe integer.`, + ); + } + if (position > 0 && index !== indices[position - 1] + 1) { + throw new Error( + `Cannot export split "${name}" in LeRobot v3 start:end format: episode indices are not contiguous in export order. Reorder episodes by split or use range splits.`, + ); + } } + out[name] = `${indices[0]}:${indices[indices.length - 1] + 1}`; } return out; } diff --git a/src/core/analysis/StatsService.ts b/src/core/analysis/StatsService.ts index 83de02b..060e3b7 100644 --- a/src/core/analysis/StatsService.ts +++ b/src/core/analysis/StatsService.ts @@ -1,174 +1,508 @@ import type { Table } from 'apache-arrow'; -import type { LeRobotInfo, EpisodeMetadata, LeRobotFeature } from '../types/lerobot'; +import type { EpisodeMetadata, LeRobotFeature, LeRobotInfo } from '../types/lerobot'; -/** Duck-typed loader surface used for export/stats aggregation. */ export interface StatsDataLoader { getEpisodeTableForExport(episodeIndex: number): Promise<{ table: Table }>; } -/** Per-feature stats: arrays have one value per dimension (or length 1 for scalar). */ +/** Recursive JSON array; visual stats have shape [channels][1][1]. */ +export type StatsArray = Array; + export interface FeatureStats { - min: number[]; - max: number[]; - mean: number[]; - std: number[]; - count: number; + min: StatsArray; + max: StatsArray; + mean: StatsArray; + std: StatsArray; + q01: StatsArray; + q10: StatsArray; + q50: StatsArray; + q90: StatsArray; + q99: StatsArray; + count: number[]; } export type DatasetStats = Record; +export type NumericFeatureRow = number | readonly number[]; -function isNumericDtype(dtype: string | undefined): boolean { - if (!dtype) return false; - const d = dtype.toLowerCase(); - return d.includes('float') || d.includes('int') || d === 'int64' || d === 'float32'; +export interface NumericStatsRowContext { + episode: EpisodeMetadata; + outputEpisodeIndex: number; + rowIndex: number; + outputGlobalIndex: number; + sourceValues: readonly number[]; } -/** Extract numeric feature keys from info that exist in the table schema. */ -function getNumericFeatureKeys(info: LeRobotInfo, table: Table): string[] { - const keys: string[] = []; - for (const [key, feature] of Object.entries(info.features || {})) { - if (!feature || !isNumericDtype(feature.dtype)) continue; - if (!table.schema.fields.some((f) => f.name === key)) continue; - keys.push(key); - } - return keys; +export interface ComputeDatasetStatsOptions { + onProgress?: (current: number, total: number) => void; + signal?: AbortSignal; + /** + * Return the final exported value for a numeric feature, or undefined to use + * the source value. This is the integration point for rewritten index, + * episode_index, and task_index columns. + */ + resolveNumericRow?: ( + featureKey: string, + context: NumericStatsRowContext, + ) => NumericFeatureRow | undefined; + /** Supply unflattened per-episode stats if EpisodeMetadata does not contain stats/* fields. */ + getEpisodeStats?: ( + episode: EpisodeMetadata, + ) => + | Record> + | undefined + | Promise> | undefined>; +} + +const STAT_KEYS = ['min', 'max', 'mean', 'std', 'q01', 'q10', 'q50', 'q90', 'q99'] as const; +const QUANTILES = [ + ['q01', 0.01], + ['q10', 0.1], + ['q50', 0.5], + ['q90', 0.9], + ['q99', 0.99], +] as const; +const NUM_BINS = 5000; + +function isNumeric(feature: LeRobotFeature): boolean { + const dtype = feature.dtype.toLowerCase(); + return ( + dtype.includes('float') || dtype.includes('int') || dtype.includes('uint') || dtype === 'bool' + ); +} + +function isVisual(feature: LeRobotFeature): boolean { + const dtype = feature.dtype.toLowerCase(); + return ( + dtype === 'image' || + dtype === 'video' || + dtype === 'depth' || + feature.info?.is_depth_map === true + ); +} + +function featureDim(feature: LeRobotFeature): number { + return feature.shape.length > 0 ? feature.shape[0] : 1; } -/** Get dimension for a feature (number of scalar values per row). */ -function getFeatureDim(feature: LeRobotFeature): number { - const shape = feature.shape; - if (shape && shape.length > 0 && typeof shape[0] === 'number') return shape[0]; +function visualChannels(feature: LeRobotFeature): number { + const names = feature.names?.map((name) => name.toLowerCase()) ?? []; + const namedChannel = names.findIndex((name) => name === 'channel' || name === 'channels'); + if (namedChannel >= 0 && feature.shape[namedChannel] != null) return feature.shape[namedChannel]; + if (feature.shape.length >= 3) { + if (feature.shape[0] === 1 || feature.shape[0] === 3) return feature.shape[0]; + if (feature.shape.at(-1) === 1 || feature.shape.at(-1) === 3) return feature.shape.at(-1)!; + } return 1; } -/** Read a numeric value from Arrow (scalar or list) as number[]. */ -function getNumericRow( +function numericRow( vector: ReturnType, row: number, dim: number, ): number[] | null { - const val = vector?.get(row); - if (val === undefined || val === null) return null; - if (typeof val === 'number' && !Number.isNaN(val)) return [val]; - if (typeof val === 'bigint') return [Number(val)]; - if (Array.isArray(val)) { - const out = val.slice(0, dim).map((v) => (typeof v === 'bigint' ? Number(v) : Number(v))); - if (out.some((v) => Number.isNaN(v))) return null; - return out; - } - const obj = val as { toArray?: () => number[]; length?: number }; - if (typeof obj.toArray === 'function') { - const arr = obj.toArray().slice(0, dim); - if (arr.some((v) => Number.isNaN(v))) return null; - return arr; - } - return null; -} - -/** Running stats accumulator per dimension (Welford-friendly: n, mean, m2, min, max). */ -function createAccum(dim: number): { - n: number; - mean: number[]; - m2: number[]; - min: number[]; - max: number[]; -} { - return { - n: 0, - mean: new Array(dim).fill(0), - m2: new Array(dim).fill(0), - min: new Array(dim).fill(Number.POSITIVE_INFINITY), - max: new Array(dim).fill(Number.NEGATIVE_INFINITY), + const value = vector?.get(row); + if (typeof value === 'number') return Number.isFinite(value) && dim === 1 ? [value] : null; + if (typeof value === 'bigint') { + const converted = Number(value); + if (!Number.isSafeInteger(converted)) { + throw new RangeError(`Numeric feature contains unsafe int64 value: ${value}`); + } + return dim === 1 ? [converted] : null; + } + let values: unknown[] | undefined; + if (Array.isArray(value)) values = value; + else if (value && typeof (value as { toArray?: () => unknown[] }).toArray === 'function') { + values = Array.from((value as { toArray: () => unknown[] }).toArray()); + } + if (!values || values.length !== dim) return null; + const converted = values.map((item) => Number(item)); + if ( + converted.some( + (item, index) => + !Number.isFinite(item) || + (typeof values?.[index] === 'bigint' && !Number.isSafeInteger(item)), + ) + ) { + return null; + } + return converted; +} + +function plainArray(value: unknown): StatsArray | null { + if (typeof value === 'number') return Number.isFinite(value) ? [value] : null; + if (ArrayBuffer.isView(value) && !(value instanceof DataView)) { + return plainArray(Array.from(value as unknown as Iterable)); + } + if (!Array.isArray(value)) { + const arrayLike = value as { toArray?: () => unknown }; + return typeof arrayLike?.toArray === 'function' ? plainArray(arrayLike.toArray()) : null; + } + const output: StatsArray = []; + for (const item of value) { + if (typeof item === 'number') { + if (!Number.isFinite(item)) return null; + output.push(item); + } else { + const nested = plainArray(item); + if (!nested) return null; + output.push(nested); + } + } + return output; +} + +function shapeOf(value: StatsArray): number[] { + const shape: number[] = []; + let current: number | StatsArray = value; + while (Array.isArray(current)) { + shape.push(current.length); + if (current.length === 0) break; + const childShapes = current + .filter(Array.isArray) + .map((child) => JSON.stringify(shapeOf(child as StatsArray))); + if ( + current.some((child) => Array.isArray(child)) !== + current.every((child) => Array.isArray(child)) || + new Set(childShapes).size > 1 + ) { + throw new Error('Episode stats contain a ragged array'); + } + current = current[0]; + } + return shape; +} + +function flatten(value: StatsArray): number[] { + const result: number[] = []; + const visit = (item: number | StatsArray): void => { + if (typeof item === 'number') result.push(item); + else item.forEach(visit); }; + visit(value); + return result; } -function updateAccum(acc: ReturnType, values: number[]): void { - const dim = values.length; - acc.n += 1; - const n = acc.n; - for (let d = 0; d < dim; d++) { - const x = values[d]; - if (Number.isNaN(x) || !Number.isFinite(x)) continue; - const delta = x - acc.mean[d]; - acc.mean[d] += delta / n; - acc.m2[d] += delta * (x - acc.mean[d]); - if (x < acc.min[d]) acc.min[d] = x; - if (x > acc.max[d]) acc.max[d] = x; - } -} - -function toFeatureStats(acc: ReturnType, dim: number): FeatureStats { - const mean = acc.mean.slice(0, dim); - const std = new Array(dim).fill(0); - for (let d = 0; d < dim; d++) { - if (acc.n > 0) { - const variance = acc.m2[d] / acc.n; - std[d] = Math.sqrt(variance); +function reshape(values: readonly number[], shape: readonly number[]): StatsArray { + let offset = 0; + const build = (depth: number): StatsArray => + Array.from({ length: shape[depth] }, () => + depth === shape.length - 1 ? values[offset++] : build(depth + 1), + ); + return build(0); +} + +function validateStats( + raw: Record, + featureKey: string, + expectedShape?: readonly number[], +): FeatureStats { + const count = plainArray(raw.count); + if (!count || count.length !== 1 || typeof count[0] !== 'number' || count[0] <= 0) { + throw new Error( + `Cannot compute training-ready stats: invalid count for feature "${featureKey}"`, + ); + } + const result = { count: [count[0]] } as FeatureStats; + let shape: number[] | undefined; + for (const key of STAT_KEYS) { + const value = plainArray(raw[key]); + if (!value) { + throw new Error( + `Cannot compute training-ready stats: missing or invalid "${key}" for feature "${featureKey}"`, + ); } + const valueShape = shapeOf(value); + shape ??= valueShape; + if (JSON.stringify(shape) !== JSON.stringify(valueShape)) { + throw new Error( + `Cannot compute training-ready stats: inconsistent shape for "${featureKey}"`, + ); + } + result[key] = value; + } + if (expectedShape && JSON.stringify(shape) !== JSON.stringify(expectedShape)) { + throw new Error( + `Cannot compute training-ready stats: feature "${featureKey}" has stats shape ` + + `[${shape?.join(',')}] but official shape is [${expectedShape.join(',')}]`, + ); + } + return result; +} + +function flattenedEpisodeStats( + episode: EpisodeMetadata, + featureKey: string, +): Record | undefined { + const nested = (episode as Record).stats as + Record> | undefined; + if (nested?.[featureKey]) return nested[featureKey]; + const prefix = `stats/${featureKey}/`; + const result: Record = {}; + for (const [key, value] of Object.entries(episode)) { + if (key.startsWith(prefix)) result[key.slice(prefix.length)] = value; } - const min = acc.min.slice(0, dim).map((v) => (v === Number.POSITIVE_INFINITY ? 0 : v)); - const max = acc.max.slice(0, dim).map((v) => (v === Number.NEGATIVE_INFINITY ? 0 : v)); - return { min, max, mean, std, count: acc.n }; + return Object.keys(result).length > 0 ? result : undefined; +} + +function aggregateFeature(items: readonly FeatureStats[], featureKey: string): FeatureStats { + if (items.length === 0) throw new Error(`No episode stats for feature "${featureKey}"`); + const shape = shapeOf(items[0].mean); + const width = flatten(items[0].mean).length; + const counts = items.map((item) => item.count[0]); + const total = counts.reduce((sum, value) => sum + value, 0); + const values = items.map((item) => { + const output = {} as Record<(typeof STAT_KEYS)[number], number[]>; + for (const key of STAT_KEYS) { + if (JSON.stringify(shapeOf(item[key])) !== JSON.stringify(shape)) { + throw new Error(`Incompatible episode stats shape for feature "${featureKey}"`); + } + output[key] = flatten(item[key]); + } + return output; + }); + const mean = Array.from( + { length: width }, + (_, dim) => + values.reduce((sum, value, index) => sum + value.mean[dim] * counts[index], 0) / total, + ); + const variance = Array.from( + { length: width }, + (_, dim) => + values.reduce((sum, value, index) => { + const delta = value.mean[dim] - mean[dim]; + return sum + (value.std[dim] ** 2 + delta ** 2) * counts[index]; + }, 0) / total, + ); + const result = { + min: reshape( + Array.from({ length: width }, (_, dim) => Math.min(...values.map((value) => value.min[dim]))), + shape, + ), + max: reshape( + Array.from({ length: width }, (_, dim) => Math.max(...values.map((value) => value.max[dim]))), + shape, + ), + mean: reshape(mean, shape), + std: reshape( + variance.map((value) => Math.sqrt(Math.max(0, value))), + shape, + ), + count: [total], + } as FeatureStats; + for (const [key] of QUANTILES) { + result[key] = reshape( + Array.from( + { length: width }, + (_, dim) => + values.reduce((sum, value, index) => sum + value[key][dim] * counts[index], 0) / total, + ), + shape, + ); + } + return result; +} + +function histogramQuantile( + histogram: Uint32Array, + low: number, + high: number, + target: number, +): number { + let cumulative = 0; + let before = 0; + let index = 0; + for (; index < histogram.length; index++) { + before = cumulative; + cumulative += histogram[index]; + if (cumulative >= target) break; + } + if (index === 0) return low; + if (index >= histogram.length) return high; + const countInBin = cumulative - before; + const fraction = countInBin === 0 ? 0 : (target - before) / countInBin; + return low + (index + fraction) * ((high - low) / histogram.length); +} + +function computeNumericEpisode( + count: number, + dim: number, + readRow: (row: number) => number[], +): FeatureStats { + const min = new Array(dim).fill(Infinity); + const max = new Array(dim).fill(-Infinity); + const sum = new Array(dim).fill(0); + const squares = new Array(dim).fill(0); + for (let row = 0; row < count; row++) { + const values = readRow(row); + for (let d = 0; d < dim; d++) { + const value = values[d]; + min[d] = Math.min(min[d], value); + max[d] = Math.max(max[d], value); + sum[d] += value; + squares[d] += value * value; + } + } + const mean = sum.map((value) => value / count); + const std = squares.map((value, d) => Math.sqrt(Math.max(0, value / count - mean[d] * mean[d]))); + const result = { min, max, mean, std, count: [count] } as FeatureStats; + if (count < 2) { + for (const [key] of QUANTILES) result[key] = [...mean]; + return result; + } + const low = min.map((value, d) => + value === max[d] ? value - 1e-10 : value - (max[d] - value) * 1e-10, + ); + const high = max.map((value, d) => + value === min[d] ? value + 1e-10 : value + (value - min[d]) * 1e-10, + ); + const histograms = Array.from({ length: dim }, () => new Uint32Array(NUM_BINS)); + for (let row = 0; row < count; row++) { + const values = readRow(row); + for (let d = 0; d < dim; d++) { + const scaled = ((values[d] - low[d]) / (high[d] - low[d])) * NUM_BINS; + const index = Math.max(0, Math.min(NUM_BINS - 1, Math.floor(scaled))); + histograms[d][index]++; + } + } + for (const [key, quantile] of QUANTILES) { + result[key] = histograms.map((histogram, d) => + histogramQuantile(histogram, low[d], high[d], quantile * count), + ); + } + return result; +} + +/** Aggregate persisted episode stats with official count-weighted semantics. */ +export function aggregateEpisodeStats( + statsByEpisode: readonly Record>[], +): DatasetStats { + const result: DatasetStats = {}; + const featureKeys = new Set(statsByEpisode.flatMap((stats) => Object.keys(stats))); + for (const featureKey of featureKeys) { + result[featureKey] = aggregateFeature( + statsByEpisode + .map((stats) => stats[featureKey]) + .filter((stats): stats is FeatureStats | Record => stats !== undefined) + .map((stats) => validateStats(stats as Record, featureKey)), + featureKey, + ); + } + return result; } /** - * Compute global numeric feature statistics (min, max, mean, std) over all episodes. - * Streams episode tables and aggregates; reports progress and respects AbortSignal. + * Compute v0.6.1 training stats. Numeric data uses one bounded 5000-bin + * histogram per feature/episode; visual data is aggregated from persisted + * episode stats so media sampling is not silently changed. */ export async function computeDatasetStats( dataLoader: StatsDataLoader, info: LeRobotInfo, episodes: EpisodeMetadata[], - options?: { onProgress?: (current: number, total: number) => void; signal?: AbortSignal }, + options?: ComputeDatasetStatsOptions, ): Promise { - const result: DatasetStats = {}; - const numericKeys = new Set(); - const dims: Record = {}; - const accums: Record> = {}; - const total = episodes.length; - const progressInterval = Math.max(1, Math.floor(total / 50)); + if (episodes.length === 0) { + throw new Error('Cannot compute training-ready stats for a dataset with no episodes'); + } + const numericKeys = Object.entries(info.features) + .filter(([, feature]) => isNumeric(feature) && !isVisual(feature)) + .map(([key]) => key); + const visualKeys = Object.entries(info.features) + .filter(([, feature]) => isVisual(feature)) + .map(([key]) => key); + if (numericKeys.length === 0 && visualKeys.length === 0) { + throw new Error('Cannot compute training-ready stats: no trainable features are available'); + } - for (let i = 0; i < episodes.length; i++) { + const perFeature = new Map(); + let globalRow = 0; + for (let episodePosition = 0; episodePosition < episodes.length; episodePosition++) { if (options?.signal?.aborted) { - const err = new Error('Export cancelled'); - err.name = 'AbortError'; - throw err; + const error = new Error('Export cancelled'); + error.name = 'AbortError'; + throw error; } - if (i > 0 && i % progressInterval === 0) { - options?.onProgress?.(i, total); - } - - const ep = episodes[i]; - const { table } = await dataLoader.getEpisodeTableForExport(ep.episode_index); - - if (i === 0) { - for (const key of getNumericFeatureKeys(info, table)) { - numericKeys.add(key); - const feat = info.features[key]; - const dim = feat ? getFeatureDim(feat) : 1; - dims[key] = dim; - accums[key] = createAccum(dim); + const episode = episodes[episodePosition]; + const supplied = await options?.getEpisodeStats?.(episode); + for (const featureKey of visualKeys) { + const feature = info.features[featureKey]; + const raw = supplied?.[featureKey] ?? flattenedEpisodeStats(episode, featureKey); + if (!raw) { + throw new Error( + `Cannot compute training-ready stats: selected episode ${episode.episode_index} ` + + `is missing required visual stats for feature "${featureKey}"`, + ); } + const channels = visualChannels(feature); + const stats = validateStats(raw as Record, featureKey, [channels, 1, 1]); + const list = perFeature.get(featureKey) ?? []; + list.push(stats); + perFeature.set(featureKey, list); } - for (const key of numericKeys) { - const vector = table.getChild(key); - if (!vector) continue; - const dim = dims[key] ?? 1; - const accum = accums[key]; - for (let row = 0; row < table.numRows; row++) { - const values = getNumericRow(vector, row, dim); - if (values) updateAccum(accum, values); + if (numericKeys.length > 0) { + const { table } = await dataLoader.getEpisodeTableForExport(episode.episode_index); + if (table.numRows === 0) { + throw new Error( + `Cannot compute training-ready stats: episode ${episode.episode_index} is empty`, + ); + } + if (table.numRows !== episode.length) { + throw new Error( + `Cannot compute training-ready stats: episode ${episode.episode_index} row count ` + + `${table.numRows} does not match metadata length ${episode.length}`, + ); } + for (const featureKey of numericKeys) { + const vector = table.getChild(featureKey); + if (!vector) { + throw new Error( + `Cannot compute training-ready stats: episode ${episode.episode_index} ` + + `is missing feature "${featureKey}"`, + ); + } + const dim = featureDim(info.features[featureKey]); + const readRow = (row: number): number[] => { + const sourceValues = numericRow(vector, row, dim); + if (!sourceValues) { + throw new Error( + `Cannot compute training-ready stats: feature "${featureKey}" has an invalid row`, + ); + } + const resolved = options?.resolveNumericRow?.(featureKey, { + episode, + outputEpisodeIndex: episodePosition, + rowIndex: row, + outputGlobalIndex: globalRow + row, + sourceValues, + }); + const values = + resolved === undefined + ? sourceValues + : typeof resolved === 'number' + ? [resolved] + : Array.from(resolved); + if (values.length !== dim || values.some((value) => !Number.isFinite(value))) { + throw new Error( + `Cannot compute training-ready stats: final value for "${featureKey}" is invalid`, + ); + } + return values; + }; + const list = perFeature.get(featureKey) ?? []; + list.push(computeNumericEpisode(table.numRows, dim, readRow)); + perFeature.set(featureKey, list); + } + globalRow += table.numRows; + } else { + globalRow += episode.length; } + options?.onProgress?.(episodePosition + 1, episodes.length); } - options?.onProgress?.(total, total); - - for (const key of numericKeys) { - const dim = dims[key] ?? 1; - result[key] = toFeatureStats(accums[key], dim); + const result: DatasetStats = {}; + for (const [featureKey, values] of perFeature) { + result[featureKey] = aggregateFeature(values, featureKey); } - return result; } diff --git a/src/core/datasource/types.ts b/src/core/datasource/types.ts index 1c1a527..ff5d823 100644 --- a/src/core/datasource/types.ts +++ b/src/core/datasource/types.ts @@ -1,19 +1,32 @@ +/** A phase reported while a data source reads or prepares content. */ export type LoadingPhase = 'download' | 'index' | 'gunzip' | 'read'; +/** Progress reported by a data-source operation. */ export interface ProgressInfo { + /** Current operation phase. */ phase: LoadingPhase; + /** Bytes or logical units processed so far. */ loaded?: number; + /** Total bytes or logical units when known. */ total?: number; + /** Optional human-readable detail. */ message?: string; } +/** Receives data-source progress updates. */ export type ProgressHandler = (info: ProgressInfo) => void; +/** Browser-readable storage abstraction consumed by the viewer. */ export interface DataSource { + /** Return whether a logical dataset path exists. */ exists(path: string): Promise; + /** Read a UTF-8 text file. */ readText(path: string, onProgress?: ProgressHandler): Promise; + /** Read a file as bytes. */ readBytes(path: string, onProgress?: ProgressHandler): Promise; + /** Return an object URL or remote URL suitable for browser media APIs. */ getObjectUrl(path: string, mimeType?: string, onProgress?: ProgressHandler): Promise; + /** Release object URLs, handles, caches, and other source-owned resources. */ clear(): void | Promise; /** * Lists logical dataset paths when the backing source has an index. diff --git a/src/core/export/types.ts b/src/core/export/types.ts index aeaa212..91a1498 100644 --- a/src/core/export/types.ts +++ b/src/core/export/types.ts @@ -35,7 +35,21 @@ export interface ExportOptions { splitsConfig?: SplitConfig; } -/** Per-episode video offsets for v3 (one file per chunk/key, segment by from_timestamp/to_timestamp). */ +export interface V3DataEpisodeLocation { + chunk_index: number; + file_index: number; + dataset_from_index: number; + dataset_to_index: number; +} + +/** Actual v3 data shards produced by the exporter, keyed by source episode index. */ +export interface V3DataLayout { + episodes: Map; + total_chunks: number; + total_files: number; +} + +/** Per-episode video offsets for v3, keyed by source episode index and video feature. */ export type EpisodeVideoOffsets = Map< number, Record< diff --git a/src/core/index.ts b/src/core/index.ts index 239ccca..f14a519 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -21,12 +21,22 @@ export type { ExportOptions, TargetVersion, EpisodeVideoOffsets, + V3DataEpisodeLocation, + V3DataLayout, } from './export/types'; export { computeSplits, splitsIndicesToInfoSplits } from './analysis/SplitService'; export type { SplitStrategy, SplitConfig, SplitsIndices } from './analysis/SplitService'; -export type { FeatureStats, DatasetStats, StatsDataLoader } from './analysis/StatsService'; -export { computeDatasetStats } from './analysis/StatsService'; +export type { + FeatureStats, + DatasetStats, + StatsArray, + StatsDataLoader, + NumericFeatureRow, + NumericStatsRowContext, + ComputeDatasetStatsOptions, +} from './analysis/StatsService'; +export { aggregateEpisodeStats, computeDatasetStats } from './analysis/StatsService'; export { getEagerEpisodeColumns, diff --git a/src/core/types/lerobot.ts b/src/core/types/lerobot.ts index 0fdd01b..7a13b14 100644 --- a/src/core/types/lerobot.ts +++ b/src/core/types/lerobot.ts @@ -9,6 +9,7 @@ export interface LeRobotFeature { info?: { [key: string]: string | number | boolean | null; }; + [key: string]: unknown; } /** @@ -16,6 +17,7 @@ export interface LeRobotFeature { */ export interface BaseLeRobotInfo { codebase_version: string; + /** Some official metadata writes null; validators accept it at runtime. */ robot_type: string; total_episodes: number; total_frames: number; @@ -24,15 +26,17 @@ export interface BaseLeRobotInfo { total_videos?: number; fps: number; data_path: string; - video_path: string; + video_path: string | null; + tools?: unknown[] | null; features: Record; + [key: string]: unknown; } /** * Specialized Info for v2.x */ export interface LeRobotInfoV2 extends BaseLeRobotInfo { - codebase_version: 'v2.0' | 'v2.1'; + codebase_version: 'v2.1'; chunks_size: number; } @@ -55,6 +59,7 @@ export type LeRobotInfo = LeRobotInfoV2 | LeRobotInfoV3; export interface BaseEpisodeMetadata { episode_index: number; length: number; + [key: string]: unknown; } /** @@ -76,7 +81,7 @@ export interface EpisodeMetadataV3 extends BaseEpisodeMetadata { chunk_index?: number; file_index?: number; // Specific video keys like observation.images.up - [key: string]: string | number | string[] | undefined; + [key: string]: unknown; } export type EpisodeMetadata = EpisodeMetadataV2 | EpisodeMetadataV3; @@ -102,11 +107,11 @@ export type PlaybackMode = 'loop' | 'sequential' | 'shuffle'; * Type Guards */ export function isV3Info(info: LeRobotInfo): info is LeRobotInfoV3 { - return info.codebase_version.startsWith('v3'); + return info.codebase_version.trim().toLowerCase() === 'v3.0'; } export function isV2Info(info: LeRobotInfo): info is LeRobotInfoV2 { - return info.codebase_version.startsWith('v2'); + return info.codebase_version.trim().toLowerCase() === 'v2.1'; } export function isV3Metadata(meta: EpisodeMetadata): meta is EpisodeMetadataV3 { diff --git a/src/core/versioning/LeRobotVersionAdapter.ts b/src/core/versioning/LeRobotVersionAdapter.ts index 33b5bdf..da464cd 100644 --- a/src/core/versioning/LeRobotVersionAdapter.ts +++ b/src/core/versioning/LeRobotVersionAdapter.ts @@ -8,7 +8,7 @@ import type { import { normalizeTaskDisplay } from './arrowUtils'; /** - * Abstract base for LeRobot codebase version adapters (v2.0, v2.1, v3.0, etc.). + * Abstract base for LeRobot codebase version adapters. * Subclasses must implement version and the four methods. Path resolution and * metadata loading are delegated to the adapter so DataLoader, panels, and * export do not branch on version. diff --git a/src/core/versioning/arrowUtils.ts b/src/core/versioning/arrowUtils.ts index 38e0e85..af9f89a 100644 --- a/src/core/versioning/arrowUtils.ts +++ b/src/core/versioning/arrowUtils.ts @@ -19,7 +19,15 @@ function tryParseJsonString(str: string): unknown { export function convertArrowValue(val: unknown): unknown { if (val === null || val === undefined) return val; - if (typeof val === 'bigint') return Number(val); + if (typeof val === 'bigint') { + const converted = Number(val); + if (!Number.isSafeInteger(converted)) { + throw new RangeError( + `Parquet int64 value is outside JavaScript's safe integer range: ${val}`, + ); + } + return converted; + } if (typeof val === 'string') return tryParseJsonString(val); if (typeof val !== 'object') return val; diff --git a/src/core/versioning/index.ts b/src/core/versioning/index.ts index 8a05df9..f4668ea 100644 --- a/src/core/versioning/index.ts +++ b/src/core/versioning/index.ts @@ -14,9 +14,17 @@ export { createReport, BaseLeRobotValidator } from './validation'; export { getAdapterForVersion, getValidatorForVersion, + isSupportedLeRobotVersion, + normalizeLeRobotVersion, v2Adapter, v3Adapter, } from './versionRegistry'; +export { classifyLeRobotVersion } from './versionCapability'; +export type { + LeRobotVersionCapability, + LeRobotVersionCapabilityStatus, + SupportedLeRobotVersion, +} from './versionCapability'; export { V2Adapter } from './v2Adapter'; export { V3Adapter } from './v3Adapter'; export { V2FormatValidator } from './v2FormatValidator'; diff --git a/src/core/versioning/v2Adapter.ts b/src/core/versioning/v2Adapter.ts index 62aadfa..93854ab 100644 --- a/src/core/versioning/v2Adapter.ts +++ b/src/core/versioning/v2Adapter.ts @@ -40,7 +40,7 @@ function normalizeEpisodeTasks(tasks: unknown): string[] { } /** - * Adapter for LeRobot codebase v2.0 / v2.1. + * Adapter for LeRobot codebase v2.1 and newer read-only v2 compatibility. * - Episodes: meta/episodes.jsonl * - Tasks: meta/tasks.jsonl * - Data: one Parquet file per episode, data/chunk-XXX/episode_YYYYYY.parquet, full file = one episode @@ -109,6 +109,29 @@ export class V2Adapter extends LeRobotVersionAdapter { episodes.forEach((ep) => { ep.tasks = normalizeEpisodeTasks(ep.tasks); }); + try { + const statsText = await dataSource.readText('meta/episodes_stats.jsonl'); + const statsByEpisode = new Map>(); + for (const line of statsText.split('\n').filter((item) => item.trim())) { + const row = JSON.parse(line) as { + episode_index: number; + stats?: Record>; + }; + const flattened: Record = {}; + for (const [featureKey, featureStats] of Object.entries(row.stats ?? {})) { + for (const [statKey, value] of Object.entries(featureStats)) { + flattened[`stats/${featureKey}/${statKey}`] = value; + } + } + statsByEpisode.set(row.episode_index, flattened); + } + episodes.forEach((episode) => { + Object.assign(episode, statsByEpisode.get(episode.episode_index)); + }); + } catch { + // v2.1 permits datasets without legacy per-episode stats. Training + // export will reject missing visual stats explicitly when required. + } return episodes; } catch (e) { console.warn('Failed to load episodes.jsonl', e); diff --git a/src/core/versioning/v2FormatValidator.ts b/src/core/versioning/v2FormatValidator.ts index 011c3e5..189a256 100644 --- a/src/core/versioning/v2FormatValidator.ts +++ b/src/core/versioning/v2FormatValidator.ts @@ -2,13 +2,14 @@ import type { LeRobotInfo } from '../types/lerobot'; import type { DataSource } from '../datasource/types'; import type { MetadataLoadingHelpers } from './types'; import { BaseLeRobotValidator, createReport } from './validation'; +import { classifyLeRobotVersion } from './versionCapability'; const INFO_JSON = 'meta/info.json'; const EPISODES_JSONL = 'meta/episodes.jsonl'; const TASKS_JSONL = 'meta/tasks.jsonl'; /** - * Comprehensive validator for LeRobot v2.0 / v2.1 datasets. + * Comprehensive validator for LeRobot v2.1 and same-major read-only datasets. * Checks: file structure, meta/info.json fields, features, episodes consistency. */ export class V2FormatValidator extends BaseLeRobotValidator { @@ -57,32 +58,43 @@ export class V2FormatValidator extends BaseLeRobotValidator { } // ── 2. codebase_version ───────────────────────────────────────────────── - const ver = (resolvedInfo.codebase_version || '').trim().toLowerCase(); - if (!ver.startsWith('v2')) { + const versionCapability = classifyLeRobotVersion(resolvedInfo.codebase_version); + if (versionCapability.adapterVersion !== 'v2.1') { this.fail( 'meta_info', 'codebase_version', 'VERSION_MISMATCH', - 'codebase_version is not v2.x', + 'codebase_version is not supported by the v2.1 reader', resolvedInfo.codebase_version ?? 'Missing', - '"v2.0" or "v2.1"', - 'If this is v3.0, use the correct loader; otherwise set codebase_version to v2.0 or v2.1', + '"v2.1"', + 'Use a v2.1 dataset, or the matching reader for another major version', ); return createReport(this.items); } + if (versionCapability.status === 'read-only') { + this.warn( + 'meta_info', + 'codebase_version', + 'VERSION_READ_ONLY', + 'This newer v2 version is opened in read-only compatibility mode', + `"${resolvedInfo.codebase_version}"`, + '"v2.1" for export', + 'Convert with an official compatible tool before exporting', + ); + } this.pass( 'meta_info', 'codebase_version', `"${resolvedInfo.codebase_version}"`, - '"v2.0" or "v2.1"', + '"v2.1" or a newer read-only v2 minor', ); // ── 3. Meta info fields ────────────────────────────────────────────────── this.checkScalar( resolvedInfo.robot_type, 'robot_type', - 'Non-empty string', - (v) => typeof v === 'string' && (v as string).trim().length > 0, + 'Non-empty string or null', + (v) => v === null || (typeof v === 'string' && (v as string).trim().length > 0), 'ROBOT_TYPE_MISSING', 'robot_type is missing or empty', 'Set robot type, e.g. "so100" or "lekiwi"', diff --git a/src/core/versioning/v3Adapter.ts b/src/core/versioning/v3Adapter.ts index 549a14f..5f2a049 100644 --- a/src/core/versioning/v3Adapter.ts +++ b/src/core/versioning/v3Adapter.ts @@ -19,17 +19,18 @@ const TASKS_PARQUET_PATH = 'meta/tasks.parquet'; const DEFAULT_CHUNKS_SIZE = 1000; const DEFAULT_DATA_PATH = 'data/chunk-{chunk_index:03d}/file-{file_index:03d}.parquet'; const DEFAULT_VIDEO_PATH = 'videos/{video_key}/chunk-{chunk_index:03d}/file-{file_index:03d}.mp4'; -const BASE_EPISODE_COLUMNS = [ - 'episode_index', - 'length', - 'task_index', - 'dataset_from_index', - 'dataset_to_index', - 'chunk_index', - 'file_index', - 'data/chunk_index', - 'data/file_index', -] as const; + +function normalizeV3EpisodeTasks(rawTasks: unknown, taskMap: Record): string[] { + const values = Array.isArray(rawTasks) ? rawTasks : rawTasks == null ? [] : [rawTasks]; + return values + .map((value) => { + if (typeof value === 'number' && Number.isSafeInteger(value)) { + return taskMap[value] ?? ''; + } + return normalizeTaskDisplay(value).trim(); + }) + .filter((value) => value.length > 0); +} function pad(n: number): string { return String(n).padStart(3, '0'); @@ -69,20 +70,6 @@ function getDataFileValue(episode: EpisodeMetadata): unknown { return record['data/file_index'] ?? record.file_index; } -function getEpisodeColumnsToLoad(info?: LeRobotInfo): string[] { - const columns = new Set(BASE_EPISODE_COLUMNS); - - Object.entries(info?.features ?? {}).forEach(([featureKey, feature]) => { - if (feature?.dtype !== 'video') return; - columns.add(`videos/${featureKey}/chunk_index`); - columns.add(`videos/${featureKey}/file_index`); - columns.add(`videos/${featureKey}/from_timestamp`); - columns.add(`videos/${featureKey}/to_timestamp`); - }); - - return Array.from(columns); -} - /** * Adapter for LeRobot codebase v3.0. * - Episodes: meta/episodes/ directory (one or more parquet files, all loaded) @@ -166,7 +153,6 @@ export class V3Adapter extends LeRobotVersionAdapter { const chunksSize = ((info as Record | undefined)?.chunks_size as number | undefined) ?? DEFAULT_CHUNKS_SIZE; - const requestedColumns = getEpisodeColumnsToLoad(info); const allEpisodes: EpisodeMetadataV3[] = []; let chunkIdx = 0; let fileIdx = 0; @@ -188,7 +174,10 @@ export class V3Adapter extends LeRobotVersionAdapter { const path = paths?.[pathIndex] ?? episodeFilePath(chunkIdx, fileIdx); let ipcBytes: Uint8Array; try { - ipcBytes = await helpers.readParquetToIPC(path, requestedColumns); + // Load the complete schema. Episode parquet also carries per-episode + // stats and forward-compatible extension columns that must survive an + // edit/export roundtrip. + ipcBytes = await helpers.readParquetToIPC(path); } catch { if (paths) { throw new Error(`Failed to read v3 episode metadata shard: ${path}`); @@ -237,6 +226,11 @@ export class V3Adapter extends LeRobotVersionAdapter { ); }); + const taskMap = await this.loadTasks(dataSource, helpers); + allEpisodes.forEach((episode) => { + episode.tasks = normalizeV3EpisodeTasks((episode as Record).tasks, taskMap); + }); + return allEpisodes; } @@ -266,7 +260,8 @@ export class V3Adapter extends LeRobotVersionAdapter { tasks[index] = display !== '' ? display : 'Unknown Task'; } return tasks; - } catch { + } catch (error) { + if (error instanceof RangeError) throw error; try { const text = await dataSource.readText('meta/tasks.jsonl'); return LeRobotVersionAdapter.parseTasksFromJsonl(text); diff --git a/src/core/versioning/v3FormatValidator.ts b/src/core/versioning/v3FormatValidator.ts index dac745a..a30df23 100644 --- a/src/core/versioning/v3FormatValidator.ts +++ b/src/core/versioning/v3FormatValidator.ts @@ -4,6 +4,7 @@ import type { DataSource } from '../datasource/types'; import type { MetadataLoadingHelpers } from './types'; import { BaseLeRobotValidator, createReport } from './validation'; import { convertArrowValue } from './arrowUtils'; +import { classifyLeRobotVersion } from './versionCapability'; const INFO_JSON = 'meta/info.json'; const EPISODES_PARQUET_PATH = 'meta/episodes/chunk-000/file-000.parquet'; @@ -62,20 +63,36 @@ export class V3FormatValidator extends BaseLeRobotValidator { } // ── 2. codebase_version ───────────────────────────────────────────────── - const ver = (resolvedInfo.codebase_version || '').trim().toLowerCase(); - if (!ver.startsWith('v3')) { + const versionCapability = classifyLeRobotVersion(resolvedInfo.codebase_version); + if (versionCapability.adapterVersion !== 'v3.0') { this.fail( 'meta_info', 'codebase_version', 'VERSION_MISMATCH_V3', - 'codebase_version is not v3.x', + 'codebase_version is not supported by the v3.0 reader', resolvedInfo.codebase_version ?? 'Missing', '"v3.0"', 'Set codebase_version to v3.0', ); return createReport(this.items); } - this.pass('meta_info', 'codebase_version', `"${resolvedInfo.codebase_version}"`, '"v3.0"'); + if (versionCapability.status === 'read-only') { + this.warn( + 'meta_info', + 'codebase_version', + 'VERSION_READ_ONLY', + 'This newer v3 version is opened in read-only compatibility mode', + `"${resolvedInfo.codebase_version}"`, + '"v3.0" for export', + 'Convert with an official compatible tool before exporting', + ); + } + this.pass( + 'meta_info', + 'codebase_version', + `"${resolvedInfo.codebase_version}"`, + '"v3.0" or a newer read-only v3 minor', + ); const infoV3 = resolvedInfo as LeRobotInfo & { data_path?: string; @@ -133,8 +150,8 @@ export class V3FormatValidator extends BaseLeRobotValidator { this.checkScalar( resolvedInfo.robot_type, 'robot_type', - 'Non-empty string', - (v) => typeof v === 'string' && (v as string).trim().length > 0, + 'Non-empty string or null', + (v) => v === null || (typeof v === 'string' && (v as string).trim().length > 0), 'ROBOT_TYPE_MISSING', 'robot_type is missing or empty', 'Set robot type, e.g. "so100" or "lekiwi"', diff --git a/src/core/versioning/validation.ts b/src/core/versioning/validation.ts index 7a00282..f9bf7bc 100644 --- a/src/core/versioning/validation.ts +++ b/src/core/versioning/validation.ts @@ -196,6 +196,8 @@ export abstract class BaseLeRobotValidator { 'image', 'video', 'string', + 'language', + 'depth', ]); const DEFAULT_FEATURE_KEYS = new Set([ diff --git a/src/core/versioning/versionCapability.ts b/src/core/versioning/versionCapability.ts new file mode 100644 index 0000000..21cf3d4 --- /dev/null +++ b/src/core/versioning/versionCapability.ts @@ -0,0 +1,48 @@ +export type SupportedLeRobotVersion = 'v2.1' | 'v3.0'; +export type LeRobotVersionCapabilityStatus = 'supported' | 'read-only' | 'unsupported'; + +export interface LeRobotVersionCapability { + status: LeRobotVersionCapabilityStatus; + normalizedVersion: string | null; + adapterVersion: SupportedLeRobotVersion | null; +} + +export function normalizeLeRobotVersion(codebaseVersion: unknown): string | null { + if (typeof codebaseVersion !== 'string') return null; + const normalized = codebaseVersion.trim().toLowerCase(); + return normalized === '' ? null : normalized; +} + +/** + * Exact releases are writable. Strictly newer minor releases in a known major + * may be parsed by the latest same-major adapter, but remain read-only. + */ +export function classifyLeRobotVersion(codebaseVersion: unknown): LeRobotVersionCapability { + const normalizedVersion = normalizeLeRobotVersion(codebaseVersion); + if (normalizedVersion === 'v2.1' || normalizedVersion === 'v3.0') { + return { + status: 'supported', + normalizedVersion, + adapterVersion: normalizedVersion, + }; + } + + const match = normalizedVersion ? /^v(2|3)\.(0|[1-9]\d*)$/.exec(normalizedVersion) : null; + if (match) { + const major = Number(match[1]); + const minor = Number(match[2]); + if (Number.isSafeInteger(minor) && ((major === 2 && minor > 1) || (major === 3 && minor > 0))) { + return { + status: 'read-only', + normalizedVersion, + adapterVersion: major === 2 ? 'v2.1' : 'v3.0', + }; + } + } + + return { status: 'unsupported', normalizedVersion, adapterVersion: null }; +} + +export function isSupportedLeRobotVersion(codebaseVersion: unknown): boolean { + return classifyLeRobotVersion(codebaseVersion).status === 'supported'; +} diff --git a/src/core/versioning/versionRegistry.ts b/src/core/versioning/versionRegistry.ts index b24271d..b73145b 100644 --- a/src/core/versioning/versionRegistry.ts +++ b/src/core/versioning/versionRegistry.ts @@ -4,18 +4,22 @@ import { v2Adapter } from './v2Adapter'; import { v3Adapter } from './v3Adapter'; import { V2FormatValidator } from './v2FormatValidator'; import { V3FormatValidator } from './v3FormatValidator'; +import { + classifyLeRobotVersion, + isSupportedLeRobotVersion, + normalizeLeRobotVersion, +} from './versionCapability'; const v2FormatValidator = new V2FormatValidator(); const v3FormatValidator = new V3FormatValidator(); /** - * Returns the adapter for the given codebase_version (e.g. 'v2.0', 'v2.1', 'v3.0'). - * Uses prefix match: v2 -> v2Adapter, v3 -> v3Adapter. + * Returns an adapter for supported or same-major read-only versions. */ export function getAdapterForVersion(codebaseVersion: string): LeRobotVersionAdapter { - const normalized = codebaseVersion.trim().toLowerCase(); - if (normalized.startsWith('v3')) return v3Adapter; - if (normalized.startsWith('v2')) return v2Adapter; + const capability = classifyLeRobotVersion(codebaseVersion); + if (capability.adapterVersion === 'v3.0') return v3Adapter; + if (capability.adapterVersion === 'v2.1') return v2Adapter; throw new Error(`Unsupported LeRobot codebase_version: ${codebaseVersion}`); } @@ -24,10 +28,11 @@ export function getAdapterForVersion(codebaseVersion: string): LeRobotVersionAda * Run before load; use report.hasError to decide whether to proceed. */ export function getValidatorForVersion(codebaseVersion: string): BaseLeRobotValidator { - const normalized = codebaseVersion.trim().toLowerCase(); - if (normalized.startsWith('v3')) return v3FormatValidator; - if (normalized.startsWith('v2')) return v2FormatValidator; + const capability = classifyLeRobotVersion(codebaseVersion); + if (capability.adapterVersion === 'v3.0') return v3FormatValidator; + if (capability.adapterVersion === 'v2.1') return v2FormatValidator; throw new Error(`Unsupported LeRobot codebase_version: ${codebaseVersion}`); } +export { isSupportedLeRobotVersion, normalizeLeRobotVersion }; export { v2Adapter, v3Adapter }; diff --git a/src/platform/datasource/ArchiveDataSourceFactory.ts b/src/platform/datasource/ArchiveDataSourceFactory.ts index 125603b..913c71a 100644 --- a/src/platform/datasource/ArchiveDataSourceFactory.ts +++ b/src/platform/datasource/ArchiveDataSourceFactory.ts @@ -1,4 +1,4 @@ -import type { ProgressHandler } from './dataSources'; +import type { DataSource, ProgressHandler } from './dataSources'; import { TarDataSourceHttp, TarDataSourceLocal, @@ -47,11 +47,18 @@ export function getArchiveKindFromHeaders(headers: Headers): ArchiveKind | null if (cdLower.includes('.tar')) return 'tar'; if (cdLower.includes('.zip')) return 'zip'; - // content-type fallback (rough, but useful for same-origin/proxy downloads) - const ct = (headers.get('content-type') || '').toLowerCase(); - if (ct.includes('zip')) return 'zip'; - if (ct.includes('gzip')) return 'targz'; - if (ct.includes('x-tar') || ct.includes('tar')) return 'tar'; + // Content-Type fallback. Compare the media type rather than using substring + // matching: "application/gzip" itself contains "zip". + const contentType = (headers.get('content-type') || '').split(';', 1)[0].trim().toLowerCase(); + if (contentType === 'application/gzip' || contentType === 'application/x-gzip') return 'targz'; + if ( + contentType === 'application/zip' || + contentType === 'application/x-zip-compressed' || + contentType === 'multipart/x-zip' + ) { + return 'zip'; + } + if (contentType === 'application/x-tar' || contentType === 'application/tar') return 'tar'; return null; } @@ -92,7 +99,7 @@ export function createArchiveDataSourceFromUrl( onProgress?: ProgressHandler, knownKind?: ArchiveKind, options?: { accessMode?: RemoteArchiveAccessMode }, -) { +): DataSource { const kind = knownKind || getArchiveKindFromUrl(url); if (!kind) throw new Error('Unsupported archive type (need .zip/.tar/.tar.gz/.tgz)'); if (options?.accessMode === 'full') { @@ -103,7 +110,10 @@ export function createArchiveDataSourceFromUrl( return new ZipDataSourceHttp(url); } -export function createArchiveDataSourceFromFile(file: File, onProgress?: ProgressHandler) { +export function createArchiveDataSourceFromFile( + file: File, + onProgress?: ProgressHandler, +): DataSource { const kind = getArchiveKindFromFile(file); if (!kind) throw new Error('Unsupported archive type (need .zip/.tar/.tar.gz/.tgz)'); if (kind === 'targz') return new TarGzDataSourceLocal(file, onProgress); diff --git a/src/platform/datasource/RemoteManifestDataSource.ts b/src/platform/datasource/RemoteManifestDataSource.ts index f282a5c..8cb875c 100644 --- a/src/platform/datasource/RemoteManifestDataSource.ts +++ b/src/platform/datasource/RemoteManifestDataSource.ts @@ -1,4 +1,11 @@ import type { DataSource, ProgressHandler } from './dataSources'; +import { + assertMaterializable, + DataSourceSafetyError, + type DataSourceSafetyLimits, + resolveSafetyLimits, + validateUntrustedPath, +} from './inputSafety'; export interface RemoteFileEntry { logicalPath: string; @@ -24,51 +31,106 @@ function normalizePath(path: string): string { * remote datasets without loading all data upfront. */ export class RemoteManifestDataSource implements DataSource { - private readonly urlMap: Map; + private readonly entryMap = new Map(); + private readonly limits: Readonly; + + constructor(files: RemoteFileEntry[], limitOverrides?: Partial) { + this.limits = resolveSafetyLimits(limitOverrides); + if (files.length > this.limits.maxManifestEntries) { + throw new DataSourceSafetyError('ENTRY_COUNT_LIMIT', 'Manifest contains too many entries', { + count: files.length, + limit: this.limits.maxManifestEntries, + }); + } - constructor(files: RemoteFileEntry[]) { - this.urlMap = new Map(files.map((f) => [normalizePath(f.logicalPath), f.presignedUrl])); + let totalBytes = 0; + for (const file of files) { + const path = validateUntrustedPath(file.logicalPath, this.limits); + if (this.entryMap.has(path)) { + throw new DataSourceSafetyError('DUPLICATE_PATH', 'Manifest contains a duplicate path', { + path, + }); + } + validateManifestUrl(file.presignedUrl, this.limits, path); + if (file.sizeBytes != null && (!Number.isSafeInteger(file.sizeBytes) || file.sizeBytes < 0)) { + throw new DataSourceSafetyError('MANIFEST_INVALID', 'Manifest entry has an invalid size', { + path, + size: file.sizeBytes, + }); + } + if (file.sizeBytes != null) { + if (file.sizeBytes > this.limits.maxManifestEntryBytes) { + throw new DataSourceSafetyError( + 'ENTRY_SIZE_LIMIT', + 'Manifest entry exceeds the size limit', + { path, size: file.sizeBytes, limit: this.limits.maxManifestEntryBytes }, + ); + } + totalBytes += file.sizeBytes; + if (!Number.isSafeInteger(totalBytes) || totalBytes > this.limits.maxManifestTotalBytes) { + throw new DataSourceSafetyError( + 'TOTAL_SIZE_LIMIT', + 'Manifest exceeds the total size limit', + { total: totalBytes, limit: this.limits.maxManifestTotalBytes }, + ); + } + } + this.entryMap.set(path, file); + } } async exists(path: string): Promise { - return this.urlMap.has(normalizePath(path)); + return this.entryMap.has(normalizePath(path)); } async readText(path: string): Promise { - const url = this.getUrl(path); - const res = await fetch(url); - if (!res.ok) { - throw new Error(`HTTP ${res.status} fetching ${path}`); - } - return res.text(); + return new TextDecoder().decode(await this.readBytes(path)); } async readBytes(path: string, onProgress?: ProgressHandler): Promise { - const url = this.getUrl(path); + const entry = this.getEntry(path); + const url = entry.presignedUrl; + assertMaterializable(path, entry.sizeBytes ?? undefined, this.limits); const res = await fetch(url); if (!res.ok) { throw new Error(`HTTP ${res.status} fetching ${path}`); } const contentLength = res.headers.get('content-length'); - const total = contentLength ? parseInt(contentLength, 10) : undefined; - - if (!onProgress || !res.body) { - return new Uint8Array(await res.arrayBuffer()); + const total = contentLength ? Number(contentLength) : undefined; + if (total !== undefined && (!Number.isSafeInteger(total) || total < 0)) { + throw new DataSourceSafetyError( + 'MANIFEST_INVALID', + 'Response has an invalid content length', + { + path, + contentLength, + }, + ); } + assertMaterializable(path, total, this.limits); + if (!res.body) throw new Error(`HTTP response has no readable body fetching ${path}`); const reader = res.body.getReader(); const chunks: Uint8Array[] = []; let loaded = 0; - onProgress({ phase: 'download', loaded: 0, total, message: path }); + onProgress?.({ phase: 'download', loaded: 0, total, message: path }); while (true) { const { done, value } = await reader.read(); if (done) break; - chunks.push(value); loaded += value.byteLength; - onProgress({ phase: 'download', loaded, total, message: path }); + if (loaded > this.limits.maxMaterializedBytes) { + await reader.cancel(); + throw new DataSourceSafetyError( + 'MATERIALIZED_SIZE_LIMIT', + 'Remote file exceeded the in-memory size limit while downloading', + { path, loaded, limit: this.limits.maxMaterializedBytes }, + ); + } + chunks.push(value); + onProgress?.({ phase: 'download', loaded, total, message: path }); } const result = new Uint8Array(loaded); @@ -97,10 +159,36 @@ export class RemoteManifestDataSource implements DataSource { } private getUrl(path: string): string { - const url = this.urlMap.get(normalizePath(path)); - if (!url) { + return this.getEntry(path).presignedUrl; + } + + private getEntry(path: string): RemoteFileEntry { + const entry = this.entryMap.get(normalizePath(path)); + if (!entry) { throw new Error(`File not found in manifest: ${path}`); } - return url; + return entry; + } +} + +function validateManifestUrl( + rawUrl: string, + limits: Readonly, + path: string, +): void { + if (!rawUrl || rawUrl.length > limits.maxManifestUrlLength) { + throw new DataSourceSafetyError('URL_INVALID', 'Manifest URL is empty or too long', { + path, + urlLength: rawUrl?.length ?? 0, + limit: limits.maxManifestUrlLength, + }); + } + try { + const url = new URL(rawUrl); + if (url.protocol !== 'http:' && url.protocol !== 'https:') + throw new Error('unsupported protocol'); + if (!url.hostname || url.username || url.password) throw new Error('invalid authority'); + } catch { + throw new DataSourceSafetyError('URL_INVALID', 'Manifest URL must be an HTTP(S) URL', { path }); } } diff --git a/src/platform/datasource/dataSources.ts b/src/platform/datasource/dataSources.ts index a8fd46a..e9857b0 100644 --- a/src/platform/datasource/dataSources.ts +++ b/src/platform/datasource/dataSources.ts @@ -1,5 +1,13 @@ import { ZipReader, BlobReader, HttpReader, TextWriter, Uint8ArrayWriter } from '@zip.js/zip.js'; -import { gunzipSync } from 'fflate'; +import { Gunzip } from 'fflate'; +import { + assertMaterializable, + DataSourceSafetyError, + type DataSourceSafetyLimits, + resolveSafetyLimits, + validateEntrySizes, + validateUntrustedPath, +} from './inputSafety'; /** * 简易 LRU cache for blob URLs. @@ -118,9 +126,11 @@ abstract class BaseZipDataSource implements DataSource { protected objectUrlCache: BlobUrlLruCache = new BlobUrlLruCache(); protected objectUrlLoading: Map> = new Map(); protected initPromise: Promise | null = null; + protected readonly limits: Readonly; - constructor(zipReader: ZipReader) { + constructor(zipReader: ZipReader, limitOverrides?: Partial) { this.zipReader = zipReader; + this.limits = resolveSafetyLimits(limitOverrides); } protected resolvePath(path: string): string { @@ -136,13 +146,43 @@ abstract class BaseZipDataSource implements DataSource { } this.initPromise = (async () => { - const entries = await this.zipReader.getEntries(); - const paths = entries.map((e: any) => normalizePath(e.filename || e.name)); - this.datasetRootPrefix = getDatasetRootPrefix(paths); - entries.forEach((entry: any) => { - const key = normalizePath(entry.filename || entry.name); + let totalBytes = 0; + let count = 0; + let datasetRootPrefix: string | null = null; + for await (const entry of this.zipReader.getEntriesGenerator({ strictness: 'strict' })) { + count += 1; + if (count > this.limits.maxArchiveEntries) { + throw new DataSourceSafetyError('ENTRY_COUNT_LIMIT', 'ZIP contains too many entries', { + count, + limit: this.limits.maxArchiveEntries, + }); + } + const rawPath = entry.filename; + const key = validateUntrustedPath(rawPath, this.limits); + if (this.entryMap.has(key)) { + throw new DataSourceSafetyError('DUPLICATE_PATH', 'ZIP contains a duplicate path', { + path: key, + }); + } + totalBytes = validateEntrySizes( + key, + Number(entry.uncompressedSize ?? 0), + entry.compressedSize == null ? undefined : Number(entry.compressedSize), + totalBytes, + this.limits, + ); + if (datasetRootPrefix === null) { + if (key === 'meta/info.json') datasetRootPrefix = ''; + else if (key.endsWith('/meta/info.json')) { + datasetRootPrefix = key.slice(0, key.length - 'meta/info.json'.length); + } + } this.entryMap.set(key, entry); - }); + } + if (datasetRootPrefix === null) { + throw new Error('Not a valid LeRobot dataset: meta/info.json not found in archive'); + } + this.datasetRootPrefix = datasetRootPrefix; })(); try { @@ -169,6 +209,7 @@ abstract class BaseZipDataSource implements DataSource { await this.ensureInitialized(); const entry = this.entryMap.get(this.resolvePath(path)); if (!entry) throw new Error(`File not found in archive: ${path}`); + assertMaterializable(path, Number(entry.uncompressedSize ?? 0), this.limits); const writer = new TextWriter(); return entry.getData(writer); } @@ -177,6 +218,7 @@ abstract class BaseZipDataSource implements DataSource { await this.ensureInitialized(); const entry = this.entryMap.get(this.resolvePath(path)); if (!entry) throw new Error(`File not found in archive: ${path}`); + assertMaterializable(path, Number(entry.uncompressedSize ?? 0), this.limits); const writer = new Uint8ArrayWriter(); return entry.getData(writer); } @@ -230,22 +272,22 @@ abstract class BaseZipDataSource implements DataSource { } export class ZipDataSourceLocal extends BaseZipDataSource { - constructor(file: File) { + constructor(file: File, limitOverrides?: Partial) { const reader = new BlobReader(file); - const zipReader = new ZipReader(reader, { useWebWorkers: false }); - super(zipReader); + const zipReader = new ZipReader(reader, { useWebWorkers: false, strictness: 'strict' }); + super(zipReader, limitOverrides); } } export class ZipDataSourceHttp extends BaseZipDataSource { - constructor(url: string) { + constructor(url: string, limitOverrides?: Partial) { const reader = new HttpReader(url, { preventHeadRequest: true, useRangeHeader: true, forceRangeRequests: true, }); - const zipReader = new ZipReader(reader, { useWebWorkers: false }); - super(zipReader); + const zipReader = new ZipReader(reader, { useWebWorkers: false, strictness: 'strict' }); + super(zipReader, limitOverrides); } } @@ -271,6 +313,14 @@ async function collectSmallResponse( signal?: AbortSignal, ): Promise { const total = Number(response.headers.get('content-length') || 0); + if (Number.isFinite(total) && total > maxBytes) { + await response.body?.cancel().catch(() => undefined); + throw new DataSourceSafetyError( + 'MATERIALIZED_SIZE_LIMIT', + `Complete archive download exceeds the ${maxBytes} byte safety limit`, + { declaredSize: total, limit: maxBytes }, + ); + } const reader = response.body?.getReader(); if (!reader) throw new Error('The server did not provide a readable response body'); const chunks: Uint8Array[] = []; @@ -282,8 +332,11 @@ async function collectSmallResponse( if (done) break; loaded += value.byteLength; if (loaded > maxBytes) { - throw new Error( - `This browser cannot store a ${loaded} byte archive safely. Enable Range requests or use a browser with OPFS support.`, + await reader.cancel().catch(() => undefined); + throw new DataSourceSafetyError( + 'MATERIALIZED_SIZE_LIMIT', + `Complete archive download exceeds the ${maxBytes} byte safety limit`, + { loaded, limit: maxBytes }, ); } chunks.push(value); @@ -309,18 +362,22 @@ async function downloadResponseToOpfs( const storage = navigator.storage as OpfsStorage; const directory = await storage.getDirectory?.(); if (!directory) { + const inMemoryLimit = Math.min(maxBytes, MAX_IN_MEMORY_FULL_ARCHIVE_BYTES); return { - file: await collectSmallResponse( - response, - MAX_IN_MEMORY_FULL_ARCHIVE_BYTES, - onProgress, - signal, - ), + file: await collectSmallResponse(response, inMemoryLimit, onProgress, signal), cleanup: async () => undefined, }; } const expected = Number(response.headers.get('content-length') || 0); + if (Number.isFinite(expected) && expected > maxBytes) { + await response.body?.cancel().catch(() => undefined); + throw new DataSourceSafetyError( + 'MATERIALIZED_SIZE_LIMIT', + `Complete archive download exceeds the ${maxBytes} byte safety limit`, + { declaredSize: expected, limit: maxBytes }, + ); + } const estimate = await navigator.storage.estimate?.(); if ( expected > 0 && @@ -348,7 +405,12 @@ async function downloadResponseToOpfs( if (done) break; loaded += value.byteLength; if (loaded > maxBytes) { - throw new Error(`Complete archive download exceeds the ${maxBytes} byte safety limit`); + await reader.cancel().catch(() => undefined); + throw new DataSourceSafetyError( + 'MATERIALIZED_SIZE_LIMIT', + `Complete archive download exceeds the ${maxBytes} byte safety limit`, + { loaded, limit: maxBytes }, + ); } await writable.write(value); onProgress?.({ @@ -668,10 +730,15 @@ interface TarEntryMeta { function parseOctal(str: string): number { const trimmed = str.replace(/\0.*$/, '').trim(); if (!trimmed) return 0; - return parseInt(trimmed, 8); + if (!/^[0-7]+$/.test(trimmed)) return Number.NaN; + return Number.parseInt(trimmed, 8); } -function parseTarHeader(block: Uint8Array, globalOffset: number): TarEntryMeta | null { +function parseTarHeader( + block: Uint8Array, + globalOffset: number, + limits: Readonly, +): TarEntryMeta | null { // 检查是否为空块 if (block.every((b) => b === 0)) return null; @@ -702,7 +769,12 @@ function parseTarHeader(block: Uint8Array, globalOffset: number): TarEntryMeta | // 'g', 'x' - PAX 扩展头(跳过) const type: 'file' | 'dir' = typeflag === 53 /* '5' */ ? 'dir' : 'file'; - return { path: normalizePath(name), offset: globalOffset + 512, size, type }; + return { + path: validateUntrustedPath(name, limits), + offset: globalOffset + 512, + size, + type, + }; } function pad512(n: number) { @@ -715,21 +787,72 @@ function toSafeBuffer(bytes: Uint8Array): ArrayBuffer { return safe.buffer; } -function concatUint8(a: Uint8Array, b: Uint8Array): Uint8Array { - const out = new Uint8Array(a.length + b.length); - out.set(a, 0); - out.set(b, a.length); - return out; -} +async function gunzipBounded( + stream: ReadableStream, + compressedSize: number | undefined, + limits: Readonly, +): Promise { + const chunks: Uint8Array[] = []; + let outputBytes = 0; + const maxOutputBytes = Math.min(limits.maxTarGzOutputBytes, limits.maxMaterializedBytes); + const append = (chunk: Uint8Array) => { + outputBytes += chunk.byteLength; + if ( + compressedSize !== undefined && + outputBytes >= limits.compressionRatioFloorBytes && + (compressedSize <= 0 || outputBytes / compressedSize > limits.maxCompressionRatio) + ) { + throw new DataSourceSafetyError( + 'COMPRESSION_RATIO_LIMIT', + 'TAR.GZ has a suspicious compression ratio', + { compressedSize, outputBytes, limit: limits.maxCompressionRatio }, + ); + } + if (outputBytes > maxOutputBytes) { + throw new DataSourceSafetyError( + 'TOTAL_SIZE_LIMIT', + 'TAR.GZ decompressed output exceeds the in-memory safety limit', + { outputBytes, limit: maxOutputBytes }, + ); + } + chunks.push(chunk); + }; -async function gunzipNative(data: ArrayBuffer): Promise { if (typeof DecompressionStream !== 'undefined') { const ds = new DecompressionStream('gzip'); - const stream = new Response(new Blob([data]).stream().pipeThrough(ds)); - const buf = await stream.arrayBuffer(); - return new Uint8Array(buf); + const reader = stream + .pipeThrough(ds as unknown as TransformStream) + .getReader(); + while (true) { + const { done, value } = await reader.read(); + if (done) break; + append(value); + } + } else { + const inputReader = stream.getReader(); + let inflateError: Error | null = null; + const gunzip = new Gunzip((chunk) => { + try { + append(chunk); + } catch (error) { + inflateError = error as Error; + } + }); + while (true) { + const { done, value } = await inputReader.read(); + gunzip.push(value ?? new Uint8Array(0), done); + if (inflateError) throw inflateError; + if (done) break; + } + } + + const output = new Uint8Array(outputBytes); + let offset = 0; + for (const chunk of chunks) { + output.set(chunk, offset); + offset += chunk.byteLength; } - return gunzipSync(new Uint8Array(data)); + return output; } abstract class BaseTarDataSource implements DataSource { @@ -738,6 +861,34 @@ abstract class BaseTarDataSource implements DataSource { protected objectUrlLoading: Map> = new Map(); protected indexed = false; protected datasetRootPrefix = ''; + protected totalEntryBytes = 0; + protected readonly limits: Readonly; + + constructor(limitOverrides?: Partial) { + this.limits = resolveSafetyLimits(limitOverrides); + } + + protected addEntry(entry: TarEntryMeta): void { + if (this.entries.size >= this.limits.maxArchiveEntries) { + throw new DataSourceSafetyError('ENTRY_COUNT_LIMIT', 'TAR contains too many entries', { + count: this.entries.size + 1, + limit: this.limits.maxArchiveEntries, + }); + } + if (this.entries.has(entry.path)) { + throw new DataSourceSafetyError('DUPLICATE_PATH', 'TAR contains a duplicate path', { + path: entry.path, + }); + } + this.totalEntryBytes = validateEntrySizes( + entry.path, + entry.size, + undefined, + this.totalEntryBytes, + this.limits, + ); + this.entries.set(entry.path, entry); + } protected resolvePath(path: string): string { return `${this.datasetRootPrefix}${normalizePath(path)}`; @@ -811,6 +962,7 @@ abstract class BaseTarDataSource implements DataSource { this.objectUrlCache.clear(); this.objectUrlLoading.clear(); this.entries.clear(); + this.totalEntryBytes = 0; this.datasetRootPrefix = ''; this.indexed = false; } @@ -819,7 +971,7 @@ abstract class BaseTarDataSource implements DataSource { export class TarDataSourceLocal implements DataSource { private base: BaseTarDataSource; - constructor(file: File) { + constructor(file: File, limitOverrides?: Partial) { // delegate to shared base impl (composition avoids touching public type surface) this.base = new (class extends BaseTarDataSource { private outer = file; @@ -846,43 +998,27 @@ export class TarDataSourceLocal implements DataSource { this.indexPromise = (async () => { const total = this.outer.size; - // `pos` 表示已经从文件头读取到的“末端”绝对偏移(end offset) - // buffer 保存的是 [pos - buffer.length, pos) 这一段数据 let pos = 0; - let buffer: Uint8Array = new Uint8Array(0); - const chunkSize = 4 * 1024 * 1024; - - while (pos < total) { - const remain = total - pos; - const chunk = await this.readChunk(pos, Math.min(chunkSize, remain)); - buffer = concatUint8(buffer, chunk) as Uint8Array; - // 读入 chunk 后,pos 前进到新的 end offset - pos += chunk.length; - let cursor = 0; - while (buffer.length - cursor >= 512) { - const headerView = buffer.subarray(cursor, cursor + 512); - if (headerView.every((b) => b === 0)) { - pos = total; // reached end-of-archive - cursor = buffer.length; - break; - } - // 注意:buffer 可能包含上一次循环的残留数据,因此 header 的全局偏移 - // 不能用 `pos + cursor`,而应该用 `(pos - buffer.length) + cursor` - const entry = parseTarHeader(headerView, pos - buffer.length + cursor); - if (!entry) { - pos = total; - cursor = buffer.length; - break; - } - this.entries.set(entry.path, entry); - const dataSize = pad512(entry.size); - const need = 512 + dataSize; - if (buffer.length - cursor < need) { - break; // need more data - } - cursor += need; + while (pos + 512 <= total) { + const header = await this.readChunk(pos, 512); + if (header.every((b) => b === 0)) break; + const entry = parseTarHeader(header, pos, this.limits); + if (!entry) break; + this.addEntry(entry); + const next = pos + 512 + pad512(entry.size); + if (!Number.isSafeInteger(next) || next > total) { + throw new DataSourceSafetyError( + 'ARCHIVE_TRUNCATED', + 'TAR entry exceeds archive bounds', + { + path: entry.path, + offset: entry.offset, + size: entry.size, + archiveSize: total, + }, + ); } - buffer = buffer.slice(cursor); + pos = next; this.emit(onProgress, { phase: 'index', loaded: pos, total, message: 'Indexing tar' }); } this.finalizeDatasetRootPrefix(); @@ -900,11 +1036,12 @@ export class TarDataSourceLocal implements DataSource { await this.ensureIndex(onProgress); const entry = this.entries.get(this.resolvePath(path)); if (!entry) throw new Error(`File not found in tar: ${path}`); + assertMaterializable(path, entry.size, this.limits); const slice = this.outer.slice(entry.offset, entry.offset + entry.size); onProgress?.({ phase: 'read', loaded: 0, total: entry.size }); return new Uint8Array(await slice.arrayBuffer()); } - })(); + })(limitOverrides); } async exists(path: string): Promise { @@ -941,12 +1078,18 @@ class TarBufferDataSource implements DataSource { private bufferReady: Promise; private base: BaseTarDataSource; - constructor(buffer: Uint8Array | Promise) { + constructor( + buffer: Uint8Array | Promise, + limitOverrides?: Partial, + ) { + const limits = resolveSafetyLimits(limitOverrides); if (buffer instanceof Promise) { this.bufferReady = buffer.then((buf) => { + assertMaterializable('archive', buf.byteLength, limits); this.buffer = buf; }); } else { + assertMaterializable('archive', buffer.byteLength, limits); this.buffer = buffer; this.bufferReady = Promise.resolve(); } @@ -955,8 +1098,11 @@ class TarBufferDataSource implements DataSource { private outer: TarBufferDataSource; private indexPromise: Promise | null = null; - constructor(outer: TarBufferDataSource) { - super(); + constructor( + limitOverrides: Partial | undefined, + outer: TarBufferDataSource, + ) { + super(limitOverrides); this.outer = outer; } @@ -974,10 +1120,22 @@ class TarBufferDataSource implements DataSource { while (cursor + 512 <= total) { const block = this.outer.buffer.subarray(cursor, cursor + 512); if (block.every((b) => b === 0)) break; - const entry = parseTarHeader(block, cursor); + const entry = parseTarHeader(block, cursor, this.limits); if (!entry) break; - this.entries.set(entry.path, entry); + this.addEntry(entry); const step = 512 + pad512(entry.size); + if (!Number.isSafeInteger(cursor + step) || cursor + step > total) { + throw new DataSourceSafetyError( + 'ARCHIVE_TRUNCATED', + 'TAR entry exceeds archive bounds', + { + path: entry.path, + offset: entry.offset, + size: entry.size, + archiveSize: total, + }, + ); + } cursor += step; onProgress?.({ phase: 'index', loaded: cursor, total }); } @@ -996,13 +1154,14 @@ class TarBufferDataSource implements DataSource { await this.ensureIndex(onProgress); const entry = this.entries.get(this.resolvePath(path)); if (!entry) throw new Error(`File not found in tar: ${path}`); + assertMaterializable(path, entry.size, this.limits); const data = this.outer.buffer.subarray(entry.offset, entry.offset + entry.size); onProgress?.({ phase: 'read', loaded: entry.size, total: entry.size }); const copy = new Uint8Array(data.length); copy.set(data); return copy; } - })(this); + })(limitOverrides, this); } async exists(path: string): Promise { @@ -1035,41 +1194,50 @@ class TarBufferDataSource implements DataSource { } export class TarGzDataSourceLocal extends TarBufferDataSource { - constructor(file: File, onProgress?: ProgressHandler) { + constructor( + file: File, + onProgress?: ProgressHandler, + limitOverrides?: Partial, + ) { + const limits = resolveSafetyLimits(limitOverrides); const bufferPromise = (async () => { onProgress?.({ phase: 'download', loaded: 0, total: file.size, message: 'Reading tar.gz' }); - const data = new Uint8Array(await file.arrayBuffer()); - onProgress?.({ phase: 'gunzip', loaded: 0, total: data.length, message: 'Decompressing' }); - const unzipped = await gunzipNative(data.buffer); + onProgress?.({ phase: 'gunzip', loaded: 0, total: file.size, message: 'Decompressing' }); + const unzipped = await gunzipBounded(file.stream(), file.size, limits); return unzipped; })(); - super(bufferPromise); + super(bufferPromise, limitOverrides); } } export class TarGzDataSourceHttp extends TarBufferDataSource { - constructor(url: string, onProgress?: ProgressHandler) { + constructor( + url: string, + onProgress?: ProgressHandler, + limitOverrides?: Partial, + ) { + const limits = resolveSafetyLimits(limitOverrides); const bufferPromise = (async () => { const res = await fetch(url); if (!res.ok) throw new Error(`Failed to fetch ${url}: ${res.status}`); const total = Number(res.headers.get('content-length') || 0); - const arrayBuf = await res.arrayBuffer(); + if (!res.body) throw new Error(`Failed to fetch ${url}: response body is not readable`); onProgress?.({ phase: 'download', - loaded: total || arrayBuf.byteLength, + loaded: 0, total, - message: 'Downloaded tar.gz', + message: 'Downloading tar.gz', }); onProgress?.({ phase: 'gunzip', loaded: 0, - total: arrayBuf.byteLength, + total, message: 'Decompressing', }); - const unzipped = await gunzipNative(arrayBuf); + const unzipped = await gunzipBounded(res.body, total || undefined, limits); return unzipped; })(); - super(bufferPromise); + super(bufferPromise, limitOverrides); } } @@ -1083,46 +1251,44 @@ export class TarGzDataSourceHttp extends TarBufferDataSource { * tar 格式适合小型数据集(< 100MB)。 */ export class TarDataSourceHttp extends TarBufferDataSource { - constructor(url: string, onProgress?: ProgressHandler) { + constructor( + url: string, + onProgress?: ProgressHandler, + limitOverrides?: Partial, + ) { + const limits = resolveSafetyLimits(limitOverrides); const bufferPromise = (async () => { const res = await fetch(url); if (!res.ok) throw new Error(`Failed to fetch ${url}: ${res.status}`); const total = Number(res.headers.get('content-length') || 0); - - // 使用流式读取以便报告进度 - if (res.body && total > 0) { - const reader = res.body.getReader(); - const chunks: Uint8Array[] = []; - let loaded = 0; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - chunks.push(value); - loaded += value.length; - onProgress?.({ phase: 'download', loaded, total, message: 'Downloading tar...' }); - } - - // 合并所有 chunks - const result = new Uint8Array(loaded); - let offset = 0; - for (const chunk of chunks) { - result.set(chunk, offset); - offset += chunk.length; + assertMaterializable('remote tar', total || undefined, limits); + if (!res.body) throw new Error(`Failed to fetch ${url}: response body is not readable`); + const reader = res.body.getReader(); + const chunks: Uint8Array[] = []; + let loaded = 0; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + loaded += value.byteLength; + if (loaded > limits.maxMaterializedBytes) { + await reader.cancel(); + throw new DataSourceSafetyError( + 'MATERIALIZED_SIZE_LIMIT', + 'Remote TAR exceeded the in-memory size limit while downloading', + { loaded, limit: limits.maxMaterializedBytes }, + ); } - return result; - } else { - // 降级:一次性读取 - const arrayBuf = await res.arrayBuffer(); - onProgress?.({ - phase: 'download', - loaded: arrayBuf.byteLength, - total: arrayBuf.byteLength, - message: 'Downloaded tar', - }); - return new Uint8Array(arrayBuf); + chunks.push(value); + onProgress?.({ phase: 'download', loaded, total, message: 'Downloading tar...' }); + } + const result = new Uint8Array(loaded); + let offset = 0; + for (const chunk of chunks) { + result.set(chunk, offset); + offset += chunk.length; } + return result; })(); - super(bufferPromise); + super(bufferPromise, limitOverrides); } } diff --git a/src/platform/datasource/inputSafety.ts b/src/platform/datasource/inputSafety.ts new file mode 100644 index 0000000..3acde68 --- /dev/null +++ b/src/platform/datasource/inputSafety.ts @@ -0,0 +1,185 @@ +export interface DataSourceSafetyLimits { + maxArchiveEntries: number; + maxArchiveEntryBytes: number; + maxArchiveTotalBytes: number; + maxMaterializedBytes: number; + maxPathLength: number; + maxPathDepth: number; + maxCompressionRatio: number; + compressionRatioFloorBytes: number; + maxTarGzOutputBytes: number; + maxManifestEntries: number; + maxManifestEntryBytes: number; + maxManifestTotalBytes: number; + maxManifestUrlLength: number; +} + +export const DEFAULT_DATA_SOURCE_SAFETY_LIMITS: Readonly = Object.freeze({ + maxArchiveEntries: 250_000, + maxArchiveEntryBytes: 16 * 1024 ** 3, + maxArchiveTotalBytes: 4 * 1024 ** 4, + maxMaterializedBytes: 512 * 1024 ** 2, + maxPathLength: 1024, + maxPathDepth: 64, + maxCompressionRatio: 1000, + compressionRatioFloorBytes: 1024 * 1024, + maxTarGzOutputBytes: 512 * 1024 ** 2, + maxManifestEntries: 250_000, + maxManifestEntryBytes: 64 * 1024 ** 3, + maxManifestTotalBytes: 16 * 1024 ** 4, + maxManifestUrlLength: 16 * 1024, +}); + +export type DataSourceSafetyErrorCode = + | 'ENTRY_COUNT_LIMIT' + | 'ENTRY_SIZE_LIMIT' + | 'TOTAL_SIZE_LIMIT' + | 'MATERIALIZED_SIZE_LIMIT' + | 'PATH_INVALID' + | 'PATH_LENGTH_LIMIT' + | 'PATH_DEPTH_LIMIT' + | 'DUPLICATE_PATH' + | 'COMPRESSION_RATIO_LIMIT' + | 'ARCHIVE_TRUNCATED' + | 'MANIFEST_INVALID' + | 'URL_INVALID'; + +export class DataSourceSafetyError extends Error { + readonly name = 'DataSourceSafetyError'; + readonly code: DataSourceSafetyErrorCode; + readonly details: Readonly>; + + constructor( + code: DataSourceSafetyErrorCode, + message: string, + details: Readonly> = {}, + ) { + super(message); + this.code = code; + this.details = details; + } +} + +export function resolveSafetyLimits( + overrides?: Partial, +): Readonly { + const limits = { ...DEFAULT_DATA_SOURCE_SAFETY_LIMITS, ...overrides }; + for (const [name, value] of Object.entries(limits)) { + if (!Number.isFinite(value) || value <= 0) { + throw new TypeError(`Invalid data-source safety limit ${name}: ${value}`); + } + } + return limits; +} + +export function validateUntrustedPath( + rawPath: string, + limits: Readonly, +): string { + if ( + !rawPath || + rawPath.includes('\0') || + rawPath.startsWith('/') || + rawPath.startsWith('\\') || + /^[a-zA-Z]:[\\/]/.test(rawPath) + ) { + throw new DataSourceSafetyError('PATH_INVALID', 'Archive path is empty or absolute', { + path: rawPath, + }); + } + + const normalized = rawPath + .replace(/\\/g, '/') + .replace(/^\.\/+/, '') + .replace(/\/{2,}/g, '/'); + const segments = normalized.split('/'); + if (!normalized || segments.every((segment) => !segment || segment === '.')) { + throw new DataSourceSafetyError('PATH_INVALID', 'Archive path is empty after normalization', { + path: rawPath, + }); + } + if (segments.some((segment) => segment === '..')) { + throw new DataSourceSafetyError('PATH_INVALID', 'Archive path attempts to escape its root', { + path: rawPath, + }); + } + if (normalized.length > limits.maxPathLength) { + throw new DataSourceSafetyError('PATH_LENGTH_LIMIT', 'Archive path is too long', { + pathLength: normalized.length, + limit: limits.maxPathLength, + }); + } + const depth = segments.filter((segment) => segment && segment !== '.').length; + if (depth > limits.maxPathDepth) { + throw new DataSourceSafetyError('PATH_DEPTH_LIMIT', 'Archive path is too deep', { + depth, + limit: limits.maxPathDepth, + }); + } + return normalized; +} + +export function validateEntrySizes( + path: string, + uncompressedSize: number, + compressedSize: number | undefined, + currentTotal: number, + limits: Readonly, +): number { + if (!Number.isSafeInteger(uncompressedSize) || uncompressedSize < 0) { + throw new DataSourceSafetyError('ENTRY_SIZE_LIMIT', 'Archive entry has an invalid size', { + path, + size: uncompressedSize, + }); + } + if ( + compressedSize !== undefined && + (!Number.isSafeInteger(compressedSize) || compressedSize < 0) + ) { + throw new DataSourceSafetyError( + 'ENTRY_SIZE_LIMIT', + 'Archive entry has an invalid compressed size', + { path, compressedSize }, + ); + } + if (uncompressedSize > limits.maxArchiveEntryBytes) { + throw new DataSourceSafetyError('ENTRY_SIZE_LIMIT', 'Archive entry exceeds the size limit', { + path, + size: uncompressedSize, + limit: limits.maxArchiveEntryBytes, + }); + } + const total = currentTotal + uncompressedSize; + if (!Number.isSafeInteger(total) || total > limits.maxArchiveTotalBytes) { + throw new DataSourceSafetyError('TOTAL_SIZE_LIMIT', 'Archive exceeds the total size limit', { + total, + limit: limits.maxArchiveTotalBytes, + }); + } + if ( + compressedSize !== undefined && + uncompressedSize >= limits.compressionRatioFloorBytes && + (compressedSize <= 0 || uncompressedSize / compressedSize > limits.maxCompressionRatio) + ) { + throw new DataSourceSafetyError( + 'COMPRESSION_RATIO_LIMIT', + 'Archive entry has a suspicious compression ratio', + { path, compressedSize, uncompressedSize, limit: limits.maxCompressionRatio }, + ); + } + return total; +} + +export function assertMaterializable( + path: string, + size: number | undefined, + limits: Readonly, +): void { + if (size !== undefined && size > limits.maxMaterializedBytes) { + throw new DataSourceSafetyError( + 'MATERIALIZED_SIZE_LIMIT', + 'File is too large to materialize safely in memory', + { path, size, limit: limits.maxMaterializedBytes }, + ); + } +} diff --git a/src/platform/datasource/remotePreflight.ts b/src/platform/datasource/remotePreflight.ts index 6a5eeb9..2533837 100644 --- a/src/platform/datasource/remotePreflight.ts +++ b/src/platform/datasource/remotePreflight.ts @@ -2,6 +2,7 @@ import type { ArchiveKind } from './ArchiveDataSourceFactory'; import { getArchiveKindFromHeaders, getArchiveKindFromMagicBytes, + getArchiveKindFromUrl, } from './ArchiveDataSourceFactory'; export type RemotePreflightFailureCode = @@ -113,6 +114,31 @@ async function readPrefix(response: Response, maxBytes: number): Promise BigInt(newEpisodeIndex)); + const index = Array.from({ length: n }, (_, i) => BigInt(globalStart + i)); - const overrides: Record = {}; + const overrides: Record = {}; if (table.schema.fields.some((f) => f.name === 'episode_index')) { - overrides.episode_index = episodeIndex; + overrides.episode_index = arrow.vectorFromArray(episodeIndex, new arrow.Int64()); } if (table.schema.fields.some((f) => f.name === 'index')) { - overrides.index = index; + overrides.index = arrow.vectorFromArray(index, new arrow.Int64()); + } + const sourceTaskIndex = table.getChild('task_index'); + if (sourceTaskIndex) { + const rewritten = new Array(n); + for (let row = 0; row < n; row++) { + const sourceIndex = toSafeInteger(sourceTaskIndex.get(row), 'task_index'); + const targetIndex = resolveExportTaskIndex(taskPlan, newEpisodeIndex, sourceIndex); + if (targetIndex === undefined) { + throw new Error( + `Cannot map frame task_index ${sourceIndex} for exported episode ${newEpisodeIndex}`, + ); + } + rewritten[row] = BigInt(targetIndex); + } + overrides.task_index = arrow.vectorFromArray(rewritten, new arrow.Int64()); } if (Object.keys(overrides).length === 0) return table; - const overrideTable = arrow.tableFromArrays(overrides); + const overrideTable = new arrow.Table(overrides as never); return table.assign(overrideTable) as unknown as Table; } +async function getValidatedEpisodeTable( + dataLoader: LeRobotDataLoader, + episode: EpisodeMetadata, +): Promise { + const { table } = await dataLoader.getEpisodeTableForExport(episode.episode_index); + if (!Number.isSafeInteger(episode.length) || episode.length < 0) { + throw new Error( + `Episode ${episode.episode_index} has invalid metadata length ${episode.length}`, + ); + } + if (table.numRows !== episode.length) { + throw new Error( + `Episode ${episode.episode_index} row count ${table.numRows} does not match metadata length ${episode.length}`, + ); + } + return table; +} + export interface ExportDataOptions { /** Columns to strip from the exported parquet files (e.g. image features replaced by video). */ excludeColumns?: Set; + /** Source task mapping used to rewrite frame-level task_index values. */ + tasks?: Record; + /** Precomputed canonical plan shared with stats and metadata preparation. */ + taskPlan?: ExportTaskPlan; } /** @@ -60,23 +113,27 @@ export async function exportDataFiles( onProgress?: (p: ExportProgress) => void, signal?: AbortSignal, options?: ExportDataOptions, -): Promise { +): Promise { if (signal?.aborted) throw new DOMException('Export cancelled', 'AbortError'); - const isTargetV3 = targetVersion?.startsWith('v3') ?? info.codebase_version.startsWith('v3'); - const isSourceV3 = info.codebase_version.startsWith('v3'); + const isTargetV3 = targetVersion !== undefined ? targetVersion === 'v3.0' : isV3Info(info); + const isSourceV3 = isV3Info(info); + const taskPlan = options?.taskPlan ?? buildExportTaskPlan(episodes, options?.tasks ?? {}); const total = episodes.length; - if (total === 0) return; + if (total === 0) { + return isTargetV3 ? { episodes: new Map(), total_chunks: 0, total_files: 0 } : undefined; + } if (isTargetV3 && !isSourceV3) { - await mergeV2ToV3(dataLoader, info, episodes, adapter, onProgress, signal, options); + return mergeV2ToV3(dataLoader, info, episodes, adapter, taskPlan, onProgress, signal, options); } else if (!isTargetV3 && isSourceV3) { - await splitV3ToV2(dataLoader, episodes, adapter, onProgress, signal, options); + await splitV3ToV2(dataLoader, episodes, adapter, taskPlan, onProgress, signal, options); } else { - await filterAndWriteSameVersion( + return filterAndWriteSameVersion( dataLoader, info, episodes, adapter, + taskPlan, onProgress, signal, options, @@ -96,58 +153,31 @@ async function filterAndWriteSameVersion( info: LeRobotInfo, episodes: EpisodeMetadata[], adapter: ExportAdapter, + taskPlan: ExportTaskPlan, onProgress?: (p: ExportProgress) => void, signal?: AbortSignal, options?: ExportDataOptions, -): Promise { +): Promise { const total = episodes.length; if (isV3Info(info)) { - // Output is packed into new 0-based v3 layout: one file per `chunks_size` - // block of exported episodes (data/chunk-XXX/file-000.parquet). This makes - // the exported dataset self-contained and resilient to the user deleting - // episodes from arbitrary source chunks. - const chunksSize = info.chunks_size ?? CHUNK_SIZE_DEFAULT; - const chunkToIndices = new Map(); - for (let i = 0; i < episodes.length; i++) { - const chunkIdx = Math.floor(i / chunksSize); - if (!chunkToIndices.has(chunkIdx)) chunkToIndices.set(chunkIdx, []); - chunkToIndices.get(chunkIdx)!.push(i); - } - let globalRow = 0; - let done = 0; - for (const [chunkIdx, indices] of chunkToIndices.entries()) { - if (signal?.aborted) throw new DOMException('Export cancelled', 'AbortError'); - const tables: Table[] = []; - for (const i of indices) { - const ep = episodes[i]; - const { table } = await dataLoader.getEpisodeTableForExport(ep.episode_index); - const stripped = dropColumns(table, options?.excludeColumns); - const reindexed = rewriteEpisodeAndGlobalIndex(stripped, i, globalRow); - globalRow += stripped.numRows; - tables.push(reindexed); - } - const merged = concatTables(tables); - const outPath = `data/chunk-${String(chunkIdx).padStart(3, '0')}/file-000.parquet`; - await ensureParentDir(adapter, outPath); - const bytes = await tableToParquetBytes(merged); - adapter.writeFile(outPath, bytes); - done += indices.length; - onProgress?.({ - phase: 'data', - current: done, - total, - message: `Data chunk ${chunkIdx}`, - cancelable: false, - }); - } + return writeV3DataFiles( + dataLoader, + info, + episodes, + adapter, + taskPlan, + onProgress, + signal, + options, + ); } else { let globalRow = 0; for (let i = 0; i < episodes.length; i++) { if (signal?.aborted) throw new DOMException('Export cancelled', 'AbortError'); const ep = episodes[i]; - const { table } = await dataLoader.getEpisodeTableForExport(ep.episode_index); + const table = await getValidatedEpisodeTable(dataLoader, ep); const stripped = dropColumns(table, options?.excludeColumns); - const reindexed = rewriteEpisodeAndGlobalIndex(stripped, i, globalRow); + const reindexed = rewriteEpisodeAndGlobalIndex(stripped, i, globalRow, taskPlan); globalRow += stripped.numRows; const chunkIdx = Math.floor(i / (info.chunks_size ?? CHUNK_SIZE_DEFAULT)); const outPath = `data/chunk-${String(chunkIdx).padStart(3, '0')}/episode_${String(i).padStart(6, '0')}.parquet`; @@ -163,6 +193,7 @@ async function filterAndWriteSameVersion( }); } } + return undefined; } async function mergeV2ToV3( @@ -170,53 +201,140 @@ async function mergeV2ToV3( info: LeRobotInfo, episodes: EpisodeMetadata[], adapter: ExportAdapter, + taskPlan: ExportTaskPlan, onProgress?: (p: ExportProgress) => void, signal?: AbortSignal, options?: ExportDataOptions, -): Promise { - const chunksSize = info.chunks_size ?? CHUNK_SIZE_DEFAULT; - // Group exported episodes by NEW 0-based position into chunks. This guarantees - // the output layout is self-contained regardless of the source episode_index - // values. - const chunkToIndices = new Map(); - for (let i = 0; i < episodes.length; i++) { - const chunkIdx = Math.floor(i / chunksSize); - if (!chunkToIndices.has(chunkIdx)) chunkToIndices.set(chunkIdx, []); - chunkToIndices.get(chunkIdx)!.push(i); +): Promise { + return writeV3DataFiles( + dataLoader, + info, + episodes, + adapter, + taskPlan, + onProgress, + signal, + options, + ); +} + +function positiveIntegerOrDefault(value: unknown, fallback: number, field: string): number { + if (value === undefined) return fallback; + if (!Number.isSafeInteger(value) || Number(value) <= 0) { + throw new Error(`${field} must be a positive integer`); + } + return Number(value); +} + +function positiveNumberOrDefault(value: unknown, fallback: number, field: string): number { + if (value === undefined) return fallback; + if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) { + throw new Error(`${field} must be a positive number`); } - let done = 0; + return value; +} + +/** + * Write whole episodes into size-bounded v3 Parquet files. + * + * Official LeRobot estimates the next episode's uncompressed contribution from + * the current file's average bytes/frame before rotating. In the browser we + * already have the complete Arrow tables, so serializing the candidate file + * gives a deterministic and more accurate equivalent. Episodes remain atomic; + * an individual oversized episode is written as one oversized file. + */ +async function writeV3DataFiles( + dataLoader: LeRobotDataLoader, + info: LeRobotInfo, + episodes: EpisodeMetadata[], + adapter: ExportAdapter, + taskPlan: ExportTaskPlan, + onProgress?: (p: ExportProgress) => void, + signal?: AbortSignal, + options?: ExportDataOptions, +): Promise { + const chunksSize = positiveIntegerOrDefault(info.chunks_size, CHUNK_SIZE_DEFAULT, 'chunks_size'); + const sizeLimitBytes = + positiveNumberOrDefault( + (info as { data_files_size_in_mb?: number }).data_files_size_in_mb, + DATA_FILE_SIZE_MB_DEFAULT, + 'data_files_size_in_mb', + ) * BYTES_PER_MB; + const locations: V3DataLayout['episodes'] = new Map(); + let chunkIndex = 0; + let fileIndex = 0; let globalRow = 0; - for (const [chunkIdx, indices] of chunkToIndices.entries()) { + let fileTables: Table[] = []; + let fileBytes: Uint8Array | undefined; + let totalFiles = 0; + + const flush = async (): Promise => { + if (fileTables.length === 0 || !fileBytes) return; + const outPath = `data/chunk-${String(chunkIndex).padStart(3, '0')}/file-${String(fileIndex).padStart(3, '0')}.parquet`; + await ensureParentDir(adapter, outPath); + await adapter.writeFile(outPath, fileBytes); + totalFiles++; + fileTables = []; + fileBytes = undefined; + }; + + const advanceFile = (): void => { + if (fileIndex + 1 >= chunksSize) { + chunkIndex++; + fileIndex = 0; + } else { + fileIndex++; + } + }; + + for (let outputEpisodeIndex = 0; outputEpisodeIndex < episodes.length; outputEpisodeIndex++) { if (signal?.aborted) throw new DOMException('Export cancelled', 'AbortError'); - const tables: Table[] = []; - for (const i of indices) { - const ep = episodes[i]; - const { table } = await dataLoader.getEpisodeTableForExport(ep.episode_index); - const stripped = dropColumns(table, options?.excludeColumns); - const reindexed = rewriteEpisodeAndGlobalIndex(stripped, i, globalRow); - globalRow += stripped.numRows; - tables.push(reindexed); + const episode = episodes[outputEpisodeIndex]; + const sourceTable = await getValidatedEpisodeTable(dataLoader, episode); + const stripped = dropColumns(sourceTable, options?.excludeColumns); + const table = rewriteEpisodeAndGlobalIndex(stripped, outputEpisodeIndex, globalRow, taskPlan); + let candidateTables = [...fileTables, table]; + let candidateBytes = await tableToParquetBytes(concatTables(candidateTables)); + + // Match the official >= boundary, but never emit an empty file. + if (fileTables.length > 0 && candidateBytes.byteLength >= sizeLimitBytes) { + await flush(); + advanceFile(); + candidateTables = [table]; + candidateBytes = await tableToParquetBytes(table); } - const merged = concatTables(tables); - const bytes = await tableToParquetBytes(merged); - const outPath = `data/chunk-${String(chunkIdx).padStart(3, '0')}/file-000.parquet`; - await ensureParentDir(adapter, outPath); - adapter.writeFile(outPath, bytes); - done += indices.length; + + locations.set(episode.episode_index, { + chunk_index: chunkIndex, + file_index: fileIndex, + dataset_from_index: globalRow, + dataset_to_index: globalRow + table.numRows, + }); + globalRow += table.numRows; + fileTables = candidateTables; + fileBytes = candidateBytes; onProgress?.({ phase: 'data', - current: done, + current: outputEpisodeIndex + 1, total: episodes.length, - message: `Data chunk ${chunkIdx}`, + message: `Data chunk ${chunkIndex}, file ${fileIndex}`, cancelable: false, }); } + await flush(); + + return { + episodes: locations, + total_files: totalFiles, + total_chunks: totalFiles === 0 ? 0 : chunkIndex + 1, + }; } async function splitV3ToV2( dataLoader: LeRobotDataLoader, episodes: EpisodeMetadata[], adapter: ExportAdapter, + taskPlan: ExportTaskPlan, onProgress?: (p: ExportProgress) => void, signal?: AbortSignal, options?: ExportDataOptions, @@ -227,9 +345,9 @@ async function splitV3ToV2( if (signal?.aborted) throw new DOMException('Export cancelled', 'AbortError'); const ep = episodes[i]; if (!isV3Metadata(ep)) continue; - const { table } = await dataLoader.getEpisodeTableForExport(ep.episode_index); + const table = await getValidatedEpisodeTable(dataLoader, ep); const stripped = dropColumns(table, options?.excludeColumns); - const reindexed = rewriteEpisodeAndGlobalIndex(stripped, i, globalRow); + const reindexed = rewriteEpisodeAndGlobalIndex(stripped, i, globalRow, taskPlan); globalRow += stripped.numRows; const chunkIdx = Math.floor(i / chunksSize); const outPath = `data/chunk-${String(chunkIdx).padStart(3, '0')}/episode_${String(i).padStart(6, '0')}.parquet`; diff --git a/src/platform/export/ExportService.ts b/src/platform/export/ExportService.ts index 03d30ce..771884f 100644 --- a/src/platform/export/ExportService.ts +++ b/src/platform/export/ExportService.ts @@ -1,8 +1,14 @@ import type { LeRobotDataLoader } from '../services/LeRobotDataLoader'; import type { LeRobotInfo, EpisodeMetadata } from '@/core'; import type { ExportAdapter } from '@/core'; -import type { ExportOptions, TargetVersion, ExportProgress, EpisodeVideoOffsets } from '@/core'; -import { writeMetadata } from './MetadataExporter'; +import type { + ExportOptions, + TargetVersion, + ExportProgress, + EpisodeVideoOffsets, + V3DataLayout, +} from '@/core'; +import { validateMetadataForExport, writeMetadata } from './MetadataExporter'; import { exportDataFiles } from './DataProcessor'; import { exportVideosByTarget } from './VideoExporter'; import { @@ -10,8 +16,14 @@ import { getImageFeatureKeys, rewriteFeaturesForImageToVideo, } from './ImageVideoExporter'; -import { computeDatasetStats, type DatasetStats } from '@/core'; +import { + classifyLeRobotVersion, + computeDatasetStats, + isSupportedLeRobotVersion, + type DatasetStats, +} from '@/core'; import { computeSplits, splitsIndicesToInfoSplits } from '@/core'; +import { buildExportTaskPlan, resolveExportTaskIndex } from './TaskPlan'; export class ExportService { private dataLoader: LeRobotDataLoader; @@ -21,14 +33,38 @@ export class ExportService { this.adapter = adapter; } + private assertExportAllowed(info: LeRobotInfo, targetVersion?: string): void { + const infoCapability = classifyLeRobotVersion(info.codebase_version); + const loaderCapability = this.dataLoader.getVersionCapability(); + if (loaderCapability.status !== 'supported') { + throw new Error( + `Export is disabled for loaded LeRobot ${String(loaderCapability.normalizedVersion)} (${loaderCapability.status})`, + ); + } + if (infoCapability.status !== 'supported') { + throw new Error( + `Export is disabled for LeRobot ${String(info.codebase_version)} (${infoCapability.status})`, + ); + } + if (loaderCapability.normalizedVersion !== infoCapability.normalizedVersion) { + throw new Error( + `Export info version ${String(infoCapability.normalizedVersion)} does not match loaded version ${String(loaderCapability.normalizedVersion)}`, + ); + } + if (targetVersion !== undefined && !isSupportedLeRobotVersion(targetVersion)) { + throw new Error(`Unsupported LeRobot export target: ${targetVersion}`); + } + } + async exportMetadataOnly( info: LeRobotInfo, episodes: EpisodeMetadata[], tasks: Record, options: Pick, ): Promise { - this.adapter.clear(); const targetVersion = options.targetVersion as TargetVersion | undefined; + this.assertExportAllowed(info, targetVersion); + this.adapter.clear(); const splits = options.splitsConfig && episodes.length > 0 ? splitsIndicesToInfoSplits(computeSplits(episodes, options.splitsConfig)) @@ -96,10 +132,11 @@ export class ExportService { }; throwIfAborted(); - this.adapter.clear(); + this.assertExportAllowed(info, options.targetVersion); const targetVersion = (options.targetVersion ?? 'v2.1') as TargetVersion; - let episodesForMeta = episodes; + const episodesForMeta = episodes; let videoOffsets: EpisodeVideoOffsets | null = null; + let dataLayout: V3DataLayout | undefined; const videoOptions = { signal }; // If source has dtype: 'image' features, re-encode them into MP4 videos @@ -107,10 +144,66 @@ export class ExportService { // The image columns are stripped from the exported parquet so the output // dataset does not duplicate the image payload. const imageFeatureKeys = options.includeVideos ? getImageFeatureKeys(info) : []; - let infoForExport = info; + const infoForExport = + imageFeatureKeys.length > 0 ? rewriteFeaturesForImageToVideo(info, imageFeatureKeys) : info; const columnsToExclude = new Set(imageFeatureKeys); let imageOffsets: EpisodeVideoOffsets | null = null; + const taskPlan = buildExportTaskPlan(episodesForMeta, tasks); + const splits = + options.splitsConfig && episodesForMeta.length > 0 + ? splitsIndicesToInfoSplits(computeSplits(episodesForMeta, options.splitsConfig)) + : undefined; + let stats: DatasetStats | undefined; + if (options.includeData && episodesForMeta.length > 0) { + reportPhaseStart(1, 'Validating training statistics...', 'metadata'); + stats = await computeDatasetStats(this.dataLoader, infoForExport, episodesForMeta, { + signal, + resolveNumericRow: (featureKey, context) => { + if (featureKey === 'index') return context.outputGlobalIndex; + if (featureKey === 'episode_index') return context.outputEpisodeIndex; + if (featureKey !== 'task_index') return undefined; + const sourceTaskIndex = context.sourceValues[0]; + if (!Number.isSafeInteger(sourceTaskIndex)) { + throw new Error( + `Cannot map non-integer frame task_index ${String(sourceTaskIndex)} ` + + `for exported episode ${context.outputEpisodeIndex}`, + ); + } + const targetTaskIndex = resolveExportTaskIndex( + taskPlan, + context.outputEpisodeIndex, + sourceTaskIndex, + ); + if (targetTaskIndex === undefined) { + throw new Error( + `Cannot map frame task_index ${sourceTaskIndex} ` + + `for exported episode ${context.outputEpisodeIndex}`, + ); + } + return targetTaskIndex; + }, + onProgress: (current, total) => { + const ratio = total > 0 ? current / total : 0; + onProg?.({ + phase: 'metadata', + current, + total, + message: 'Validating training statistics...', + cancelable: true, + percent: 1 + ratio * 2, + }); + }, + }); + throwIfAborted(); + } + validateMetadataForExport(infoForExport, episodesForMeta, targetVersion, splits); + throwIfAborted(); + + // All training and metadata validation has succeeded. Mutating the target + // is now safe: subsequent failures are I/O/encoding failures, not bad input. + this.adapter.clear(); + if (imageFeatureKeys.length > 0) { reportPhaseStart(3, 'Encoding image features as MP4...', 'videos'); throwIfAborted(); @@ -124,7 +217,6 @@ export class ExportService { { signal }, ); imageOffsets = offsets; - infoForExport = rewriteFeaturesForImageToVideo(info, imageFeatureKeys); } if (options.includeVideos && targetVersion === 'v3.0') { @@ -146,7 +238,6 @@ export class ExportService { Object.assign(videoOffsets.get(epIdx)!, entry); } } - episodesForMeta = episodes; } else if (options.includeVideos && targetVersion === 'v2.1') { reportPhaseStart(30, 'Preparing video export (v2.1)...', 'videos'); throwIfAborted(); @@ -163,63 +254,40 @@ export class ExportService { throwIfAborted(); - let stats: DatasetStats | undefined; - if (options.includeData && episodesForMeta.length > 0) { - reportPhaseStart(50, 'Computing dataset stats...', 'metadata'); + if (options.includeData) { + reportPhaseStart(53, 'Exporting data (Parquet)...', 'data'); throwIfAborted(); - stats = await computeDatasetStats(this.dataLoader, info, episodesForMeta, { + dataLayout = await exportDataFiles( + this.dataLoader, + infoForExport, + episodesForMeta, + targetVersion, + this.adapter, + onProg ? wrap(53, 86) : undefined, signal, - onProgress: (current, total) => { - const ratio = total > 0 ? current / total : 0; - onProg?.({ - phase: 'metadata', - current, - total, - message: 'Computing stats...', - cancelable: true, - percent: 50 + ratio * 3, - }); + { + ...(columnsToExclude.size > 0 ? { excludeColumns: columnsToExclude } : {}), + tasks, + taskPlan, }, - }); - throwIfAborted(); - } - - let splits: Record | undefined; - if (options.splitsConfig && episodesForMeta.length > 0) { - const indices = computeSplits(episodesForMeta, options.splitsConfig); - splits = splitsIndicesToInfoSplits(indices); + ); } - - reportPhaseStart(53, 'Writing metadata...', 'metadata'); throwIfAborted(); + reportPhaseStart(86, 'Writing metadata...', 'metadata'); await writeMetadata( infoForExport, episodesForMeta, tasks, targetVersion, this.adapter, - onProg ? wrap(53, 55) : undefined, + onProg ? wrap(86, 88) : undefined, videoOffsets ?? undefined, signal, stats, splits, + dataLayout, ); throwIfAborted(); - if (options.includeData) { - reportPhaseStart(55, 'Exporting data (Parquet)...', 'data'); - throwIfAborted(); - await exportDataFiles( - this.dataLoader, - infoForExport, - episodesForMeta, - targetVersion, - this.adapter, - onProg ? wrap(55, 88) : undefined, - signal, - columnsToExclude.size > 0 ? { excludeColumns: columnsToExclude } : undefined, - ); - } - throwIfAborted(); onProg?.({ phase: 'packaging', current: 1, diff --git a/src/platform/export/MetadataExporter.ts b/src/platform/export/MetadataExporter.ts index fc6ec76..7a6a359 100644 --- a/src/platform/export/MetadataExporter.ts +++ b/src/platform/export/MetadataExporter.ts @@ -1,12 +1,195 @@ import * as arrow from 'apache-arrow'; import type { LeRobotInfo, EpisodeMetadata } from '@/core'; import type { ExportAdapter } from '@/core'; -import type { ExportProgress, EpisodeVideoOffsets } from '@/core'; +import type { ExportProgress, EpisodeVideoOffsets, V3DataLayout } from '@/core'; import type { DatasetStats } from '@/core'; +import { isV3Info } from '../../core/types/lerobot'; +import { buildExportTaskPlan, type ExportTaskPlan } from './TaskPlan'; const TEXT_ENCODER = new TextEncoder(); const V3_DATA_PATH = 'data/chunk-{chunk_index:03d}/file-{file_index:03d}.parquet'; const V3_VIDEO_PATH = 'videos/{video_key}/chunk-{chunk_index:03d}/file-{file_index:03d}.mp4'; +const V3_CHUNKS_SIZE_DEFAULT = 1000; +const V3_DATA_FILE_SIZE_MB_DEFAULT = 100; +const V3_VIDEO_FILE_SIZE_MB_DEFAULT = 200; + +type SupportedEpisodeScalar = string | number | boolean; + +function getV3RewrittenColumns(info?: LeRobotInfo): Set { + const columns = new Set([ + 'episode_index', + 'length', + 'tasks', + 'task_index', + 'dataset_from_index', + 'dataset_to_index', + 'chunk_index', + 'file_index', + 'meta/episodes/chunk_index', + 'meta/episodes/file_index', + 'data/chunk_index', + 'data/file_index', + ]); + for (const [key, feature] of Object.entries(info?.features ?? {})) { + if (feature?.dtype !== 'video') continue; + columns.add(`videos/${key}/chunk_index`); + columns.add(`videos/${key}/file_index`); + columns.add(`videos/${key}/from_timestamp`); + columns.add(`videos/${key}/to_timestamp`); + } + return columns; +} + +function validateEpisodeScalar(column: string, value: unknown): SupportedEpisodeScalar { + if (typeof value === 'number') { + if (!Number.isFinite(value) || (Number.isInteger(value) && !Number.isSafeInteger(value))) { + throw new Error( + `Cannot export v3 episode metadata column "${column}": number ${String(value)} is not safely serializable.`, + ); + } + return value; + } + if (typeof value === 'string' || typeof value === 'boolean') return value; + throw new Error( + `Cannot export v3 episode metadata column "${column}": unsupported value type ${Object.prototype.toString.call(value)}.`, + ); +} + +function validatePreservedEpisodeColumn(column: string, values: unknown[]): void { + let columnKind: 'scalar' | 'array' | undefined; + let scalarKind: 'string' | 'number' | 'boolean' | undefined; + let hasTypedValue = false; + + for (const value of values) { + if (value == null) continue; + const kind = Array.isArray(value) ? 'array' : 'scalar'; + if (columnKind !== undefined && kind !== columnKind) { + throw new Error( + `Cannot export v3 episode metadata column "${column}": scalar and array values cannot share one Parquet column.`, + ); + } + columnKind = kind; + const items: unknown[] = []; + const collectItems = (item: unknown): void => { + if (Array.isArray(item)) item.forEach(collectItems); + else items.push(item); + }; + collectItems(value); + for (const item of items) { + if (item == null) continue; + const scalar = validateEpisodeScalar(column, item); + const itemKind: 'string' | 'number' | 'boolean' = + typeof scalar === 'string' ? 'string' : typeof scalar === 'number' ? 'number' : 'boolean'; + if (scalarKind !== undefined && itemKind !== scalarKind) { + throw new Error( + `Cannot export v3 episode metadata column "${column}": mixed ${scalarKind} and ${itemKind} values are not supported.`, + ); + } + scalarKind = itemKind; + hasTypedValue = true; + } + } + + if (!hasTypedValue) { + throw new Error( + `Cannot export v3 episode metadata column "${column}": its type cannot be inferred from only null or empty values.`, + ); + } +} + +function collectPreservedV3EpisodeColumns( + episodes: EpisodeMetadata[], + rewrittenColumns: Set, +): Record { + const preserved: Record = {}; + const extraKeys = new Set(episodes.flatMap((episode) => Object.keys(episode))); + for (const key of extraKeys) { + if (rewrittenColumns.has(key)) continue; + const values = episodes.map((episode) => (episode as Record)[key] ?? null); + validatePreservedEpisodeColumn(key, values); + preserved[key] = values; + } + return preserved; +} + +function createPreservedEpisodeVector(values: unknown[]): arrow.Vector { + const nonNullValue = values.find((value) => value != null); + const isArray = Array.isArray(nonNullValue); + const items: unknown[] = []; + const collectItems = (value: unknown): void => { + if (value == null) return; + if (Array.isArray(value)) value.forEach(collectItems); + else items.push(value); + }; + values.forEach(collectItems); + const sample = items[0] as SupportedEpisodeScalar; + let itemType: arrow.DataType; + let normalizedValues = values; + + if (typeof sample === 'string') { + itemType = new arrow.Utf8(); + } else if (typeof sample === 'boolean') { + itemType = new arrow.Bool(); + } else { + const integerValues = items.every((item) => typeof item === 'number' && Number.isInteger(item)); + itemType = integerValues ? new arrow.Int64() : new arrow.Float64(); + if (integerValues) { + const normalizeInteger = (value: unknown): unknown => + Array.isArray(value) + ? value.map(normalizeInteger) + : value == null + ? null + : BigInt(value as number); + normalizedValues = values.map(normalizeInteger); + } + } + + let type = itemType; + if (isArray) { + let current: unknown = nonNullValue; + while (Array.isArray(current)) { + type = new arrow.List(new arrow.Field('item', type, true)); + current = current[0]; + } + } + return arrow.vectorFromArray(normalizedValues, type); +} + +function validateV3Splits(splits: Record | undefined, episodeCount: number): void { + for (const [name, range] of Object.entries(splits ?? {})) { + const match = /^(\d+):(\d+)$/.exec(range); + if (!match) { + throw new Error( + `Cannot export split "${name}": LeRobot v3 requires one continuous start:end range, got "${range}".`, + ); + } + const start = Number(match[1]); + const end = Number(match[2]); + if ( + !Number.isSafeInteger(start) || + !Number.isSafeInteger(end) || + start >= end || + end > episodeCount + ) { + throw new Error( + `Cannot export split "${name}": range "${range}" is outside the ${episodeCount} exported episodes.`, + ); + } + } +} + +/** Validate all metadata that can fail before an export writes any files. */ +export function validateMetadataForExport( + info: LeRobotInfo, + episodes: EpisodeMetadata[], + targetVersion: 'v2.1' | 'v3.0' | undefined, + splits?: Record, +): void { + const isTargetV3 = targetVersion !== undefined ? targetVersion === 'v3.0' : isV3Info(info); + if (!isTargetV3) return; + collectPreservedV3EpisodeColumns(episodes, getV3RewrittenColumns(info)); + validateV3Splits(splits, episodes.length); +} export async function writeMetadata( info: LeRobotInfo, @@ -19,27 +202,38 @@ export async function writeMetadata( signal?: AbortSignal, stats?: DatasetStats, splits?: Record, + dataLayout?: V3DataLayout, ): Promise { if (signal?.aborted) throw new DOMException('Export cancelled', 'AbortError'); - const isTargetV3 = targetVersion?.startsWith('v3') ?? info.codebase_version.startsWith('v3'); + const isTargetV3 = targetVersion !== undefined ? targetVersion === 'v3.0' : isV3Info(info); + const taskPlan = buildExportTaskPlan(episodes, tasks); const { total_videos: legacyTotalVideos, ...officialInfo } = info; + if (isTargetV3) { + // Validate before writing info.json so unsupported extension metadata + // cannot leave a deceptively successful partial export. + validateMetadataForExport(info, episodes, targetVersion, splits); + } const infoToWrite = { ...officialInfo, ...(!isTargetV3 && legacyTotalVideos !== undefined ? { total_videos: legacyTotalVideos } : {}), codebase_version: (targetVersion ?? info.codebase_version) as LeRobotInfo['codebase_version'], - ...(isTargetV3 ? { data_path: V3_DATA_PATH, video_path: V3_VIDEO_PATH } : {}), + ...(isTargetV3 + ? { + chunks_size: info.chunks_size ?? V3_CHUNKS_SIZE_DEFAULT, + data_files_size_in_mb: + (info as { data_files_size_in_mb?: number }).data_files_size_in_mb ?? + V3_DATA_FILE_SIZE_MB_DEFAULT, + video_files_size_in_mb: + (info as { video_files_size_in_mb?: number }).video_files_size_in_mb ?? + V3_VIDEO_FILE_SIZE_MB_DEFAULT, + data_path: V3_DATA_PATH, + video_path: V3_VIDEO_PATH, + } + : {}), total_episodes: episodes.length, total_frames: episodes.reduce((total, episode) => total + episode.length, 0), - total_tasks: new Set( - episodes.flatMap((episode) => - (episode.tasks ?? []).length > 0 - ? (episode.tasks ?? []) - : episode.task_index !== undefined - ? [tasks[episode.task_index] ?? String(episode.task_index)] - : [], - ), - ).size, + total_tasks: Object.keys(taskPlan.tasks).length, ...(isTargetV3 && splits != null && Object.keys(splits).length > 0 ? { splits } : {}), } as LeRobotInfo; @@ -50,12 +244,21 @@ export async function writeMetadata( } if (isTargetV3) { - await writeV3Episodes(episodes, adapter, onProgress, info, videoOffsetsByEpisode, signal); + await writeV3Episodes( + episodes, + taskPlan, + adapter, + onProgress, + info, + videoOffsetsByEpisode, + signal, + dataLayout, + ); if (signal?.aborted) throw new DOMException('Export cancelled', 'AbortError'); - await writeV3Tasks(tasks, adapter, onProgress, signal); + await writeV3Tasks(taskPlan.tasks, adapter, onProgress, signal); } else { - writeV2Episodes(episodes, adapter); - writeV2Tasks(tasks, adapter); + writeV2Episodes(episodes, taskPlan, adapter); + writeV2Tasks(taskPlan.tasks, adapter); } if (stats != null && Object.keys(stats).length > 0) { @@ -63,9 +266,25 @@ export async function writeMetadata( } } -function writeV2Episodes(episodes: EpisodeMetadata[], adapter: ExportAdapter): void { +function writeV2Episodes( + episodes: EpisodeMetadata[], + taskPlan: ExportTaskPlan, + adapter: ExportAdapter, +): void { // Export uses 0-based contiguous episode_index so the exported dataset is self-contained. - const lines = episodes.map((ep, i) => JSON.stringify({ ...ep, episode_index: i })).join('\n'); + const lines = episodes + .map((ep, i) => { + const taskIndices = taskPlan.episodeTaskIndices[i]; + const { task_index: _sourceTaskIndex, ...preservedEpisode } = ep; + void _sourceTaskIndex; + return JSON.stringify({ + ...preservedEpisode, + episode_index: i, + tasks: taskIndices.map((index) => taskPlan.tasks[index]), + ...(taskIndices.length === 1 ? { task_index: taskIndices[0] } : {}), + }); + }) + .join('\n'); adapter.writeFile('meta/episodes.jsonl', TEXT_ENCODER.encode(lines)); } @@ -80,11 +299,13 @@ function writeV2Tasks(tasks: Record, adapter: ExportAdapter): vo async function writeV3Episodes( episodes: EpisodeMetadata[], + taskPlan: ExportTaskPlan, adapter: ExportAdapter, onProgress?: (p: ExportProgress) => void, info?: LeRobotInfo, videoOffsetsByEpisode?: EpisodeVideoOffsets, signal?: AbortSignal, + dataLayout?: V3DataLayout, ): Promise { if (signal?.aborted) throw new DOMException('Export cancelled', 'AbortError'); onProgress?.({ @@ -99,38 +320,58 @@ async function writeV3Episodes( const n = episodes.length; const episodeIndex = episodes.map((_, i) => i); const length = episodes.map((e) => e.length); - const tasksArr = episodes.map((e) => e.tasks ?? []); - const taskIndex = episodes.map((e) => e.task_index ?? 0); + const tasksArr = taskPlan.episodeTaskIndices.map((indices) => + indices.map((index) => taskPlan.tasks[index]), + ); const tableData: Record = { episode_index: episodeIndex, length, tasks: tasksArr, - task_index: taskIndex, }; + Object.assign(tableData, collectPreservedV3EpisodeColumns(episodes, getV3RewrittenColumns(info))); - const chunksSize = info?.chunks_size ?? 1000; - // dataset_from_index / dataset_to_index must point into the *exported* data files (cumulative row offsets). + // dataset_from_index / dataset_to_index are global exported row offsets. let cum = 0; const datasetFromIndex = new Array(n); const datasetToIndex = new Array(n); + const dataLocations = dataLayout + ? episodes.map((episode) => { + const location = dataLayout.episodes.get(episode.episode_index); + if (!location) { + throw new Error(`Missing exported data location for episode ${episode.episode_index}`); + } + return location; + }) + : undefined; for (let i = 0; i < n; i++) { - datasetFromIndex[i] = cum; + const location = dataLocations?.[i]; + if ( + location && + (location.dataset_from_index !== cum || location.dataset_to_index !== cum + length[i]) + ) { + throw new Error( + `Exported data row range for episode ${episodes[i].episode_index} does not match its length`, + ); + } + datasetFromIndex[i] = location?.dataset_from_index ?? cum; cum += length[i]; - datasetToIndex[i] = cum; + datasetToIndex[i] = location?.dataset_to_index ?? cum; } tableData.dataset_from_index = datasetFromIndex; tableData.dataset_to_index = datasetToIndex; - // Data path: one file per chunk (file-000.parquet). v3Adapter uses data/chunk_index, data/file_index when present. - tableData['data/chunk_index'] = episodes.map((_, i) => Math.floor(i / chunksSize)); - tableData['data/file_index'] = episodes.map(() => 0); + // References must match the actual size-rolled data shards. Metadata-only + // exports have no produced layout and retain a single logical shard. + tableData['data/chunk_index'] = episodes.map( + (_, index) => dataLocations?.[index].chunk_index ?? 0, + ); + tableData['data/file_index'] = episodes.map((_, index) => dataLocations?.[index].file_index ?? 0); - // Top-level chunk_index/file_index reflect the DATA layout, not video. - // Previously this used the first video key, which was wrong for multi-camera - // datasets where different cameras may live in different chunks. - tableData.chunk_index = (tableData['data/chunk_index'] as number[]).slice(); - tableData.file_index = (tableData['data/file_index'] as number[]).slice(); + // These columns locate each row's episode-metadata parquet shard. This + // exporter currently emits one shard, so every row points to chunk/file 0. + tableData['meta/episodes/chunk_index'] = episodes.map(() => 0); + tableData['meta/episodes/file_index'] = episodes.map(() => 0); const videoKeys = info ? Object.entries(info.features) @@ -157,11 +398,10 @@ async function writeV3Episodes( const integerColumns = new Set([ 'episode_index', 'length', - 'task_index', 'dataset_from_index', 'dataset_to_index', - 'chunk_index', - 'file_index', + 'meta/episodes/chunk_index', + 'meta/episodes/file_index', 'data/chunk_index', 'data/file_index', ]); @@ -182,7 +422,15 @@ async function writeV3Episodes( ), ]; } - return [name, arrow.vectorFromArray(values)]; + try { + return [name, createPreservedEpisodeVector(values)]; + } catch (error) { + throw new Error( + `Cannot serialize v3 episode metadata column "${name}" to Arrow: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + } }), ); const table = new arrow.Table({ @@ -227,10 +475,52 @@ async function writeV3Tasks( // Official v3 also has meta/tasks.parquet (task_index + task columns for Python/pandas compatibility) if (sorted.length > 0) { - const task_index = sorted.map(([idx]) => Number(idx)); + const task_index = arrow.vectorFromArray( + sorted.map(([idx]) => BigInt(Number(idx))), + new arrow.Int64(), + ); const task = sorted.map(([, desc]) => String(desc ?? '')); - const tableData: Record = { task_index, task }; - const table = arrow.tableFromArrays(tableData); + const baseTable = new arrow.Table({ task_index, task: arrow.vectorFromArray(task) }); + // Official LeRobot writes tasks from a pandas DataFrame whose string + // `task` index maps each row to its numeric task_index. A plain Arrow + // column is not enough: pandas otherwise restores a RangeIndex and the + // official reader returns "0", "1", ... as task labels. + const pandasMetadata = JSON.stringify({ + index_columns: ['task'], + column_indexes: [ + { + name: null, + field_name: null, + pandas_type: 'unicode', + numpy_type: 'object', + metadata: { encoding: 'UTF-8' }, + }, + ], + columns: [ + { + name: 'task_index', + field_name: 'task_index', + pandas_type: 'int64', + numpy_type: 'int64', + metadata: null, + }, + { + name: 'task', + field_name: 'task', + pandas_type: 'unicode', + numpy_type: 'object', + metadata: null, + }, + ], + attributes: {}, + creator: { library: 'lerobot-studio', version: '1.0.0' }, + pandas_version: '2.3.3', + }); + const schema = new arrow.Schema( + baseTable.schema.fields, + new Map([...baseTable.schema.metadata, ['pandas', pandasMetadata]]), + ); + const table = new arrow.Table(schema, baseTable.batches); const wasmModule = await import('./parquetWasmLoader').then((m) => m.getParquetWasm()); const ipcBytes = arrow.tableToIPC(table, 'stream'); const ipcCopy = new Uint8Array(ipcBytes.length); diff --git a/src/platform/export/TaskPlan.ts b/src/platform/export/TaskPlan.ts new file mode 100644 index 0000000..d79a12b --- /dev/null +++ b/src/platform/export/TaskPlan.ts @@ -0,0 +1,80 @@ +import type { EpisodeMetadata } from '@/core'; + +export interface ExportTaskPlan { + tasks: Record; + episodeTaskIndices: number[][]; + episodeSourceIndexToTargetIndex: Array>; +} + +/** + * Resolve one frame's final task_index from the canonical export task plan. + * Single-task episodes intentionally ignore stale source frame indices. + */ +export function resolveExportTaskIndex( + taskPlan: ExportTaskPlan, + episodePosition: number, + sourceTaskIndex: number, +): number | undefined { + const episodeTargets = taskPlan.episodeTaskIndices[episodePosition] ?? []; + if (episodeTargets.length === 1) return episodeTargets[0]; + return taskPlan.episodeSourceIndexToTargetIndex[episodePosition]?.get(sourceTaskIndex); +} + +function normalizedLabels(episode: EpisodeMetadata, sourceTasks: Record): string[] { + const labels = Array.isArray(episode.tasks) + ? episode.tasks + .map(String) + .map((task) => task.trim()) + .filter(Boolean) + : []; + if (labels.length > 0) return Array.from(new Set(labels)); + + if (episode.task_index != null) { + const label = sourceTasks[Number(episode.task_index)]; + if (label != null && String(label).trim() !== '') return [String(label).trim()]; + } + const onlySourceTask = Object.values(sourceTasks) + .map((task) => String(task).trim()) + .filter(Boolean); + if (onlySourceTask.length === 1) return onlySourceTask; + return []; +} + +/** Build one canonical task mapping shared by frames, episodes and tasks metadata. */ +export function buildExportTaskPlan( + episodes: EpisodeMetadata[], + sourceTasks: Record, +): ExportTaskPlan { + const labelToTargetIndex = new Map(); + const tasks: Record = {}; + const episodeTaskIndices = episodes.map((episode) => + normalizedLabels(episode, sourceTasks).map((label) => { + let index = labelToTargetIndex.get(label); + if (index === undefined) { + index = labelToTargetIndex.size; + labelToTargetIndex.set(label, index); + tasks[index] = label; + } + return index; + }), + ); + + const episodeSourceIndexToTargetIndex = episodes.map((_, position) => { + const mapping = new Map(); + const allowedTargets = new Set(episodeTaskIndices[position]); + for (const [rawIndex, rawLabel] of Object.entries(sourceTasks)) { + const sourceIndex = Number(rawIndex); + const targetIndex = labelToTargetIndex.get(String(rawLabel).trim()); + if ( + Number.isSafeInteger(sourceIndex) && + targetIndex !== undefined && + allowedTargets.has(targetIndex) + ) { + mapping.set(sourceIndex, targetIndex); + } + } + return mapping; + }); + + return { tasks, episodeTaskIndices, episodeSourceIndexToTargetIndex }; +} diff --git a/src/platform/export/VideoExporter.ts b/src/platform/export/VideoExporter.ts index 50aeb21..893f1f6 100644 --- a/src/platform/export/VideoExporter.ts +++ b/src/platform/export/VideoExporter.ts @@ -5,6 +5,7 @@ import type { ExportAdapter } from '@/core'; import type { ExportProgress, EpisodeVideoOffsets, TargetVersion } from '@/core'; const CHUNK_SIZE_DEFAULT = 1000; +const VIDEO_FILE_SIZE_MB_DEFAULT = 200; export type VideoExportOptions = { signal?: AbortSignal }; @@ -190,21 +191,19 @@ async function exportVideosTranscodeToV3( if (videoKeys.length === 0) return new Map(); const chunksSize = info.chunks_size ?? CHUNK_SIZE_DEFAULT; - - // Group by NEW 0-based position so the output layout is self-contained. - const chunkToIndices = new Map(); - for (let i = 0; i < episodes.length; i++) { - const c = Math.floor(i / chunksSize); - if (!chunkToIndices.has(c)) chunkToIndices.set(c, []); - chunkToIndices.get(c)!.push(i); + if (!Number.isSafeInteger(chunksSize) || chunksSize <= 0) { + throw new Error('chunks_size must be a positive integer'); + } + const videoSizeLimit = + (info as { video_files_size_in_mb?: number }).video_files_size_in_mb ?? + VIDEO_FILE_SIZE_MB_DEFAULT; + if (!Number.isFinite(videoSizeLimit) || videoSizeLimit <= 0) { + throw new Error('video_files_size_in_mb must be a positive number'); } const offsets: EpisodeVideoOffsets = new Map(); let done = 0; - const total = [...chunkToIndices.values()].reduce( - (sum, eps) => sum + eps.length * videoKeys.length, - 0, - ); + const total = episodes.length * videoKeys.length; onProgress?.({ phase: 'videos', current: 0, @@ -213,76 +212,85 @@ async function exportVideosTranscodeToV3( cancelable: false, }); - for (const [chunkIdx, indices] of chunkToIndices.entries()) { - for (const key of videoKeys) { - await ensureParentDir(adapter, `videos/${key}/chunk-${String(chunkIdx).padStart(3, '0')}`); - for (let position = 0; position < indices.length; position++) { - assertNotAborted(signal); - const newIdx = indices[position]; - const ep = episodes[newIdx]; - const pathResult = dataLoader.getEpisodeVideoPath(ep.episode_index, key); - const outPath = `videos/${key}/chunk-${String(chunkIdx).padStart(3, '0')}/file-${String(position).padStart(3, '0')}.mp4`; + for (const key of videoKeys) { + // Official LeRobot concatenates episode MP4s while the current file stays + // below video_files_size_in_mb. Mediabunny cannot remux-concatenate + // independently encoded MP4s without a lossy transcode. Preserve every + // episode losslessly as its own file instead, and apply chunks_size to the + // number of files (not the number of episodes as a format concept). + let chunkIdx = 0; + let fileIdx = 0; + for (let newIdx = 0; newIdx < episodes.length; newIdx++) { + assertNotAborted(signal); + const ep = episodes[newIdx]; + const pathResult = dataLoader.getEpisodeVideoPath(ep.episode_index, key); + const outPath = `videos/${key}/chunk-${String(chunkIdx).padStart(3, '0')}/file-${String(fileIdx).padStart(3, '0')}.mp4`; - if (!pathResult) { - throw new Error(`Missing source video for episode ${ep.episode_index}, feature "${key}"`); - } + if (!pathResult) { + throw new Error(`Missing source video for episode ${ep.episode_index}, feature "${key}"`); + } - try { - const bytes = await dataLoader.readFileBytes(pathResult.path); - const hasTrim = hasMeaningfulTrim(pathResult); - const trim = hasTrim - ? { fromSec: pathResult.fromSec, toSec: pathResult.toSec } - : undefined; + try { + const bytes = await dataLoader.readFileBytes(pathResult.path); + const hasTrim = hasMeaningfulTrim(pathResult); + const trim = hasTrim ? { fromSec: pathResult.fromSec, toSec: pathResult.toSec } : undefined; - // Fast path: source is already an MP4 AND we don't need to trim → - // byte copy. Massively faster and avoids any transcoding failure - // mode for already-compatible sources (v2 MP4 → v3 MP4). - if (!hasTrim && looksLikeMp4(bytes)) { - await adapter.writeFile(outPath, bytes); - const duration = - Number(pathResult.toSec ?? 0) > 0 - ? Number(pathResult.toSec) - : await tryComputeMp4Duration(bytes); + // Fast path: source is already an MP4 AND we don't need to trim → + // byte copy. Massively faster and avoids any transcoding failure + // mode for already-compatible sources (v2 MP4 → v3 MP4). + if (!hasTrim && looksLikeMp4(bytes)) { + await ensureParentDir(adapter, outPath); + await adapter.writeFile(outPath, bytes); + const duration = + Number(pathResult.toSec ?? 0) > 0 + ? Number(pathResult.toSec) + : await tryComputeMp4Duration(bytes); + if (!offsets.has(ep.episode_index)) offsets.set(ep.episode_index, {}); + offsets.get(ep.episode_index)![key] = { + chunk_index: chunkIdx, + file_index: fileIdx, + from_timestamp: 0, + to_timestamp: duration, + }; + } else { + const result = await convertSegmentWithMediabunny(bytes, trim, signal); + if (!result) { + throw new Error(`Video conversion produced no MP4 output: ${outPath}`); + } else { + await ensureParentDir(adapter, outPath); + await adapter.writeFile(outPath, new Uint8Array(result.buffer)); if (!offsets.has(ep.episode_index)) offsets.set(ep.episode_index, {}); offsets.get(ep.episode_index)![key] = { chunk_index: chunkIdx, - file_index: position, + file_index: fileIdx, from_timestamp: 0, - to_timestamp: duration, + to_timestamp: result.duration, }; - } else { - const result = await convertSegmentWithMediabunny(bytes, trim, signal); - if (!result) { - throw new Error(`Video conversion produced no MP4 output: ${outPath}`); - } else { - await adapter.writeFile(outPath, new Uint8Array(result.buffer)); - if (!offsets.has(ep.episode_index)) offsets.set(ep.episode_index, {}); - offsets.get(ep.episode_index)![key] = { - chunk_index: chunkIdx, - file_index: position, - from_timestamp: 0, - to_timestamp: result.duration, - }; - } } - } catch (e) { - if (e instanceof DOMException && e.name === 'AbortError') throw e; - throw new Error( - `Failed to export video "${outPath}": ${e instanceof Error ? e.message : String(e)}`, - { - cause: e, - }, - ); } + } catch (e) { + if (e instanceof DOMException && e.name === 'AbortError') throw e; + throw new Error( + `Failed to export video "${outPath}": ${e instanceof Error ? e.message : String(e)}`, + { + cause: e, + }, + ); + } - done++; - onProgress?.({ - phase: 'videos', - current: done, - total, - message: outPath, - cancelable: false, - }); + done++; + onProgress?.({ + phase: 'videos', + current: done, + total, + message: outPath, + cancelable: false, + }); + if (fileIdx + 1 >= chunksSize) { + chunkIdx++; + fileIdx = 0; + } else { + fileIdx++; } } } diff --git a/src/platform/services/LeRobotDataLoader.ts b/src/platform/services/LeRobotDataLoader.ts index 79d71eb..70b1333 100644 --- a/src/platform/services/LeRobotDataLoader.ts +++ b/src/platform/services/LeRobotDataLoader.ts @@ -9,8 +9,9 @@ import type { EpisodeVideoPathResult, LeRobotVersionAdapter, ValidationReport, + LeRobotVersionCapability, } from '@/core'; -import { getAdapterForVersion, getValidatorForVersion } from '@/core'; +import { classifyLeRobotVersion, getAdapterForVersion, getValidatorForVersion } from '@/core'; import { tableFromIPC, Table } from 'apache-arrow'; import { LRUCache } from '../utils/MediaCache'; import type { DataSource } from '../datasource/dataSources'; @@ -42,6 +43,11 @@ export class LeRobotDataLoader { private fileUrlCache: LRUCache; private disposed = false; private validationReport: ValidationReport | null = null; + private versionCapability: LeRobotVersionCapability = { + status: 'unsupported', + normalizedVersion: null, + adapterVersion: null, + }; // 文件解析缓存 - 避免重复读取和解析同一个文件 private parsedFileCache: ParsedFileCache | null = null; @@ -73,9 +79,14 @@ export class LeRobotDataLoader { } try { await this.worker.clearCache(); - terminateWorker(this.worker); } catch { // ignore + } finally { + try { + terminateWorker(this.worker); + } catch { + // ignore + } } try { await this._dataSource.clear(); @@ -95,8 +106,11 @@ export class LeRobotDataLoader { ); } - if (!this.info.codebase_version) { - throw new Error('Invalid info.json: codebase_version is missing'); + this.versionCapability = classifyLeRobotVersion(this.info.codebase_version); + if (this.versionCapability.status === 'unsupported') { + throw new Error( + `Unsupported LeRobot codebase_version: ${String(this.info.codebase_version ?? 'missing')}`, + ); } this.adapter = getAdapterForVersion(this.info.codebase_version); @@ -120,7 +134,7 @@ export class LeRobotDataLoader { // meta/tasks.parquet. Official Python writers store per-episode labels in the // ``tasks`` list column only — there is often no episode-level ``task_index``. // Defaulting missing task_index to 0 incorrectly showed every episode as task 0. - if (this.adapter.version.startsWith('v3')) { + if (this.versionCapability.adapterVersion === 'v3.0') { this.episodes.forEach((ep) => { if (ep.task_index == null) return; const idx = Number(ep.task_index); @@ -339,6 +353,10 @@ export class LeRobotDataLoader { return this.validationReport; } + getVersionCapability(): LeRobotVersionCapability { + return { ...this.versionCapability }; + } + /** * Returns the Arrow table for a single episode (for export). * When episode is a slice of a shared file (e.g. v3), returns sliced table; otherwise full table. diff --git a/src/platform/workers/wasmUrl.inline.ts b/src/platform/workers/wasmUrl.inline.ts deleted file mode 100644 index 47f94be..0000000 --- a/src/platform/workers/wasmUrl.inline.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as wasmUrl } from 'parquet-wasm/esm/parquet_wasm_bg.wasm?url&inline'; diff --git a/src/platform/workers/workerManager.ts b/src/platform/workers/workerManager.ts index 68f3266..375a9ba 100644 --- a/src/platform/workers/workerManager.ts +++ b/src/platform/workers/workerManager.ts @@ -1,9 +1,8 @@ /** - * Worker 管理器 - 使用 Vite 原生 ?worker&inline 特性 + * Worker 管理器 - 使用 Vite 原生外置 module Worker * - * 由于内联 Worker 运行在 Blob URL 环境,parquet-wasm 无法通过 import.meta.url - * 正确解析 WASM 文件路径("Invalid URL" 错误)。解决方案是在主线程预加载 - * WASM 为 ArrayBuffer,然后通过 postMessage Transfer 传递给 Worker。 + * Worker 使用相对模块 URL,WASM 保留为单份按需 data URL chunk,因此不依赖部署根路径。 + * WASM 由主线程预加载为 ArrayBuffer,再通过 postMessage Transfer 传递给 Worker。 * * Worker 侧使用 Promise 等待(而非轮询),彻底消除竞态条件: * 无论主线程多快/多慢完成 fetch,Worker 都会等到 buffer 到达后再初始化。 @@ -12,13 +11,8 @@ import * as Comlink from 'comlink'; import type { ParquetWorkerAPI, ParquetImageWorkerAPI } from '@/core'; -// SPA build: ?url -> separate .wasm file fetched on demand -// lib build: vite.lib.config.ts aliases this to wasmUrl.inline.ts (?url&inline -> base64 data URL) -import { wasmUrl } from './wasmUrl'; - -// Vite 会自动将 worker 内联为 base64 -import ParquetWorkerConstructor from './parquet.worker?worker&inline'; -import ParquetImageWorkerConstructor from './parquetImage.worker?worker&inline'; +import parquetWorkerUrl from './parquet.worker?worker&url'; +import parquetImageWorkerUrl from './parquetImage.worker?worker&url'; // WASM 预加载缓存(Promise 形式,多个 worker 共享同一份 fetch) let wasmBufferPromise: Promise | null = null; @@ -29,11 +23,12 @@ const workerRegistry = new WeakMap, Worker>(); */ async function loadWasmBuffer(): Promise { if (!wasmBufferPromise) { - wasmBufferPromise = fetch(wasmUrl) + // Library mode places this data URL in its own lazy chunk. Keeping one + // package-local copy is relocatable across Vite, Next, and arbitrary bases. + wasmBufferPromise = import('./wasmUrl') + .then(({ wasmUrl }) => fetch(wasmUrl)) .then((response) => { - if (!response.ok) { - throw new Error(`Failed to fetch WASM: ${response.status}`); - } + if (!response.ok) throw new Error(`Failed to fetch WASM: ${response.status}`); return response.arrayBuffer(); }) .catch((err) => { @@ -63,7 +58,7 @@ function sendWasmToWorker(worker: Worker): void { * 创建 Parquet Worker 实例并传递 WASM ArrayBuffer */ export function createParquetWorker(): Comlink.Remote { - const worker = new ParquetWorkerConstructor(); + const worker = new Worker(parquetWorkerUrl, { type: 'module' }); sendWasmToWorker(worker); const remote = Comlink.wrap(worker); workerRegistry.set(remote as Comlink.Remote, worker); @@ -79,7 +74,7 @@ export function createParquetImageWorker(): { remote: Comlink.Remote; raw: Worker; } { - const worker = new ParquetImageWorkerConstructor(); + const worker = new Worker(parquetImageWorkerUrl, { type: 'module' }); sendWasmToWorker(worker); const remote = Comlink.wrap(worker); workerRegistry.set(remote as Comlink.Remote, worker); diff --git a/src/react/components/Commons/ErrorState.tsx b/src/react/components/Commons/ErrorState.tsx index 9d4cad1..d97a703 100644 --- a/src/react/components/Commons/ErrorState.tsx +++ b/src/react/components/Commons/ErrorState.tsx @@ -26,7 +26,11 @@ export const ErrorState: React.FC = ({ title, message, onRetry, }; return ( -
+
@@ -47,6 +51,7 @@ export const ErrorState: React.FC = ({ title, message, onRetry, size="icon" className="h-6 w-6" onClick={handleCopy} + aria-label={copied ? t('common.copied', 'Copied') : t('common.copy')} title={t('common.copy')} > {copied ? : } diff --git a/src/react/components/ErrorBoundary.tsx b/src/react/components/ErrorBoundary.tsx index 2016105..0bfa360 100644 --- a/src/react/components/ErrorBoundary.tsx +++ b/src/react/components/ErrorBoundary.tsx @@ -2,10 +2,14 @@ import { Component } from 'react'; import type { ErrorInfo, ReactNode } from 'react'; import { AlertCircle, RotateCcw } from 'lucide-react'; import { Button } from '@/ui'; +import { useTranslation } from 'react-i18next'; +import { I18nProvider } from '../i18n/core'; +import { formatErrorForDisplay } from './errorDetails'; interface Props { children?: ReactNode; fallback?: ReactNode; + renderFallback?: (props: ErrorFallbackProps) => ReactNode; } interface State { @@ -13,6 +17,48 @@ interface State { error: Error | null; } +export interface ErrorFallbackProps { + error: Error | null; + onReset: () => void; +} + +/** Function component kept separate so the class boundary can use i18n and tests can render it directly. */ +export function ErrorFallback({ error, onReset }: ErrorFallbackProps) { + const { t } = useTranslation(); + const errorDetails = formatErrorForDisplay(error, import.meta.env.DEV); + + return ( +
+
+ +
+

{t('errorBoundary.title')}

+

+ {t('errorBoundary.description')} +

+
+

+ {t('errorBoundary.errorLabel')}: +

+
+          {errorDetails || t('errorBoundary.unknownError')}
+        
+
+ +
+ ); +} + export class ErrorBoundary extends Component { public state: State = { hasError: false, @@ -38,29 +84,15 @@ export class ErrorBoundary extends Component { return this.props.fallback; } + const fallbackProps = { error: this.state.error, onReset: this.handleReset }; + if (this.props.renderFallback) { + return this.props.renderFallback(fallbackProps); + } + return ( -
-
- -
-

Something went wrong

-

- An unexpected error occurred in the application. We've been notified and are working to - fix it. -

-
-

Error:

-
-              {this.state.error?.message || 'Unknown error'}
-              {'\n\nStack trace:\n'}
-              {this.state.error?.stack}
-            
-
- -
+ + + ); } diff --git a/src/react/components/LeRobotViewer.tsx b/src/react/components/LeRobotViewer.tsx index 981acf9..6180c99 100644 --- a/src/react/components/LeRobotViewer.tsx +++ b/src/react/components/LeRobotViewer.tsx @@ -1,35 +1,68 @@ -import React, { useEffect, useState, useRef, useCallback } from 'react'; +import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { LeRobotStudioProvider } from './LeRobotStudioProvider'; import { EpisodeSidebar } from './Sidebar/EpisodeSidebar'; import { Button } from '@/ui'; import { ArrowLeft, Download } from 'lucide-react'; -import type { DataSource } from '@/platform'; -import { createArchiveDataSourceFromUrl, getArchiveKindFromUrl } from '@/platform'; -import { preflightRemoteArchive, translateRemotePreflightFailure } from '@/platform'; +import type { DataSource } from '../../core/datasource/types'; +import { + createArchiveDataSourceFromUrl, + getArchiveKindFromUrl, +} from '../../platform/datasource/ArchiveDataSourceFactory'; +import { + preflightRemoteArchive, + translateRemotePreflightFailure, +} from '../../platform/datasource/remotePreflight'; import { useLeRobotData } from '../contexts/LeRobotContext'; import { useKeyboardShortcuts } from '../hooks/useKeyboardShortcuts'; import { ViewerLayout } from '../features/viewer/ViewerLayout'; import { ErrorState } from './Commons/ErrorState'; +/** Stable error categories reported to an embedding host. */ +export type LeRobotViewerErrorCode = + | 'INVALID_DATA_SOURCE' + | 'REMOTE_SOURCE_UNAVAILABLE' + | 'UNSUPPORTED_ARCHIVE' + | 'DATASET_LOAD_FAILED'; + +/** A fatal viewer error that requires host action or an explicit retry. */ +export interface LeRobotViewerError { + /** Machine-readable category. */ + code: LeRobotViewerErrorCode; + /** Localized message suitable for display. */ + message: string; + /** Whether retrying the same operation may succeed. */ + recoverable: boolean; + /** Original failure when one is available. */ + cause?: unknown; +} + +/** Properties for the stable, client-only LeRobot dataset viewer. */ export interface LeRobotViewerProps { + /** Remote archive URL or a custom browser data source. */ dataSource: string | DataSource; + /** Viewer color scheme. @defaultValue 'system' */ theme?: 'light' | 'dark' | 'system'; + /** BCP 47 language tag. Built-in messages currently cover en, zh, and ja. */ language?: string; + /** Show the episode sidebar. @defaultValue true */ showSidebar?: boolean; + /** Show playback controls. @defaultValue true */ showPlaybackBar?: boolean; + /** Additional class name applied to the scoped viewer root. */ className?: string; + /** Optional host-owned navigation action. */ onBack?: () => void; - /** 当提供时,在侧栏返回按钮下方显示导出按钮,点击后触发。 */ + /** Optional host-owned export action; the npm viewer does not export data itself. */ onExport?: () => void; /** - * 严重错误回调:当数据源解析失败或加载失败(如 CORS / 网络错误)时触发。 - * 典型用法:宿主应用收到回调后清空 url query 并提示用户,而不是显示 lerobot 内置错误页。 + * Receives fatal source or dataset loading failures. When provided, the host + * owns fatal-error rendering and the built-in fatal state is not rendered. */ - onFatalError?: (errorMessage: string) => void; + onFatalError?: (error: LeRobotViewerError) => void; /** - * 是否启用键盘快捷键(空格播放/暂停,方向键跳转帧等) - * @default true + * Enable viewer keyboard shortcuts such as Space and arrow-key seeking. + * @defaultValue true */ enableKeyboardShortcuts?: boolean; } @@ -53,9 +86,21 @@ export function LeRobotViewerContent({ const lastSourceRef = useRef(null); const [resolvedSource, setResolvedSource] = useState(null); - const [resolveError, setResolveError] = useState(null); + const [resolveError, setResolveError] = useState(null); const [isResolving, setIsResolving] = useState(false); - const lastFatalErrorRef = useRef(error); + const lastFatalErrorRef = useRef(null); + const fatalError = useMemo( + () => + resolveError ?? + (error + ? { + code: 'DATASET_LOAD_FAILED', + message: error, + recoverable: true, + } + : null), + [error, resolveError], + ); const normalizeUrl = (raw: string) => { try { @@ -81,7 +126,11 @@ export function LeRobotViewerContent({ const raw = dataSource.trim(); if (!raw) { setResolvedSource(null); - setResolveError(t('common.error') + ': empty dataSource url'); + setResolveError({ + code: 'INVALID_DATA_SOURCE', + message: `${t('common.error')}: empty dataSource url`, + recoverable: false, + }); return; } @@ -91,11 +140,23 @@ export function LeRobotViewerContent({ try { const pre = await preflightRemoteArchive(url); if (!pre.ok) { - throw new Error(translateRemotePreflightFailure(t, pre.failure)); + setResolvedSource(null); + setResolveError({ + code: 'REMOTE_SOURCE_UNAVAILABLE', + message: translateRemotePreflightFailure(t, pre.failure), + recoverable: true, + }); + return; } const kind = getArchiveKindFromUrl(url) || pre.kind; if (!kind) { - throw new Error(t('validation.unsupportedFormat')); + setResolvedSource(null); + setResolveError({ + code: 'UNSUPPORTED_ARCHIVE', + message: t('validation.unsupportedFormat'), + recoverable: false, + }); + return; } if (cancelled) return; setResolvedSource( @@ -107,7 +168,12 @@ export function LeRobotViewerContent({ if (cancelled) return; const msg = e instanceof Error ? e.message : String(e); setResolvedSource(null); - setResolveError(msg); + setResolveError({ + code: 'REMOTE_SOURCE_UNAVAILABLE', + message: msg, + recoverable: true, + cause: e, + }); } finally { if (!cancelled) setIsResolving(false); } @@ -127,19 +193,18 @@ export function LeRobotViewerContent({ }, [resolvedSource, initialize]); useEffect(() => { - if (!error && !resolveError) { + if (!fatalError) { lastFatalErrorRef.current = null; } - }, [error, resolveError]); + }, [fatalError]); useEffect(() => { - if (!onFatalError) return; - const msg = resolveError || error; - if (!msg) return; - if (lastFatalErrorRef.current === msg) return; - lastFatalErrorRef.current = msg; - onFatalError(msg); - }, [resolveError, error, onFatalError]); + if (!onFatalError || !fatalError) return; + const key = `${fatalError.code}:${fatalError.message}`; + if (lastFatalErrorRef.current === key) return; + lastFatalErrorRef.current = key; + onFatalError(fatalError); + }, [fatalError, onFatalError]); const handleRetry = useCallback(() => { clearError(); @@ -157,7 +222,7 @@ export function LeRobotViewerContent({ } }, [clearError, resolvedSource, initialize, dataSource]); - if (onFatalError && (resolveError || error)) { + if (onFatalError && fatalError) { // The embedding host owns fatal-error rendering via onFatalError. Keeping a // spinner mounted here made permanent failures indistinguishable from load. return null; @@ -199,24 +264,30 @@ export function LeRobotViewerContent({
) : null; - const emptyState = - resolveError || error ? ( - - ) : ( -
-
-
- - {isResolving || isLoading ? t('common.initializing') : t('common.loading')} - -
+ const emptyState = fatalError ? ( + + ) : ( +
+
+
+ + {isResolving || isLoading ? t('common.initializing') : t('common.loading')} +
- ); +
+ ); return ( = ({
{/* Center: Title & Logo */} -
+
{isLoading ? ( - + ) : !datasetName ? ( LeRobot Studio Logo ) : null} @@ -345,7 +352,14 @@ export const Navbar: React.FC = ({ {/* Bottom Progress Bar */} {(activeTask || percent !== undefined) && ( -
+
{activeTask && activeTask.phase === 'error' ? (
) : percent !== undefined ? ( diff --git a/src/react/components/PanelTitle.tsx b/src/react/components/PanelTitle.tsx index de9b91f..20fbcb1 100644 --- a/src/react/components/PanelTitle.tsx +++ b/src/react/components/PanelTitle.tsx @@ -15,7 +15,7 @@ import { import { Plus, X, Video, Image, TrendingUp, FileText, ArrowDown, ArrowRight } from 'lucide-react'; import type { IDockviewPanelHeaderProps } from 'dockview-react'; import { useTranslation } from 'react-i18next'; -import { useLeRobot } from '../contexts/LeRobotContext'; +import { useLeRobotData } from '../contexts/LeRobotContext'; import { safeAddPanel } from '../utils/dockviewPanelId'; type PanelType = 'video' | 'image' | 'chart' | 'raw'; @@ -23,7 +23,7 @@ type PanelType = 'video' | 'image' | 'chart' | 'raw'; export const PanelTitle: React.FC = (props) => { const { api, containerApi } = props; const { t } = useTranslation(); - const { info } = useLeRobot(); + const { info } = useLeRobotData(); const panelId = api.id; const panel = containerApi.getPanel(panelId); diff --git a/src/react/components/Playback/PlaybackBar.tsx b/src/react/components/Playback/PlaybackBar.tsx index e6b55e3..08e2299 100644 --- a/src/react/components/Playback/PlaybackBar.tsx +++ b/src/react/components/Playback/PlaybackBar.tsx @@ -129,7 +129,12 @@ export const PlaybackBar: React.FC = () => { // Render skeleton if loading if (isLoading || activeTask) { return ( -
+
diff --git a/src/react/components/Sidebar/AnalysisSidebarContent.tsx b/src/react/components/Sidebar/AnalysisSidebarContent.tsx index b73bdb0..49d8eb8 100644 --- a/src/react/components/Sidebar/AnalysisSidebarContent.tsx +++ b/src/react/components/Sidebar/AnalysisSidebarContent.tsx @@ -1,7 +1,7 @@ import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { BarChart3 } from 'lucide-react'; -import { useLeRobot } from '../../contexts/LeRobotContext'; +import { useLeRobotData, useLeRobotSelection } from '../../contexts/LeRobotContext'; import { ScrollArea } from '@/ui'; import { Separator } from '@/ui'; import { AnalysisOverview } from './analysis/AnalysisOverview'; @@ -10,7 +10,8 @@ import { AnalysisTaskDistribution } from './analysis/AnalysisTaskDistribution'; export const AnalysisSidebarContent: React.FC = () => { const { t } = useTranslation(); - const { info, effectiveEpisodes, tasks } = useLeRobot(); + const { info, tasks } = useLeRobotData(); + const { effectiveEpisodes } = useLeRobotSelection(); const analysis = useMemo(() => { if (!info || effectiveEpisodes.length === 0) return null; @@ -73,7 +74,7 @@ export const AnalysisSidebarContent: React.FC = () => { totalFrames={analysis.totalFrames} fps={analysis.fps} codebaseVersion={analysis.codebaseVersion} - robotType={analysis.robotType} + robotType={analysis.robotType ?? undefined} /> { const { t } = useTranslation(); + const { episodes, tasks, isLoading, error, info, versionCapability, isReadOnly } = + useLeRobotData(); const { - episodes, - tasks, selectEpisode, selectedEpisodeIndex, selectedEpisodeIndices, toggleEpisodeSelection, selectAllInList, clearEpisodeSelection, - isLoading, - error, - info, deletedEpisodes, getEffectiveEpisode, editEpisodeTask, deleteEpisode, restoreEpisode, - } = useLeRobot(); + } = useLeRobotSelection(); const [searchTerm, setSearchTerm] = useState(''); const [editMode, setEditMode] = useState(false); const [multiSelectMode, setMultiSelectMode] = useState(false); @@ -43,6 +40,16 @@ export const EpisodeSidebar: React.FC = () => { const [showAdvanced, setShowAdvanced] = useState(false); const [bulkEditOpen, setBulkEditOpen] = useState(false); const [bulkEditText, setBulkEditText] = useState(''); + const mutationDisabled = isReadOnly || Boolean(info && versionCapability?.status !== 'supported'); + + useEffect(() => { + if (!mutationDisabled) return; + setEditMode(false); + setMultiSelectMode(false); + setEditDialogOpen(false); + setBulkEditOpen(false); + clearEpisodeSelection(); + }, [mutationDisabled, clearEpisodeSelection]); const filteredEpisodes = useMemo(() => { if (!episodes) return []; @@ -75,17 +82,20 @@ export const EpisodeSidebar: React.FC = () => { const openEditDialog = (e: React.MouseEvent, episode: EpisodeMetadata) => { e.stopPropagation(); + if (mutationDisabled) return; setEpisodeToEdit(getEffectiveEpisode(episode)); setEditDialogOpen(true); }; const handleDelete = (e: React.MouseEvent, episodeIndex: number) => { e.stopPropagation(); + if (mutationDisabled) return; deleteEpisode(episodeIndex); }; const handleRestore = (e: React.MouseEvent, episodeIndex: number) => { e.stopPropagation(); + if (mutationDisabled) return; restoreEpisode(episodeIndex); }; @@ -112,6 +122,7 @@ export const EpisodeSidebar: React.FC = () => { ); const handleBulkEditSave = () => { + if (mutationDisabled) return; const text = bulkEditText.trim(); selectedInFiltered.forEach((ep) => editEpisodeTask(ep.episode_index, text || '')); setBulkEditOpen(false); @@ -157,12 +168,20 @@ export const EpisodeSidebar: React.FC = () => { totalCount={episodes?.length} editMode={editMode} multiSelectMode={multiSelectMode} + isReadOnly={mutationDisabled} onToggleEditMode={() => setEditMode((v) => !v)} onToggleMultiSelectMode={() => { setMultiSelectMode((v) => !v); if (multiSelectMode) clearEpisodeSelection(); }} /> + {mutationDisabled && versionCapability && ( +

+ {t('sidebar.readOnlyVersion', { + version: versionCapability.normalizedVersion ?? info?.codebase_version ?? 'unknown', + })} +

+ )} { onDurationMaxChange={setDurationMax} /> )} - {multiSelectMode && ( + {!mutationDisabled && multiSelectMode && ( { episodes={episodes} filteredEpisodes={listRows} isLoading={isLoading} - multiSelectMode={multiSelectMode} - editMode={editMode} + multiSelectMode={!mutationDisabled && multiSelectMode} + editMode={!mutationDisabled && editMode} onRowClick={handleRowClick} onToggleSelection={(episode, event) => { event.preventDefault(); @@ -213,39 +232,43 @@ export const EpisodeSidebar: React.FC = () => { - - - - - {t('sidebar.bulkEdit', 'Edit task')} - -
-

- {t('sidebar.selectedCount', { count: selectedInFiltered.length })} -{' '} - {t('editTask.taskDescription')} -

-