diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index c88061f..0d09f8f 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -1,72 +1,45 @@ name: Publish Catalog Pages on: - workflow_dispatch: - workflow_run: - workflows: ["Publish package"] - types: [completed] - -concurrency: - group: convax-registry-pages - cancel-in-progress: false + workflow_call: permissions: + actions: read contents: read jobs: build: - if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' runs-on: ubuntu-latest timeout-minutes: 10 steps: - - name: Check out the protected catalog source + - name: Check out the exact protected catalog source uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: - ref: main persist-credentials: false - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: bun-version: 1.3.14 - - name: Build source package workspaces - run: | - bun install --frozen-lockfile --ignore-scripts - bun run workspaces:build:packages - - name: Fetch published release entries - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: bun tooling/fetch-release-entries.mjs - - name: Fetch the current production Registry - run: | - mkdir -p dist/production - curl --fail --location --silent --show-error \ - --retry 4 --retry-all-errors \ - "https://microvoid.github.io/convax-plugins/registry/v1/index.json?run=$GITHUB_RUN_ID" \ - --output dist/production/index.json - - name: Prepare independently publishable Releases - run: | - bun tooling/prepare-release-catalog.mjs \ - --entries dist/release-entries \ - --previous dist/production/index.json - - name: Build and stage strict catalogs + - name: Download the exact low-privilege build + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: marketplace-release-${{ github.sha }} + path: dist + - name: Reverify and stage strict catalogs run: | - bun tooling/build-index.mjs \ - --entries dist/release-entries \ - --previous dist/production/index.json \ - --revision "$(git rev-parse HEAD)" - bun tooling/build-showcase.mjs \ - --entries dist/release-entries \ - --registry dist/registry/v1/index.json - mkdir -p dist/site/registry/v1 dist/site/showcase/v1 dist/site/schemas - cp dist/registry/v1/index.json dist/site/registry/v1/index.json - cp dist/showcase/v1/index.json dist/site/showcase/v1/index.json - cp schemas/*.json dist/site/schemas/ + bun tooling/verify-marketplace-output.mjs dist/catalog + bun tooling/verify-product-lock-input.mjs dist/product-lock-input.json + test "$(find schemas -maxdepth 1 -type f -name '*.json' | wc -l | tr -d ' ')" = \ + "$(find dist/catalog/site/schemas -maxdepth 1 -type f -name '*.json' | wc -l | tr -d ' ')" + for schema in schemas/*.json; do + cmp "$schema" "dist/catalog/site/schemas/$(basename "$schema")" + done - name: Configure Pages uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 - name: Upload Pages artifact uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 with: - path: dist/site + path: dist/catalog/site deploy: needs: build diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 359044d..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,208 +0,0 @@ -name: Publish package - -on: - push: - tags: - - "plugin-*-v*" - - "skill-*-v*" - -permissions: - contents: read - -jobs: - verify: - runs-on: macos-14 - timeout-minutes: 20 - steps: - - name: Check out tagged source - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - fetch-depth: 0 - persist-credentials: false - - name: Require a commit reachable from main - run: | - git fetch --no-tags origin main - git merge-base --is-ancestor HEAD origin/main - - name: Set up Bun - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 - with: - bun-version: 1.3.14 - - name: Install workspace dependencies - run: bun install --frozen-lockfile --ignore-scripts - - name: Verify official FFmpeg source - if: startsWith(github.ref_name, 'plugin-ffmpeg-tools-v') - run: | - set -euo pipefail - if ! command -v gpg >/dev/null 2>&1; then - brew install gnupg - fi - source_path="$RUNNER_TEMP/ffmpeg-8.1.2.tar.xz" - signature_path="$source_path.asc" - key_path="$RUNNER_TEMP/ffmpeg-devel.asc" - curl --fail --location --silent --show-error https://ffmpeg.org/releases/ffmpeg-8.1.2.tar.xz --output "$source_path" - curl --fail --location --silent --show-error https://ffmpeg.org/releases/ffmpeg-8.1.2.tar.xz.asc --output "$signature_path" - curl --fail --location --silent --show-error https://ffmpeg.org/ffmpeg-devel.asc --output "$key_path" - test "$(stat -f '%z' "$source_path")" = "11710924" - test "$(shasum -a 256 "$source_path" | awk '{print $1}')" = "464beb5e7bf0c311e68b45ae2f04e9cc2af88851abb4082231742a74d97b524c" - test "$(stat -f '%z' "$signature_path")" = "520" - test "$(shasum -a 256 "$signature_path" | awk '{print $1}')" = "0a0963fccd70597838073f3e31b20f4a4d8cc2b5e577472c9a5a1f22624246f8" - test "$(stat -f '%z' "$key_path")" = "1709" - test "$(shasum -a 256 "$key_path" | awk '{print $1}')" = "397b3becedcd5a98769967ff1ff8501ddc89f8368b8f766e4701377d7dbaabe5" - export GNUPGHOME="$RUNNER_TEMP/ffmpeg-gnupg" - mkdir -m 700 "$GNUPGHOME" - gpg --batch --import "$key_path" - fingerprint="$(gpg --batch --with-colons --fingerprint | awk -F: '$1 == "fpr" { print $10; exit }')" - test "$fingerprint" = "FCF986EA15E6E293A5644F10B4322F04D67658D8" - gpg --batch --verify "$signature_path" "$source_path" - echo "CONVAX_FFMPEG_SOURCE_ARCHIVE=$source_path" >> "$GITHUB_ENV" - echo "CONVAX_FFMPEG_SOURCE_SIGNATURE=$signature_path" >> "$GITHUB_ENV" - echo "CONVAX_FFMPEG_SIGNING_KEY=$key_path" >> "$GITHUB_ENV" - echo "CONVAX_FFMPEG_REQUIRE_PGP=1" >> "$GITHUB_ENV" - - name: Validate repository and tag - run: | - bun run check - bun tooling/pack.mjs --tag "$GITHUB_REF_NAME" - - publish: - needs: verify - runs-on: macos-14 - timeout-minutes: 20 - permissions: - attestations: write - contents: write - id-token: write - steps: - - name: Check out tagged source - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - persist-credentials: false - - name: Set up Bun - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 - with: - bun-version: 1.3.14 - - name: Install workspace dependencies - run: bun install --frozen-lockfile --ignore-scripts - - name: Verify official FFmpeg source - if: startsWith(github.ref_name, 'plugin-ffmpeg-tools-v') - run: | - set -euo pipefail - if ! command -v gpg >/dev/null 2>&1; then - brew install gnupg - fi - source_path="$RUNNER_TEMP/ffmpeg-8.1.2.tar.xz" - signature_path="$source_path.asc" - key_path="$RUNNER_TEMP/ffmpeg-devel.asc" - curl --fail --location --silent --show-error https://ffmpeg.org/releases/ffmpeg-8.1.2.tar.xz --output "$source_path" - curl --fail --location --silent --show-error https://ffmpeg.org/releases/ffmpeg-8.1.2.tar.xz.asc --output "$signature_path" - curl --fail --location --silent --show-error https://ffmpeg.org/ffmpeg-devel.asc --output "$key_path" - test "$(stat -f '%z' "$source_path")" = "11710924" - test "$(shasum -a 256 "$source_path" | awk '{print $1}')" = "464beb5e7bf0c311e68b45ae2f04e9cc2af88851abb4082231742a74d97b524c" - test "$(stat -f '%z' "$signature_path")" = "520" - test "$(shasum -a 256 "$signature_path" | awk '{print $1}')" = "0a0963fccd70597838073f3e31b20f4a4d8cc2b5e577472c9a5a1f22624246f8" - test "$(stat -f '%z' "$key_path")" = "1709" - test "$(shasum -a 256 "$key_path" | awk '{print $1}')" = "397b3becedcd5a98769967ff1ff8501ddc89f8368b8f766e4701377d7dbaabe5" - export GNUPGHOME="$RUNNER_TEMP/ffmpeg-gnupg" - mkdir -m 700 "$GNUPGHOME" - gpg --batch --import "$key_path" - fingerprint="$(gpg --batch --with-colons --fingerprint | awk -F: '$1 == "fpr" { print $10; exit }')" - test "$fingerprint" = "FCF986EA15E6E293A5644F10B4322F04D67658D8" - gpg --batch --verify "$signature_path" "$source_path" - echo "CONVAX_FFMPEG_SOURCE_ARCHIVE=$source_path" >> "$GITHUB_ENV" - echo "CONVAX_FFMPEG_SOURCE_SIGNATURE=$signature_path" >> "$GITHUB_ENV" - echo "CONVAX_FFMPEG_SIGNING_KEY=$key_path" >> "$GITHUB_ENV" - echo "CONVAX_FFMPEG_REQUIRE_PGP=1" >> "$GITHUB_ENV" - - name: Build immutable package assets - id: package - run: | - bun run workspaces:build:packages - bun tooling/build-companions.mjs --tag "$GITHUB_REF_NAME" - bun tooling/pack.mjs --tag "$GITHUB_REF_NAME" - package_dir="dist/packages/$GITHUB_REF_NAME" - zip_path="$(find "$package_dir" -maxdepth 1 -type f -name '*.zip' -print -quit)" - test -n "$zip_path" - echo "zip=$zip_path" >> "$GITHUB_OUTPUT" - echo "entry=$package_dir/registry-entry.json" >> "$GITHUB_OUTPUT" - echo "directory=$package_dir" >> "$GITHUB_OUTPUT" - if compgen -G "$package_dir/convax-companion-*" >/dev/null; then - echo "has_companions=true" >> "$GITHUB_OUTPUT" - else - echo "has_companions=false" >> "$GITHUB_OUTPUT" - fi - if [[ "$GITHUB_REF_NAME" == plugin-ffmpeg-tools-v* ]]; then - companion_path="$(find "$package_dir" -maxdepth 1 -type f -name 'convax-companion-convax-ffmpeg-mcp-*' -print -quit)" - test -n "$companion_path" - cp "$CONVAX_FFMPEG_SOURCE_ARCHIVE" "$package_dir/convax-source-ffmpeg-8.1.2.tar.xz" - cp "$CONVAX_FFMPEG_SOURCE_SIGNATURE" "$package_dir/convax-source-ffmpeg-8.1.2.tar.xz.asc" - cp "$CONVAX_FFMPEG_SIGNING_KEY" "$package_dir/convax-source-ffmpeg-signing-key.asc" - cp packages/tools/ffmpeg-mcp/FFMPEG-LICENSE "$package_dir/convax-ffmpeg-LGPL-2.1.txt" - cp packages/tools/ffmpeg-mcp/THIRD_PARTY_NOTICES.md "$package_dir/convax-ffmpeg-THIRD-PARTY-NOTICES.md" - tar -xOf "$CONVAX_FFMPEG_SOURCE_ARCHIVE" ffmpeg-8.1.2/LICENSE.md > "$package_dir/convax-ffmpeg-LICENSE.md" - tar -xOf "$CONVAX_FFMPEG_SOURCE_ARCHIVE" ffmpeg-8.1.2/CREDITS > "$package_dir/convax-ffmpeg-CREDITS" - test -s "$package_dir/convax-ffmpeg-LICENSE.md" - test -s "$package_dir/convax-ffmpeg-CREDITS" - SOURCE_DATE_EPOCH="$(git show -s --format=%ct HEAD)" bun packages/tools/ffmpeg-mcp/scripts/build-sbom.ts \ - --companion "$companion_path" \ - --output "$package_dir/convax-ffmpeg-sbom.spdx.json" \ - --revision "$GITHUB_SHA" \ - --tag "$GITHUB_REF_NAME" - echo "sbom=$package_dir/convax-ffmpeg-sbom.spdx.json" >> "$GITHUB_OUTPUT" - echo "has_ffmpeg_source=true" >> "$GITHUB_OUTPUT" - else - echo "has_ffmpeg_source=false" >> "$GITHUB_OUTPUT" - fi - - name: Attest package ZIP - uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 - with: - subject-path: ${{ steps.package.outputs.zip }} - - name: Attest companion executables - if: steps.package.outputs.has_companions == 'true' - uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 - with: - subject-path: ${{ steps.package.outputs.directory }}/convax-companion-* - - name: Attest FFmpeg SBOM - if: steps.package.outputs.has_ffmpeg_source == 'true' - uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 - with: - subject-path: ${{ steps.package.outputs.sbom }} - - name: Create immutable GitHub Release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then - echo "Release or draft already exists; inspect it before retrying." >&2 - exit 1 - fi - gh release create "$GITHUB_REF_NAME" \ - --draft \ - --verify-tag \ - --title "$GITHUB_REF_NAME" \ - --notes "Deterministic Convax package. Verify the ZIP against registry-entry.json and its build provenance attestation." - shopt -s nullglob - assets=( - "${{ steps.package.outputs.zip }}" - "${{ steps.package.outputs.entry }}" - ) - companion_assets=("${{ steps.package.outputs.directory }}"/convax-companion-*) - assets+=("${companion_assets[@]}") - if [[ "${{ steps.package.outputs.has_ffmpeg_source }}" == "true" ]]; then - assets+=( - "${{ steps.package.outputs.directory }}/convax-source-ffmpeg-8.1.2.tar.xz" - "${{ steps.package.outputs.directory }}/convax-source-ffmpeg-8.1.2.tar.xz.asc" - "${{ steps.package.outputs.directory }}/convax-source-ffmpeg-signing-key.asc" - "${{ steps.package.outputs.directory }}/convax-ffmpeg-LGPL-2.1.txt" - "${{ steps.package.outputs.directory }}/convax-ffmpeg-THIRD-PARTY-NOTICES.md" - "${{ steps.package.outputs.directory }}/convax-ffmpeg-LICENSE.md" - "${{ steps.package.outputs.directory }}/convax-ffmpeg-CREDITS" - "${{ steps.package.outputs.sbom }}" - ) - fi - showcase_entry="${{ steps.package.outputs.directory }}/showcase-entry.json" - showcase_assets=("${{ steps.package.outputs.directory }}"/convax-showcase-*) - if [[ -f "$showcase_entry" ]]; then - assets+=("$showcase_entry" "${showcase_assets[@]}") - elif (( ${#showcase_assets[@]} > 0 )); then - echo "Showcase assets exist without showcase-entry.json." >&2 - exit 1 - fi - gh release upload "$GITHUB_REF_NAME" "${assets[@]}" - gh release edit "$GITHUB_REF_NAME" --draft=false diff --git a/.github/workflows/release-on-main.yml b/.github/workflows/release-on-main.yml new file mode 100644 index 0000000..b88f1ef --- /dev/null +++ b/.github/workflows/release-on-main.yml @@ -0,0 +1,198 @@ +name: Publish changed Marketplace versions + +on: + push: + branches: [main] + +permissions: + contents: read + +concurrency: + group: convax-marketplace-release + cancel-in-progress: false + +jobs: + verify: + runs-on: macos-14 + timeout-minutes: 45 + outputs: + count: ${{ steps.plan.outputs.count }} + steps: + - name: Check out protected source + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 + persist-credentials: false + - name: Set up Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + bun-version: 1.3.14 + - name: Install inert workspace dependencies + run: bun install --frozen-lockfile --ignore-scripts + - name: Select exact version changes + id: plan + env: + BASE_SHA: ${{ github.event.before }} + run: | + bun tooling/marketplace-release.mjs \ + --base "$BASE_SHA" \ + --head "$GITHUB_SHA" \ + --output dist/release-plan.json + echo "count=$(jq length dist/release-plan.json)" >> "$GITHUB_OUTPUT" + echo "CONVAX_MARKETPLACE_CHANGED=dist/release-plan.json" >> "$GITHUB_ENV" + - name: Fetch the current production sequence input + if: steps.plan.outputs.count != '0' + run: bun tooling/fetch-marketplace-previous.mjs + - name: Verify the pinned official FFmpeg source + if: steps.plan.outputs.count != '0' + run: | + set -euo pipefail + if ! jq -e '.[] | select(.kind == "plugin" and .id == "ffmpeg-tools")' dist/release-plan.json >/dev/null; then + exit 0 + fi + if ! command -v gpg >/dev/null 2>&1; then + brew install gnupg + fi + source_path="$RUNNER_TEMP/ffmpeg-8.1.2.tar.xz" + signature_path="$source_path.asc" + key_path="$RUNNER_TEMP/ffmpeg-devel.asc" + curl --fail --location --silent --show-error https://ffmpeg.org/releases/ffmpeg-8.1.2.tar.xz --output "$source_path" + curl --fail --location --silent --show-error https://ffmpeg.org/releases/ffmpeg-8.1.2.tar.xz.asc --output "$signature_path" + curl --fail --location --silent --show-error https://ffmpeg.org/ffmpeg-devel.asc --output "$key_path" + test "$(stat -f '%z' "$source_path")" = "11710924" + test "$(shasum -a 256 "$source_path" | awk '{print $1}')" = "464beb5e7bf0c311e68b45ae2f04e9cc2af88851abb4082231742a74d97b524c" + test "$(stat -f '%z' "$signature_path")" = "520" + test "$(shasum -a 256 "$signature_path" | awk '{print $1}')" = "0a0963fccd70597838073f3e31b20f4a4d8cc2b5e577472c9a5a1f22624246f8" + test "$(stat -f '%z' "$key_path")" = "1709" + test "$(shasum -a 256 "$key_path" | awk '{print $1}')" = "397b3becedcd5a98769967ff1ff8501ddc89f8368b8f766e4701377d7dbaabe5" + export GNUPGHOME="$RUNNER_TEMP/ffmpeg-gnupg" + mkdir -m 700 "$GNUPGHOME" + gpg --batch --import "$key_path" + fingerprint="$(gpg --batch --with-colons --fingerprint | awk -F: '$1 == "fpr" { print $10; exit }')" + test "$fingerprint" = "FCF986EA15E6E293A5644F10B4322F04D67658D8" + gpg --batch --verify "$signature_path" "$source_path" + echo "CONVAX_FFMPEG_SOURCE_ARCHIVE=$source_path" >> "$GITHUB_ENV" + echo "CONVAX_FFMPEG_SOURCE_SIGNATURE=$signature_path" >> "$GITHUB_ENV" + echo "CONVAX_FFMPEG_SIGNING_KEY=$key_path" >> "$GITHUB_ENV" + echo "CONVAX_FFMPEG_REQUIRE_PGP=1" >> "$GITHUB_ENV" + - name: Validate and build immutable release inputs + if: steps.plan.outputs.count != '0' + run: bun run check + - name: Stage reviewed public schemas into the low-privilege Pages tree + if: steps.plan.outputs.count != '0' + run: | + mkdir -p dist/catalog/site/schemas + cp schemas/*.json dist/catalog/site/schemas/ + - name: Stage verified FFmpeg source and SBOM beside the companion + if: env.CONVAX_FFMPEG_REQUIRE_PGP == '1' + run: | + set -euo pipefail + tag="$(jq -r '.[] | select(.kind == "plugin" and .id == "ffmpeg-tools") | .releaseTag' dist/release-plan.json)" + test -n "$tag" + package_dir="dist/catalog/releases/$tag" + companion_path="$(find "$package_dir" -maxdepth 1 -type f -name '*-convax-ffmpeg-mcp' -print -quit)" + test -n "$companion_path" + cp "$CONVAX_FFMPEG_SOURCE_ARCHIVE" "$package_dir/convax-source-ffmpeg-8.1.2.tar.xz" + cp "$CONVAX_FFMPEG_SOURCE_SIGNATURE" "$package_dir/convax-source-ffmpeg-8.1.2.tar.xz.asc" + cp "$CONVAX_FFMPEG_SIGNING_KEY" "$package_dir/convax-source-ffmpeg-signing-key.asc" + cp packages/tools/ffmpeg-mcp/FFMPEG-LICENSE "$package_dir/convax-ffmpeg-LGPL-2.1.txt" + cp packages/tools/ffmpeg-mcp/THIRD_PARTY_NOTICES.md "$package_dir/convax-ffmpeg-THIRD-PARTY-NOTICES.md" + tar -xOf "$CONVAX_FFMPEG_SOURCE_ARCHIVE" ffmpeg-8.1.2/LICENSE.md > "$package_dir/convax-ffmpeg-LICENSE.md" + tar -xOf "$CONVAX_FFMPEG_SOURCE_ARCHIVE" ffmpeg-8.1.2/CREDITS > "$package_dir/convax-ffmpeg-CREDITS" + SOURCE_DATE_EPOCH="$(git show -s --format=%ct HEAD)" bun packages/tools/ffmpeg-mcp/scripts/build-sbom.ts \ + --companion "$companion_path" \ + --output "$package_dir/convax-ffmpeg-sbom.spdx.json" \ + --revision "$GITHUB_SHA" \ + --tag "$tag" + - name: Compose the exact publication plan + if: steps.plan.outputs.count != '0' + run: bun tooling/publication-plan.mjs + - name: Upload verified exact bytes + if: steps.plan.outputs.count != '0' + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + with: + name: marketplace-release-${{ github.sha }} + path: | + dist/builtin + dist/catalog + dist/product-lock-input.json + dist/publication-plan.json + dist/release-plan.json + if-no-files-found: error + retention-days: 1 + + publish: + needs: verify + if: needs.verify.outputs.count != '0' + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + attestations: write + contents: write + id-token: write + steps: + - name: Check out protected source + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: true + - name: Download verified exact bytes + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: marketplace-release-${{ github.sha }} + path: dist + - name: Attest immutable package and companion assets + uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 + with: + subject-path: dist/**/releases/** + - name: Publish each selected version once + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + jq -c '.releases[]' dist/publication-plan.json | while read -r item; do + tag="$(jq -r '.tag' <<<"$item")" + package_dir="dist/$(jq -r '.directory' <<<"$item")" + test -d "$package_dir" + if gh release view "$tag" >/dev/null 2>&1; then + existing_dir="$RUNNER_TEMP/existing-$tag" + mkdir "$existing_dir" + gh release download "$tag" --dir "$existing_dir" + test "$(find "$existing_dir" -type f | wc -l | tr -d ' ')" = \ + "$(find "$package_dir" -type f | wc -l | tr -d ' ')" + for asset in "$package_dir"/*; do + test -f "$existing_dir/$(basename "$asset")" + cmp "$asset" "$existing_dir/$(basename "$asset")" + done + remote_tag="$(git ls-remote origin "refs/tags/$tag" | awk '{print $1}')" + test "$remote_tag" = "$GITHUB_SHA" + continue + fi + remote_tag="$(git ls-remote origin "refs/tags/$tag" | awk '{print $1}')" + if [ -z "$remote_tag" ]; then + git tag "$tag" "$GITHUB_SHA" + git push origin "refs/tags/$tag" + else + test "$remote_tag" = "$GITHUB_SHA" + fi + shopt -s nullglob + assets=("$package_dir"/*) + test "${#assets[@]}" -gt 0 + gh release create "$tag" \ + --verify-tag \ + --title "$tag" \ + --notes "Deterministic Convax Marketplace package generated from protected main." \ + "${assets[@]}" + done + + pages: + needs: [verify, publish] + if: >- + always() && + needs.verify.result == 'success' && + needs.publish.result == 'success' + permissions: + actions: read + contents: read + id-token: write + pages: write + uses: ./.github/workflows/pages.yml diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..10d96a4 --- /dev/null +++ b/.npmrc @@ -0,0 +1,3 @@ +registry=https://registry.npmjs.org/ +access=public +provenance=true diff --git a/AGENTS.md b/AGENTS.md index cd02c2f..fc0c9ad 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,8 +5,8 @@ These rules apply to people and AI agents in this repository. ## Repository ownership boundary - This repository is the authoritative source for concrete Convax Plugins, - Plugin-owned Skills, standalone Skills, and reviewed companion tools, including - official, default-catalog, and vendor integrations. + Plugin-owned Skills, standalone Skills, MCP Servers, reviewed companion tools, + the Official Marketplace, and the immutable Builtin bundle. - The `microvoid/convax` repository owns the generic host platform: manifest ABI, validation at installation, lifecycle, runtime bridges, IPC/UI, and Registry consumption. Do not copy its private implementation or create a package-specific @@ -28,8 +28,15 @@ These rules apply to people and AI agents in this repository. ## Package rules -- Plugin and Skill sources live under `packages/plugins/` and - `packages/skills/` respectively. +- Plugin, Skill, and MCP Server sources live under `packages/plugins/`, + `packages/skills/`, and `packages/mcp-servers/` respectively. +- MCP Server identity and version come only from the reviewed standard + `server.json`. A fixed HTTPS server has exactly one supported remote and no + `convax-mcp.json`. A managed-stdio server has no supported remote and uses the + strict `convax.mcp-server-extension/1`; its target executable is always a + separate immutable companion. +- Directories below `tooling/fixtures/mcp-servers/` are inert acceptance + fixtures only. They are never Marketplace packages or workspaces. - Every Plugin, Skill, and Tool directory is a Bun workspace with its own `package.json`, dependency declarations, and scripts. The repository owns one root `bun.lock`; do not add package-local lockfiles or hard-code workspace ids in CI. @@ -67,6 +74,16 @@ These rules apply to people and AI agents in this repository. - Do not use symlinks, absolute paths, traversal, Windows-reserved names, generated dependency trees, secrets, or files larger than repository limits. - Increment package SemVer whenever released bytes or catalog metadata change. +- The protected default branch publishes only packages whose identity version + changed. Authors do not create release tags. Any tracked package byte change + without an identity version change fails before privileged publication. +- Official Registry v2, its strict lossless v1 projection, Showcase v2, and the + Builtin bundle are generated with `@convax/marketplace-kit`. Do not duplicate its + schema parsers, deterministic ZIP writer, or Registry builder in this repository. +- `catalogs/builtin.json` contains only standalone Skill `canvas-storyboard` in + the first bundle. `catalogs/preinstalled.json` contains only Official Plugin + `ffmpeg-tools` for `darwin-arm64` with explicit setup. Neither membership grants + execution authority. - Treat `registry/config.json` sequence as the production sequence floor. Bump it for explicit catalog-policy changes such as yanking; ordinary package Releases advance the deployed sequence independently through the protected Pages workflow. @@ -79,10 +96,13 @@ These rules apply to people and AI agents in this repository. Run `bun install --frozen-lockfile --ignore-scripts`, trusted Plugin/Skill workspace builds, `bun run validate`, workspace tests, `bun run build:companions`, `bun test`, -`bun run pack`, and `bun run build:index` before requesting review. The explicit -package and companion build phases finish before validation and packing. Validation -and packing themselves remain inert and never execute contributor-provided scripts. -Tooling treats package contents as inert bytes. +`bun run pack`, `bun run build:index`, Marketplace Kit `check`, Official v2/v1 +build, and Builtin `bundle` before requesting review. Dogfood the real packed +`@convax/marketplace-kit` tarball in an isolated consumer until its exact version is +published; never commit an absolute `file:` override. The explicit package and +companion build phases finish before validation and packing. Validation and packing +themselves remain inert and never execute contributor-provided scripts. Tooling +treats package contents as inert bytes. ## Git discipline diff --git a/README.md b/README.md index 1da1a7d..2feaeca 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,20 @@ [English](README.md) | [简体中文](README.zh-CN.md) -# Convax Plugins +# Convax Plugins and Marketplace The official source registry, authoring kit, and release catalog for Convax -Plugins and portable [Agent Skills](https://agentskills.io/). Published Skills +Plugins, portable [Agent Skills](https://agentskills.io/), and standard MCP +Servers. Published Skills follow the open `SKILL.md` format and can be used by compatible agents such as OpenAI Codex; they are not designed exclusively for Convax. This repository lets people and AI agents start from a template and produce a -Plugin or Skill that can be validated independently, packaged deterministically, +Plugin, Skill, or MCP Server that can be validated independently and published deterministically, and downloaded safely by Convax. Package source is reviewed in Git, immutable ZIPs are published through GitHub Releases, and GitHub Pages hosts the lightweight -Registry at -`https://microvoid.github.io/convax-plugins/registry/v1/index.json`. +Marketplace descriptor at +`https://microvoid.github.io/convax-plugins/marketplace.json`. Current clients use +Registry v2; a strict Registry v1 projection remains available for older clients. ![Animated previews of the Image Remix, Audiobook, and Ecommerce Image Skills](docs/assets/skill-showcases.gif) @@ -54,6 +56,27 @@ The generated Plugin ZIP has `manifest.json` at its root. A Skill ZIP has `SKILL.md` at its root. No dependency install or contributor build script is run while validating or packing a package. +## Create a third-party Marketplace + +The public scaffold and Kit use the same Plugin, Skill, and MCP Server contracts +as this Official Marketplace: + +```sh +bunx create-convax-marketplace@0.1.0 my-market \ + --owner my-org \ + --repository my-market \ + --starter mcp-server +cd my-market +bun run check +bun run build-index +``` + +Add another package with `bun run marketplace -- new plugin --id my-plugin`. +For a reviewed managed-stdio MCP companion, admit one target with +`bun run marketplace -- add-target packages/mcp-servers/example-mcp --target darwin-arm64 --file /path/to/reviewed-companion`; +the bare executable is copied into the private authoring input area and the source +path is never published. + Executable Tool Plugins may be headless. `convax.plugin/3` separates executable tools, model-picker entries, Agent tools, and Canvas selection actions; `convax.plugin/4` and later may also own Skills. Local executable contributions @@ -187,6 +210,9 @@ packages/skills// convax-package.json # Convax publishing metadata; excluded from the ZIP package/ # portable Skill root; SKILL.md must be here showcase/ # optional catalog poster/animation; excluded from ZIP +packages/mcp-servers// + server.json # standard MCP identity/version and fixed HTTPS profile + convax-mcp.json # managed-stdio only packages/tools// # reviewed Tool workspace; separately distributed templates/ # copy-only author starters tooling/ # validation and deterministic ZIP @@ -204,23 +230,25 @@ bun run workspaces:test # test workspaces that declare the script bun test # validator, ZIP, Registry, and protocol tests bun run render:showcases -- --id ad-idea # render one poster and animation bun run build:companions # compile explicitly reviewed platform targets -bun run pack # pack every package into dist/packages -bun run build:index # create matching Registry and Showcase indexes +bun run marketplace:check # validate authoring source through the packed Kit +bun run marketplace:build # build v2/v1, Releases, Builtin, and lock input bun run check # complete local CI sequence ``` -To publish one package, create an annotated tag that exactly matches its metadata: +Marketplace publication dogfoods the packed `@convax/marketplace-kit`. +The repository pins the Kit to exact version `0.1.0`; local source links are not a +valid publication dependency. The matching Kit package must therefore be published +before a clean frozen install of this repository. -```text -plugin--v -skill--v -``` +Authors change a Plugin, Skill, or MCP Server version and merge through protected `main`; +they do not create release tags. The default-branch workflow rejects changed bytes +without a version change, builds deterministic artifacts in a low-privilege job, +then lets a minimal privileged job tag and publish only those verified bytes. +Registry v2, its strict v1 projection, Showcase v2, and the immutable Builtin bundle +are generated rather than hand-edited. -For example: `plugin-hello-convax-v0.1.0`. The publish workflow validates the -tag, creates the deterministic ZIP and Registry entry, attests the ZIP, and creates -a GitHub Release. The Pages workflow rebuilds the catalog from published Release -entries only. Each ordinary Plugin or Skill Release can enter the catalog -independently; unrelated source versions that have not been released do not block it. +`bun run pack` and `bun run build:index` remain legacy v1 compatibility diagnostics. +They do not provide bytes, URLs, or metadata to the v2 release workflow. ## Troubleshooting installation diff --git a/README.zh-CN.md b/README.zh-CN.md index 97d13bf..b25b799 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,15 +1,16 @@ [English](README.md) | [简体中文](README.zh-CN.md) -# Convax 插件与技能仓库 +# Convax 扩展与 Marketplace 仓库 -这是 Convax 插件与可移植 [Agent Skills](https://agentskills.io/) 的官方源码仓库、 -开发工具和发布目录。这里发布的技能遵循开放的 `SKILL.md` 格式,可供 OpenAI Codex -等兼容客户端使用,并非只能在 Convax 中运行。 +这是 Convax Plugin、可移植 [Agent Skills](https://agentskills.io/) 与标准 MCP +Server 的官方源码仓库、开发工具和发布目录。这里发布的技能遵循开放的 `SKILL.md` +格式,可供 OpenAI Codex 等兼容客户端使用,并非只能在 Convax 中运行。 开发者或 AI 可以从模板开始,编写能够独立校验、确定性打包并由 Convax 安全下载的 -插件或技能。包源码通过 Git 进行审查,不可变 ZIP 由 GitHub Releases 发布,轻量 -Registry 由 GitHub Pages 承载: -`https://microvoid.github.io/convax-plugins/registry/v1/index.json`。 +Plugin、Skill 或 MCP Server。包源码通过 Git 进行审查,不可变工件由 GitHub +Releases 发布,Marketplace descriptor 由 GitHub Pages 承载: +`https://microvoid.github.io/convax-plugins/marketplace.json`。新客户端使用 Registry +v2,旧客户端继续使用严格的 Registry v1 投影。 ![图像重绘、有声书和电商图片技能的动态预览](docs/assets/skill-showcases.gif) @@ -48,6 +49,26 @@ bun run pack -- --kind skill --id my-skill 生成的插件 ZIP 在根目录包含 `manifest.json`,技能 ZIP 在根目录包含 `SKILL.md`。校验和打包期间不会安装依赖,也不会执行投稿者提供的构建脚本。 +## 创建第三方 Marketplace + +公开 scaffold 和 Kit 与 Official Marketplace 使用同一套 Plugin、Skill 和 MCP +Server 合约: + +```sh +bunx create-convax-marketplace@0.1.0 my-market \ + --owner my-org \ + --repository my-market \ + --starter mcp-server +cd my-market +bun run check +bun run build-index +``` + +使用 `bun run marketplace -- new plugin --id my-plugin` 新增包。对于经过审查的 +managed-stdio MCP companion,使用 +`bun run marketplace -- add-target packages/mcp-servers/example-mcp --target darwin-arm64 --file /path/to/reviewed-companion` +接纳一个目标;裸可执行文件只会复制到私有作者输入区,源路径不会进入发布结果。 + 可执行工具插件可以是无界面的。`convax.plugin/3` 将可执行工具、模型列表、智能体工具 和画布选中动作分开声明;`convax.plugin/4` 及更高版本还可以拥有 Skill。本地可执行 贡献通过 manifest 声明一个单独安装的裸 `mcp-stdio` 命令,但绝不内嵌可执行文件、 @@ -141,6 +162,9 @@ packages/skills// convax-package.json # Convax 发布元数据,不进入 ZIP package/ # 可移植技能根目录,必须包含 SKILL.md showcase/ # 可选目录封面和动图,不进入 ZIP +packages/mcp-servers// + server.json # 标准 MCP identity/version 和固定 HTTPS profile + convax-mcp.json # 仅 managed-stdio 使用 packages/tools// # 经审查的工具 workspace,单独分发 templates/ # 可直接复制的开发模板 tooling/ # 校验与确定性 ZIP 工具 @@ -158,22 +182,23 @@ bun run workspaces:test # 测试声明了脚本的 workspace bun test # 运行校验器、ZIP、Registry 和协议测试 bun run render:showcases -- --id ad-idea # 渲染单个封面和动图 bun run build:companions # 编译明确审查过的平台目标 -bun run pack # 将全部包写入 dist/packages -bun run build:index # 生成版本一致的 Registry 和 Showcase 索引 +bun run marketplace:check # 通过打包后的 Kit 校验作者源码 +bun run marketplace:build # 生成 v2/v1、Release、Builtin 与 lock input bun run check # 执行完整本地 CI ``` -发布单个包时,需要创建与元数据完全一致的附注标签: +Marketplace 发布直接使用打包后的 `@convax/marketplace-kit`。仓库将 Kit 精确固定为 +`0.1.0`,本地源码 link 不是有效的发布依赖,因此必须先发布对应 Kit 包,干净环境中的 +frozen install 才能成功。 -```text -plugin--v -skill--v -``` +作者只修改 Plugin、 +Skill 或 MCP Server 的 identity version,并通过受保护的 `main` 合入;不再手工创建 +发布标签。默认分支工作流会拒绝“字节变化但 version 未变化”,在低权限 job 中生成 +确定性工件,再由最小权限发布 job 只发布已经验证的精确字节。Registry v2、严格 v1 +投影、Showcase v2 和不可变 Builtin bundle 都由工具生成,不手写。 -例如 `plugin-hello-convax-v0.1.0`。发布工作流会校验标签、生成确定性 ZIP 和 -Registry 条目、为 ZIP 创建来源证明并发布 GitHub Release。Pages 工作流只根据 -已经发布的 Release 条目重建目录。普通插件或技能可以独立进入目录;其他尚未发布的 -源码版本不会阻塞这次更新。 +`bun run pack` 和 `bun run build:index` 只保留为旧 v1 兼容诊断;v2 发布工作流 +不会从它们读取字节、URL 或元数据。 ## 安装问题排查 diff --git a/bun.lock b/bun.lock index a480727..3dd9e10 100644 --- a/bun.lock +++ b/bun.lock @@ -5,62 +5,65 @@ "": { "name": "@microvoid/convax-plugins-registry", "dependencies": { - "acorn": "8.17.0" - } + "acorn": "8.17.0", + }, + "devDependencies": { + "@convax/marketplace-kit": "0.1.0", + }, }, "packages/plugins/chatcut": { "name": "@microvoid/convax-plugin-chatcut", "version": "0.3.1", "dependencies": { "@microvoid/convax-chatcut-media-import-mcp": "workspace:*", - "@microvoid/convax-skill-chatcut": "workspace:*" - } + "@microvoid/convax-skill-chatcut": "workspace:*", + }, }, "packages/plugins/codex-service": { "name": "@microvoid/convax-plugin-codex-service", "version": "0.1.1", "dependencies": { - "@microvoid/convax-codex-mcp": "workspace:*" - } + "@microvoid/convax-codex-mcp": "workspace:*", + }, }, "packages/plugins/convax-pet": { "name": "@microvoid/convax-plugin-convax-pet", - "version": "0.2.2" + "version": "0.2.2", }, "packages/plugins/ffmpeg-tools": { "name": "@microvoid/convax-plugin-ffmpeg-tools", "version": "0.3.1", "dependencies": { "@microvoid/convax-ffmpeg-mcp": "workspace:*", - "@microvoid/convax-skill-ffmpeg-canvas": "workspace:*" - } + "@microvoid/convax-skill-ffmpeg-canvas": "workspace:*", + }, }, "packages/plugins/hello-convax": { "name": "@microvoid/convax-plugin-hello-convax", - "version": "0.1.0" + "version": "0.1.0", }, "packages/plugins/jianying-editor": { "name": "@microvoid/convax-plugin-jianying-editor", - "version": "2.0.0", + "version": "2.1.1", "dependencies": { "@microvoid/convax-jianying-editor-mcp": "workspace:*", - "@microvoid/convax-skill-jianying-editor": "workspace:*" - } + "@microvoid/convax-skill-jianying-editor": "workspace:*", + }, }, "packages/plugins/multi-angle": { "name": "@microvoid/convax-plugin-multi-angle", - "version": "0.1.0" + "version": "0.1.0", }, "packages/plugins/nexus-service": { "name": "@microvoid/convax-plugin-nexus-service", "version": "0.3.8", "dependencies": { - "@microvoid/convax-nexus-mcp": "workspace:*" - } + "@microvoid/convax-nexus-mcp": "workspace:*", + }, }, "packages/plugins/panorama-viewer": { "name": "@microvoid/convax-plugin-panorama-viewer", - "version": "0.2.1" + "version": "0.2.1", }, "packages/plugins/relight-studio": { "name": "@microvoid/convax-plugin-relight-studio", @@ -71,1810 +74,404 @@ "radix-ui": "1.6.2", "react": "19.2.4", "react-dom": "19.2.4", - "typescript": "5.9.3" - } + "typescript": "5.9.3", + }, }, "packages/plugins/storyai-3d-director-desk": { "name": "@microvoid/convax-plugin-storyai-3d-director-desk", - "version": "0.1.0" + "version": "0.1.0", }, "packages/plugins/video-timeline": { "name": "@microvoid/convax-plugin-video-timeline", - "version": "0.1.3" + "version": "0.1.3", }, "packages/plugins/xiaoyunque-generation": { "name": "@microvoid/convax-plugin-xiaoyunque-generation", "version": "0.3.6", "dependencies": { - "@microvoid/convax-xiaoyunque-mcp": "workspace:*" - } + "@microvoid/convax-xiaoyunque-mcp": "workspace:*", + }, }, "packages/skills/ad-idea": { "name": "@microvoid/convax-skill-ad-idea", - "version": "0.3.0" + "version": "0.3.0", }, "packages/skills/audiobook": { "name": "@microvoid/convax-skill-audiobook", - "version": "0.2.0" + "version": "0.2.0", + }, + "packages/skills/canvas-storyboard": { + "name": "@microvoid/convax-skill-canvas-storyboard", + "version": "0.1.0", }, "packages/skills/chatcut": { "name": "@microvoid/convax-skill-chatcut", - "version": "0.3.1" + "version": "0.3.1", }, "packages/skills/clip-export": { "name": "@microvoid/convax-skill-clip-export", - "version": "0.3.0" + "version": "0.3.0", }, "packages/skills/ecommerce-image": { "name": "@microvoid/convax-skill-ecommerce-image", - "version": "0.2.0" + "version": "0.2.0", }, "packages/skills/ffmpeg-canvas": { "name": "@microvoid/convax-skill-ffmpeg-canvas", - "version": "0.3.1" + "version": "0.3.1", }, "packages/skills/film-shot": { "name": "@microvoid/convax-skill-film-shot", - "version": "0.3.0" + "version": "0.3.0", }, "packages/skills/hello-convax-guide": { "name": "@microvoid/convax-skill-hello-convax-guide", - "version": "0.2.0" + "version": "0.2.0", }, "packages/skills/image-remix": { "name": "@microvoid/convax-skill-image-remix", - "version": "0.2.0" + "version": "0.2.0", }, "packages/skills/jianying-editor": { "name": "@microvoid/convax-skill-jianying-editor", - "version": "2.0.0" + "version": "2.0.0", }, "packages/skills/short-drama-screenwriter": { "name": "@microvoid/convax-skill-short-drama-screenwriter", - "version": "0.3.0" + "version": "0.3.0", }, "packages/skills/skill-creator": { "name": "@microvoid/convax-skill-skill-creator", - "version": "0.3.0" + "version": "0.3.0", }, "packages/skills/skill-reviewer": { "name": "@microvoid/convax-skill-skill-reviewer", - "version": "0.3.0" + "version": "0.3.0", }, "packages/skills/video-prompting": { "name": "@microvoid/convax-skill-video-prompting", - "version": "0.3.0" + "version": "0.3.0", }, "packages/tools/chatcut-media-import-mcp": { "name": "@microvoid/convax-chatcut-media-import-mcp", "version": "0.1.1", "bin": { - "convax-chatcut-media-import-mcp": "dist/convax-chatcut-media-import-mcp" + "convax-chatcut-media-import-mcp": "dist/convax-chatcut-media-import-mcp", }, "devDependencies": { "@types/bun": "1.3.14", - "typescript": "5.9.3" - } + "typescript": "5.9.3", + }, }, "packages/tools/codex-mcp": { "name": "@microvoid/convax-codex-mcp", "version": "0.1.1", "bin": { - "convax-codex-mcp": "dist/convax-codex-mcp" + "convax-codex-mcp": "dist/convax-codex-mcp", }, "devDependencies": { "@types/bun": "1.3.14", - "typescript": "5.9.3" - } + "typescript": "5.9.3", + }, }, "packages/tools/ffmpeg-mcp": { "name": "@microvoid/convax-ffmpeg-mcp", "version": "0.2.0", "bin": { - "convax-ffmpeg-mcp": "dist/convax-ffmpeg-mcp" + "convax-ffmpeg-mcp": "dist/convax-ffmpeg-mcp", }, "devDependencies": { "@types/bun": "1.3.14", - "typescript": "5.9.3" - } + "typescript": "5.9.3", + }, }, "packages/tools/jianying-editor-mcp": { "name": "@microvoid/convax-jianying-editor-mcp", - "version": "1.0.0", + "version": "1.1.1", "bin": { - "convax-jianying-editor-mcp": "dist/convax-jianying-editor-mcp" + "convax-jianying-editor-mcp": "dist/convax-jianying-editor-mcp", }, "devDependencies": { "@types/bun": "1.3.14", - "typescript": "5.9.3" - } + "typescript": "5.9.3", + }, }, "packages/tools/nexus-mcp": { "name": "@microvoid/convax-nexus-mcp", "version": "0.3.7", "bin": { - "convax-nexus-mcp": "dist/convax-nexus-mcp" + "convax-nexus-mcp": "dist/convax-nexus-mcp", }, "devDependencies": { "@types/bun": "1.3.14", - "typescript": "5.9.3" - } + "typescript": "5.9.3", + }, }, "packages/tools/xiaoyunque-mcp": { "name": "@microvoid/convax-xiaoyunque-mcp", "version": "0.3.4", "bin": { - "convax-xiaoyunque-mcp": "dist/convax-xiaoyunque-mcp" + "convax-xiaoyunque-mcp": "dist/convax-xiaoyunque-mcp", }, "devDependencies": { "@types/bun": "1.3.14", - "typescript": "5.9.3" - } - } + "typescript": "5.9.3", + }, + }, }, "packages": { - "@floating-ui/core": [ - "@floating-ui/core@1.8.0", - "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", - { "dependencies": { "@floating-ui/utils": "^0.2.12" } }, - "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==" - ], - - "@floating-ui/dom": [ - "@floating-ui/dom@1.8.0", - "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", - { - "dependencies": { - "@floating-ui/core": "^1.8.0", - "@floating-ui/utils": "^0.2.12" - } - }, - "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==" - ], - - "@floating-ui/react-dom": [ - "@floating-ui/react-dom@2.1.9", - "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", - { - "dependencies": { "@floating-ui/dom": "^1.8.0" }, - "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } - }, - "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==" - ], + "@convax/marketplace": ["@convax/marketplace@0.1.0", "", { "dependencies": { "ajv": "8.20.0" } }, "sha512-Etzca0NT5o2is6HMbupFP0xyFzP1DrTzCXWVNSsFmTXgID2UTaR4D4g5tDb8qfyy7TjNIOdQqnhPUbmzObVSdA=="], - "@floating-ui/utils": [ - "@floating-ui/utils@0.2.12", - "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", - {}, - "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==" - ], + "@convax/marketplace-kit": ["@convax/marketplace-kit@0.1.0", "", { "dependencies": { "@convax/marketplace": "^0.1.0" }, "bin": { "convax-marketplace": "dist/cli.js" } }, "sha512-tHlWRQURnRuLyFiajCpeBA/KYukLQlh++nFJC+/q+yqXIwBgkCuGvGTz9BhJ1eklbM8hDnS/dwN7+SHr8v1vag=="], - "@microvoid/convax-chatcut-media-import-mcp": [ - "@microvoid/convax-chatcut-media-import-mcp@workspace:packages/tools/chatcut-media-import-mcp" - ], + "@floating-ui/core": ["@floating-ui/core@1.8.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.12" } }, "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ=="], - "@microvoid/convax-codex-mcp": [ - "@microvoid/convax-codex-mcp@workspace:packages/tools/codex-mcp" - ], + "@floating-ui/dom": ["@floating-ui/dom@1.8.0", "", { "dependencies": { "@floating-ui/core": "^1.8.0", "@floating-ui/utils": "^0.2.12" } }, "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg=="], - "@microvoid/convax-ffmpeg-mcp": [ - "@microvoid/convax-ffmpeg-mcp@workspace:packages/tools/ffmpeg-mcp" - ], + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.9", "", { "dependencies": { "@floating-ui/dom": "^1.8.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg=="], - "@microvoid/convax-jianying-editor-mcp": [ - "@microvoid/convax-jianying-editor-mcp@workspace:packages/tools/jianying-editor-mcp" - ], + "@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="], - "@microvoid/convax-nexus-mcp": [ - "@microvoid/convax-nexus-mcp@workspace:packages/tools/nexus-mcp" - ], + "@microvoid/convax-chatcut-media-import-mcp": ["@microvoid/convax-chatcut-media-import-mcp@workspace:packages/tools/chatcut-media-import-mcp"], - "@microvoid/convax-plugin-chatcut": [ - "@microvoid/convax-plugin-chatcut@workspace:packages/plugins/chatcut" - ], + "@microvoid/convax-codex-mcp": ["@microvoid/convax-codex-mcp@workspace:packages/tools/codex-mcp"], - "@microvoid/convax-plugin-codex-service": [ - "@microvoid/convax-plugin-codex-service@workspace:packages/plugins/codex-service" - ], + "@microvoid/convax-ffmpeg-mcp": ["@microvoid/convax-ffmpeg-mcp@workspace:packages/tools/ffmpeg-mcp"], - "@microvoid/convax-plugin-convax-pet": [ - "@microvoid/convax-plugin-convax-pet@workspace:packages/plugins/convax-pet" - ], + "@microvoid/convax-jianying-editor-mcp": ["@microvoid/convax-jianying-editor-mcp@workspace:packages/tools/jianying-editor-mcp"], - "@microvoid/convax-plugin-ffmpeg-tools": [ - "@microvoid/convax-plugin-ffmpeg-tools@workspace:packages/plugins/ffmpeg-tools" - ], - - "@microvoid/convax-plugin-hello-convax": [ - "@microvoid/convax-plugin-hello-convax@workspace:packages/plugins/hello-convax" - ], - - "@microvoid/convax-plugin-jianying-editor": [ - "@microvoid/convax-plugin-jianying-editor@workspace:packages/plugins/jianying-editor" - ], - - "@microvoid/convax-plugin-multi-angle": [ - "@microvoid/convax-plugin-multi-angle@workspace:packages/plugins/multi-angle" - ], - - "@microvoid/convax-plugin-nexus-service": [ - "@microvoid/convax-plugin-nexus-service@workspace:packages/plugins/nexus-service" - ], - - "@microvoid/convax-plugin-panorama-viewer": [ - "@microvoid/convax-plugin-panorama-viewer@workspace:packages/plugins/panorama-viewer" - ], - - "@microvoid/convax-plugin-relight-studio": [ - "@microvoid/convax-plugin-relight-studio@workspace:packages/plugins/relight-studio" - ], - - "@microvoid/convax-plugin-storyai-3d-director-desk": [ - "@microvoid/convax-plugin-storyai-3d-director-desk@workspace:packages/plugins/storyai-3d-director-desk" - ], - - "@microvoid/convax-plugin-video-timeline": [ - "@microvoid/convax-plugin-video-timeline@workspace:packages/plugins/video-timeline" - ], - - "@microvoid/convax-plugin-xiaoyunque-generation": [ - "@microvoid/convax-plugin-xiaoyunque-generation@workspace:packages/plugins/xiaoyunque-generation" - ], - - "@microvoid/convax-skill-ad-idea": [ - "@microvoid/convax-skill-ad-idea@workspace:packages/skills/ad-idea" - ], - - "@microvoid/convax-skill-audiobook": [ - "@microvoid/convax-skill-audiobook@workspace:packages/skills/audiobook" - ], - - "@microvoid/convax-skill-chatcut": [ - "@microvoid/convax-skill-chatcut@workspace:packages/skills/chatcut" - ], - - "@microvoid/convax-skill-clip-export": [ - "@microvoid/convax-skill-clip-export@workspace:packages/skills/clip-export" - ], - - "@microvoid/convax-skill-ecommerce-image": [ - "@microvoid/convax-skill-ecommerce-image@workspace:packages/skills/ecommerce-image" - ], - - "@microvoid/convax-skill-ffmpeg-canvas": [ - "@microvoid/convax-skill-ffmpeg-canvas@workspace:packages/skills/ffmpeg-canvas" - ], - - "@microvoid/convax-skill-film-shot": [ - "@microvoid/convax-skill-film-shot@workspace:packages/skills/film-shot" - ], - - "@microvoid/convax-skill-hello-convax-guide": [ - "@microvoid/convax-skill-hello-convax-guide@workspace:packages/skills/hello-convax-guide" - ], - - "@microvoid/convax-skill-image-remix": [ - "@microvoid/convax-skill-image-remix@workspace:packages/skills/image-remix" - ], - - "@microvoid/convax-skill-jianying-editor": [ - "@microvoid/convax-skill-jianying-editor@workspace:packages/skills/jianying-editor" - ], - - "@microvoid/convax-skill-short-drama-screenwriter": [ - "@microvoid/convax-skill-short-drama-screenwriter@workspace:packages/skills/short-drama-screenwriter" - ], - - "@microvoid/convax-skill-skill-creator": [ - "@microvoid/convax-skill-skill-creator@workspace:packages/skills/skill-creator" - ], - - "@microvoid/convax-skill-skill-reviewer": [ - "@microvoid/convax-skill-skill-reviewer@workspace:packages/skills/skill-reviewer" - ], - - "@microvoid/convax-skill-video-prompting": [ - "@microvoid/convax-skill-video-prompting@workspace:packages/skills/video-prompting" - ], - - "@microvoid/convax-xiaoyunque-mcp": [ - "@microvoid/convax-xiaoyunque-mcp@workspace:packages/tools/xiaoyunque-mcp" - ], - - "@radix-ui/number": [ - "@radix-ui/number@1.1.2", - "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.2.tgz", - {}, - "sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig==" - ], - - "@radix-ui/primitive": [ - "@radix-ui/primitive@1.1.5", - "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.5.tgz", - {}, - "sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg==" - ], - - "@radix-ui/react-accessible-icon": [ - "@radix-ui/react-accessible-icon@1.1.11", - "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.11.tgz", - { - "dependencies": { "@radix-ui/react-visually-hidden": "1.2.7" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-HQDOFTKwSnmUij6l54wYJJtxTAnxI71+YJLOrjm2ladFB8HAV5Jt7hwaZPhWTGBkYoW4+ZAOfNZrLDh/qvxSYA==" - ], - - "@radix-ui/react-accordion": [ - "@radix-ui/react-accordion@1.2.16", - "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.16.tgz", - { - "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-collapsible": "1.1.16", - "@radix-ui/react-collection": "1.1.12", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-controllable-state": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-BpZJNmetujnGgUI6OX0jEhEmlA46WPqgub8Rv09Kyquwd0cc1ndMKpiPYCjmBU6KSSRPAMtgLpEoZSG/tdNIWQ==" - ], - - "@radix-ui/react-alert-dialog": [ - "@radix-ui/react-alert-dialog@1.1.19", - "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.19.tgz", - { - "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-dialog": "1.1.19", - "@radix-ui/react-primitive": "2.1.7" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-FA7n1f6D/DwGE0+AWxiY5LacNbbExQuEgMubeG06idEaH+mSLuf9dp/qBNqOnvbTQ+4gZ2ue1RATF1Ub91Mg5g==" - ], - - "@radix-ui/react-arrow": [ - "@radix-ui/react-arrow@1.1.11", - "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.11.tgz", - { - "dependencies": { "@radix-ui/react-primitive": "2.1.7" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-Kdil9BB1rIFC/khmf4hC35bn8701AJcizTU7G7cUbEbk5XqqbjDuHW60uUfKqO5WojjZcbAW51Q7P0hRmMLw8A==" - ], - - "@radix-ui/react-aspect-ratio": [ - "@radix-ui/react-aspect-ratio@1.1.11", - "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.11.tgz", - { - "dependencies": { "@radix-ui/react-primitive": "2.1.7" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-IUAhIVpBUvP5NNICjlaB1OFmtRLGqQqTF3ZOSGPoq3XeLXRFtHiWTRxSVEULgOd9GQR2c7tsYqDnhUennapZnw==" - ], - - "@radix-ui/react-avatar": [ - "@radix-ui/react-avatar@1.2.2", - "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.2.2.tgz", - { - "dependencies": { - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-is-hydrated": "0.1.1", - "@radix-ui/react-use-layout-effect": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-sST0qh8GzOB7besQ3tMLWLyngnRuSk0gc/Hm+667KYKQFCt6Y6ZXv25WlqM7dIDK54ULCh5+CHmk4LIolzfz+A==" - ], - - "@radix-ui/react-checkbox": [ - "@radix-ui/react-checkbox@1.3.7", - "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.7.tgz", - { - "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-presence": "1.1.7", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-previous": "1.1.2", - "@radix-ui/react-use-size": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-JroKHfQBfh+fDuzpPsBC+pESkhuq8ql4hljTguz8MWnS35cISr3d/Jhl9kYrB44FlDtxCArYdDvTx+BSsJ64rQ==" - ], - - "@radix-ui/react-collapsible": [ - "@radix-ui/react-collapsible@1.1.16", - "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.16.tgz", - { - "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-presence": "1.1.7", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-layout-effect": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-opfXRe6nnzyGmCDPx+l1Aqo/RbqWtQal2FnsBqF9hhePp6j0LsRoBaRxcMOlTv+uYTJVtWYZKg9t9wTe+BA/ZA==" - ], - - "@radix-ui/react-collection": [ - "@radix-ui/react-collection@1.1.12", - "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.12.tgz", - { - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-slot": "1.3.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-nb67INpE0IahJKN7EYPp9m9YGwYeKlnzxT3MwXVkgCskaSJia97kG4T0ywpjNUSSnoJk/uvk12V8vbrEHEj+/Q==" - ], - - "@radix-ui/react-compose-refs": [ - "@radix-ui/react-compose-refs@1.1.3", - "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.3.tgz", - { - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react"] - }, - "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==" - ], - - "@radix-ui/react-context": [ - "@radix-ui/react-context@1.2.0", - "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.0.tgz", - { - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react"] - }, - "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg==" - ], - - "@radix-ui/react-context-menu": [ - "@radix-ui/react-context-menu@2.3.3", - "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.3.3.tgz", - { - "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-menu": "2.1.20", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-controllable-state": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-PS+gKE0z2prJ74Y0sM+brAGK4mYOHIR7TlcV5EJgUQ6E0xMvyswkK2X4yRqyganrzsRL+WCSKAPu0NQITICRWg==" - ], - - "@radix-ui/react-dialog": [ - "@radix-ui/react-dialog@1.1.19", - "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.19.tgz", - { - "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-dismissable-layer": "1.1.15", - "@radix-ui/react-focus-guards": "1.1.4", - "@radix-ui/react-focus-scope": "1.1.12", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-portal": "1.1.13", - "@radix-ui/react-presence": "1.1.7", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-slot": "1.3.0", - "@radix-ui/react-use-controllable-state": "1.2.3", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.7.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-+HhbN2+YtkRgVirjZ2afMeutQRuGOrdkWR5+EFC58SJojGmtyNQwYzgi6tHBpOxvFHefMtPeHdgtjz0BOGxFQg==" - ], - - "@radix-ui/react-direction": [ - "@radix-ui/react-direction@1.1.2", - "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.2.tgz", - { - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react"] - }, - "sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA==" - ], - - "@radix-ui/react-dismissable-layer": [ - "@radix-ui/react-dismissable-layer@1.1.15", - "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.15.tgz", - { - "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-effect-event": "0.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-b0XaRlzn2QKuo10XyNgi2DAJDf5XC9d1nD3FJcuvCjbR7+4Ad28zmZsLsqx+hvDEzMnRuZaZxZm9gYObV6RmRA==" - ], - - "@radix-ui/react-dropdown-menu": [ - "@radix-ui/react-dropdown-menu@2.1.20", - "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.20.tgz", - { - "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-menu": "2.1.20", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-controllable-state": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-slfm+rRaZRuQBvHq60lXvSVUPhid0IPtjSZzIuUlWZMUs01iYZNlGS3mJgRD3ChLQVBAYlKiL/tFyWGX+dz8Xw==" - ], - - "@radix-ui/react-focus-guards": [ - "@radix-ui/react-focus-guards@1.1.4", - "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.4.tgz", - { - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react"] - }, - "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q==" - ], - - "@radix-ui/react-focus-scope": [ - "@radix-ui/react-focus-scope@1.1.12", - "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.12.tgz", - { - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-callback-ref": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-jjk/lqTeNL0azUx5ZYzVrl4NgaDIrdzTNE4mABV9yBFI7FQqN7pIgzV1bTleUezP2QiTGA1BFTqY8MegDgWX9A==" - ], - - "@radix-ui/react-form": [ - "@radix-ui/react-form@0.1.12", - "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.12.tgz", - { - "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-label": "2.1.11", - "@radix-ui/react-primitive": "2.1.7" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-JTX94E4LDL91rzLg7X0mHPdxr0A8JEdVwZEmeOwZJSMDHCGW5DFtSlTSJozUyUs807IQmnvbfzKZFVCK5DmkqQ==" - ], - - "@radix-ui/react-hover-card": [ - "@radix-ui/react-hover-card@1.1.19", - "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.19.tgz", - { - "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-dismissable-layer": "1.1.15", - "@radix-ui/react-popper": "1.3.3", - "@radix-ui/react-portal": "1.1.13", - "@radix-ui/react-presence": "1.1.7", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-controllable-state": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-2KTgMLQtKvicznQgbindEI2RZ3QbDIwU5gabjUPwFJsormjGDz+rUvO4NANmYwzEEpTcTONUt33vBHIfTIVSfw==" - ], - - "@radix-ui/react-id": [ - "@radix-ui/react-id@1.1.2", - "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.2.tgz", - { - "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react"] - }, - "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==" - ], - - "@radix-ui/react-label": [ - "@radix-ui/react-label@2.1.11", - "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.11.tgz", - { - "dependencies": { "@radix-ui/react-primitive": "2.1.7" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-3PKvDDxOn62k0oV1n4QtNtD2vpu+zYjXR7ojLBPaO6SPvhy53yg0vAmgNeBQeJW5rV3dffoRG+HYfLBZuzw0CQ==" - ], - - "@radix-ui/react-menu": [ - "@radix-ui/react-menu@2.1.20", - "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.20.tgz", - { - "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-collection": "1.1.12", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.15", - "@radix-ui/react-focus-guards": "1.1.4", - "@radix-ui/react-focus-scope": "1.1.12", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-popper": "1.3.3", - "@radix-ui/react-portal": "1.1.13", - "@radix-ui/react-presence": "1.1.7", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-roving-focus": "1.1.15", - "@radix-ui/react-slot": "1.3.0", - "@radix-ui/react-use-callback-ref": "1.1.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.7.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-VsUrXxFe9d2ScbZF0fR/oPR1+qjyeLs5p0jzG8h90puMoA9bq4SirYlXbE+USRg9Q2qTeJSFNqjw2nts8jJe4w==" - ], - - "@radix-ui/react-menubar": [ - "@radix-ui/react-menubar@1.1.20", - "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.20.tgz", - { - "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-collection": "1.1.12", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-menu": "2.1.20", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-roving-focus": "1.1.15", - "@radix-ui/react-use-controllable-state": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-gzFZvybgmwYsFBWDqanycIoEYnhyk8MMnuLamdFVHUZYGp4COM+sqXiwbnn0VMWqGLeeU7GV7jm+dXRa+Wufag==" - ], - - "@radix-ui/react-navigation-menu": [ - "@radix-ui/react-navigation-menu@1.2.18", - "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.18.tgz", - { - "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-collection": "1.1.12", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.15", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-presence": "1.1.7", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-layout-effect": "1.1.2", - "@radix-ui/react-use-previous": "1.1.2", - "@radix-ui/react-visually-hidden": "1.2.7" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-K9HiuxZ6xCwSaHcIuUpxyhy4w5gpwzWjh9dHTSbMN3Ix4qAyVObS9RlU3zMycb0PO3v9Tpk0BXMwWvXOUbVXew==" - ], - - "@radix-ui/react-one-time-password-field": [ - "@radix-ui/react-one-time-password-field@0.1.12", - "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.12.tgz", - { - "dependencies": { - "@radix-ui/number": "1.1.2", - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-collection": "1.1.12", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-roving-focus": "1.1.15", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-effect-event": "0.0.3", - "@radix-ui/react-use-is-hydrated": "0.1.1", - "@radix-ui/react-use-layout-effect": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-nQLu5OAcORDQp1EHAv6k3mJGV1hjMTw2NTGVAsGE1g/mWeNqAd1R5jyaAs3U+A8ZD/W8XNPY2yKT0ZdQnqo3NA==" - ], - - "@radix-ui/react-password-toggle-field": [ - "@radix-ui/react-password-toggle-field@0.1.7", - "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.7.tgz", - { - "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-effect-event": "0.0.3", - "@radix-ui/react-use-is-hydrated": "0.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-gB1Mr8vzdv1XzDjrtJTXmL0JORRs1B4g7ngUs0F+H2VvMOwXTZMTmLCl0wZZ3m7ylX8TssI7NCvgiSHmLuTm/A==" - ], - - "@radix-ui/react-popover": [ - "@radix-ui/react-popover@1.1.19", - "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.19.tgz", - { - "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-dismissable-layer": "1.1.15", - "@radix-ui/react-focus-guards": "1.1.4", - "@radix-ui/react-focus-scope": "1.1.12", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-popper": "1.3.3", - "@radix-ui/react-portal": "1.1.13", - "@radix-ui/react-presence": "1.1.7", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-slot": "1.3.0", - "@radix-ui/react-use-controllable-state": "1.2.3", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.7.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-jkrTdQVxnIB8fpn0NyyxW9CTB5aCXZZelVz5z+Xmii6g5WxMqS3fInNslZ63puP39+Puu4jYohUK31y3dT87gQ==" - ], - - "@radix-ui/react-popper": [ - "@radix-ui/react-popper@1.3.3", - "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.3.tgz", - { - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.11", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.2", - "@radix-ui/react-use-rect": "1.1.2", - "@radix-ui/react-use-size": "1.1.2", - "@radix-ui/rect": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-mS7dGpyjv6b+gsDjLF7e0ia1W4Im1B1hSCy2yuXlHuvnZxHKagfDaobt/KAKt27EpZMit2pss8eJBVyVjEWM+g==" - ], - - "@radix-ui/react-portal": [ - "@radix-ui/react-portal@1.1.13", - "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.13.tgz", - { - "dependencies": { - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-layout-effect": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-z3oXfmaHLJTF1wktbjgD6cn9jiEbq3WSondB10LIuIt2m2Ym4iJlrW04/euMwENDdWDdE7z+OuY7Qyp1YpRSwA==" - ], - - "@radix-ui/react-presence": [ - "@radix-ui/react-presence@1.1.7", - "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.7.tgz", - { - "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-zBZ4QM5XG3JRanDmqXYf3MD6th4AFXFmgU6KNMFzUaV6F3uw9I5/zjMUvFriSEn5ewo1nxuibvyxJdmLlDcslA==" - ], - - "@radix-ui/react-primitive": [ - "@radix-ui/react-primitive@2.1.7", - "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.7.tgz", - { - "dependencies": { "@radix-ui/react-slot": "1.3.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ==" - ], - - "@radix-ui/react-progress": [ - "@radix-ui/react-progress@1.1.12", - "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.12.tgz", - { - "dependencies": { - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-primitive": "2.1.7" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-ZPHyI0JyzoH/rP0tq2uRaIZTj/4s8+kAbqPz+e2N8+ejHvwPJ889dHhqn+vh7PNvNeq+boAoH9yzqeoShzwF2w==" - ], - - "@radix-ui/react-radio-group": [ - "@radix-ui/react-radio-group@1.4.3", - "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.4.3.tgz", - { - "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-presence": "1.1.7", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-roving-focus": "1.1.15", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-previous": "1.1.2", - "@radix-ui/react-use-size": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-WwZFjWV4s3aC1QtR3k04R+oANHtX2q6fgKlc7MCEiDNlnTxCZ3H8k3mHtEgVlOejystwk1WQgarQhNOQZ2bK1g==" - ], - - "@radix-ui/react-roving-focus": [ - "@radix-ui/react-roving-focus@1.1.15", - "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.15.tgz", - { - "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-collection": "1.1.12", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-is-hydrated": "0.1.1", - "@radix-ui/react-use-layout-effect": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-40svmmugfM3mUN7VUDGVE1tQGOhyi8enlGD0CNJEcMM36C1f71PKM21DFgNHUfem0XnA+d8H8oN3Z9ZpJjSslg==" - ], - - "@radix-ui/react-scroll-area": [ - "@radix-ui/react-scroll-area@1.2.14", - "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.14.tgz", - { - "dependencies": { - "@radix-ui/number": "1.1.2", - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-presence": "1.1.7", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-bBODCWZK7JTbQLHs0uIP4f73wIWatakK4OS33UzkR1x897wu0PuO658a3f+6P2GEGyDzGYMuHRatMVoAk9WZTw==" - ], - - "@radix-ui/react-select": [ - "@radix-ui/react-select@2.3.3", - "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.3.3.tgz", - { - "dependencies": { - "@radix-ui/number": "1.1.2", - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-collection": "1.1.12", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.15", - "@radix-ui/react-focus-guards": "1.1.4", - "@radix-ui/react-focus-scope": "1.1.12", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-popper": "1.3.3", - "@radix-ui/react-portal": "1.1.13", - "@radix-ui/react-presence": "1.1.7", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-slot": "1.3.0", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-layout-effect": "1.1.2", - "@radix-ui/react-use-previous": "1.1.2", - "@radix-ui/react-visually-hidden": "1.2.7", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.7.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-L5RQTXz6Anxsf9CCv+pTgiAsUpyVj7rJxsGtmhFaEOJ++cVfXucv4qWfsIO0AIB4NAhi3yovWGVMKKS1Xf1Wrg==" - ], - - "@radix-ui/react-separator": [ - "@radix-ui/react-separator@1.1.11", - "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.11.tgz", - { - "dependencies": { "@radix-ui/react-primitive": "2.1.7" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-jRhe86+8PF7VZ1u14eOWVOuh2BuAhALg/FT1VcMC4OHedMTRUazDnDlKTt+yxo5cRNKHMfmvZ4sSQtWDeMV4CQ==" - ], - - "@radix-ui/react-slider": [ - "@radix-ui/react-slider@1.4.3", - "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.4.3.tgz", - { - "dependencies": { - "@radix-ui/number": "1.1.2", - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-collection": "1.1.12", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-layout-effect": "1.1.2", - "@radix-ui/react-use-previous": "1.1.2", - "@radix-ui/react-use-size": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-CWVVj+XaTom0SKCqw1EUgb0NuiLwS+N3OFG73mVEezKEjgNIvZiu0EevMelSSU+CbX3owbqJweG2gPU31WGC5A==" - ], - - "@radix-ui/react-slot": [ - "@radix-ui/react-slot@1.3.0", - "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.0.tgz", - { - "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react"] - }, - "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==" - ], - - "@radix-ui/react-switch": [ - "@radix-ui/react-switch@1.3.3", - "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.3.3.tgz", - { - "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-previous": "1.1.2", - "@radix-ui/react-use-size": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-1+mlB4/lxJfk5tgJ4g+R5mUCbRpPE1T9+UsEyeLYbGgMtwiMgmuTnfKz4Mw1nHALHjuwyxw4MLd4cSHn6pNSlQ==" - ], - - "@radix-ui/react-tabs": [ - "@radix-ui/react-tabs@1.1.17", - "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.17.tgz", - { - "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-presence": "1.1.7", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-roving-focus": "1.1.15", - "@radix-ui/react-use-controllable-state": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-nRyXnrAVCwjeXcHbvEbLS6ndbTeKHG1RqCP4A8Gw5L4cemDzPXdD8rAmr6wet0v57R69wGvuIIsFjHSVkZiMzQ==" - ], - - "@radix-ui/react-toast": [ - "@radix-ui/react-toast@1.2.19", - "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.19.tgz", - { - "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-collection": "1.1.12", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-dismissable-layer": "1.1.15", - "@radix-ui/react-portal": "1.1.13", - "@radix-ui/react-presence": "1.1.7", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-layout-effect": "1.1.2", - "@radix-ui/react-visually-hidden": "1.2.7" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-SxfVZfVOibWKWdkf0Xx1awW2d09fQu4V4PXDY1j5hi4MVf7MWdJZqTBJMa1KWtOr1S6GGtCk02nniZ0Iia+dHw==" - ], - - "@radix-ui/react-toggle": [ - "@radix-ui/react-toggle@1.1.14", - "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.14.tgz", - { - "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-controllable-state": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-QI/hB65XKWACA66P64A+aHxtLUgHJeJLkaQa+awUNXT6T3swndtY5DojeHA+vldrTspMTtFBd7HfZ9QGbM1Qrw==" - ], - - "@radix-ui/react-toggle-group": [ - "@radix-ui/react-toggle-group@1.1.15", - "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.15.tgz", - { - "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-roving-focus": "1.1.15", - "@radix-ui/react-toggle": "1.1.14", - "@radix-ui/react-use-controllable-state": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-gIC5Q+Xljg7lmUdzSuDoy0t97yZn1sZl00Ra37ZvKrYdWnQLU6sWLd09yG8cIB9jUAlQfHgJ2ACAG00MFwsqSQ==" - ], - - "@radix-ui/react-toolbar": [ - "@radix-ui/react-toolbar@1.1.15", - "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.15.tgz", - { - "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-roving-focus": "1.1.15", - "@radix-ui/react-separator": "1.1.11", - "@radix-ui/react-toggle-group": "1.1.15" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-t/iEuVjUnXXtrsGK40AA43uIx37sn3AqZ7oAVnPICK6lFJP6dzMzWR3U9b6eCfFjb6wtSEqkJ9Rn9xDjiOx20g==" - ], - - "@radix-ui/react-tooltip": [ - "@radix-ui/react-tooltip@1.2.12", - "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.12.tgz", - { - "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-dismissable-layer": "1.1.15", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-popper": "1.3.3", - "@radix-ui/react-portal": "1.1.13", - "@radix-ui/react-presence": "1.1.7", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-slot": "1.3.0", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-visually-hidden": "1.2.7" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-U3HoftgWnmla78vzQbLvKKb7bUYJxoiiqYFzp1wu/TBMyDqMZSuCl3aRICsD6EfVEwcJD2mumGDGUXLFVqQHKA==" - ], - - "@radix-ui/react-use-callback-ref": [ - "@radix-ui/react-use-callback-ref@1.1.2", - "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.2.tgz", - { - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react"] - }, - "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==" - ], - - "@radix-ui/react-use-controllable-state": [ - "@radix-ui/react-use-controllable-state@1.2.3", - "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.3.tgz", - { - "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.3", - "@radix-ui/react-use-layout-effect": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react"] - }, - "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==" - ], - - "@radix-ui/react-use-effect-event": [ - "@radix-ui/react-use-effect-event@0.0.3", - "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.3.tgz", - { - "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react"] - }, - "sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==" - ], - - "@radix-ui/react-use-escape-keydown": [ - "@radix-ui/react-use-escape-keydown@1.1.3", - "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.3.tgz", - { - "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.2" }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react"] - }, - "sha512-3wEkMiPHXha/2VadZ68rYBcmYnPINVGl4Y3gtcM7fKRjANk0OscK+cdqBgUWdozb7YJxsh0vefM7vgAMHXOjqg==" - ], - - "@radix-ui/react-use-is-hydrated": [ - "@radix-ui/react-use-is-hydrated@0.1.1", - "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.1.tgz", - { - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react"] - }, - "sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A==" - ], - - "@radix-ui/react-use-layout-effect": [ - "@radix-ui/react-use-layout-effect@1.1.2", - "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.2.tgz", - { - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react"] - }, - "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==" - ], - - "@radix-ui/react-use-previous": [ - "@radix-ui/react-use-previous@1.1.2", - "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.2.tgz", - { - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react"] - }, - "sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw==" - ], - - "@radix-ui/react-use-rect": [ - "@radix-ui/react-use-rect@1.1.2", - "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.2.tgz", - { - "dependencies": { "@radix-ui/rect": "1.1.2" }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react"] - }, - "sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw==" - ], - - "@radix-ui/react-use-size": [ - "@radix-ui/react-use-size@1.1.2", - "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.2.tgz", - { - "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react"] - }, - "sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w==" - ], - - "@radix-ui/react-visually-hidden": [ - "@radix-ui/react-visually-hidden@1.2.7", - "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.7.tgz", - { - "dependencies": { "@radix-ui/react-primitive": "2.1.7" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-1wNZBggTDK3GRuuQ6nP4k2yi7a6l7I5qbMPbZcRsrGsGVead/f/d5FhEzUvqFs0bcrDLx7n1zKQ3JvLR6whaaw==" - ], - - "@radix-ui/rect": [ - "@radix-ui/rect@1.1.2", - "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.2.tgz", - {}, - "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==" - ], - - "@types/bun": [ - "@types/bun@1.3.14", - "https://registry.npmjs.org/@types/bun/-/bun-1.3.14.tgz", - { "dependencies": { "bun-types": "1.3.14" } }, - "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw==" - ], - - "@types/node": [ - "@types/node@26.1.1", - "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", - { "dependencies": { "undici-types": "~8.3.0" } }, - "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==" - ], - - "@types/react": [ - "@types/react@19.2.14", - "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", - { "dependencies": { "csstype": "^3.2.2" } }, - "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==" - ], - - "@types/react-dom": [ - "@types/react-dom@19.2.3", - "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - { "peerDependencies": { "@types/react": "^19.2.0" } }, - "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==" - ], - - "acorn": [ - "acorn@8.17.0", - "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", - { "bin": { "acorn": "bin/acorn" } }, - "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==" - ], - - "aria-hidden": [ - "aria-hidden@1.2.6", - "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", - { "dependencies": { "tslib": "^2.0.0" } }, - "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==" - ], - - "bun-types": [ - "bun-types@1.3.14", - "https://registry.npmjs.org/bun-types/-/bun-types-1.3.14.tgz", - { "dependencies": { "@types/node": "*" } }, - "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ==" - ], - - "csstype": [ - "csstype@3.2.3", - "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - {}, - "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" - ], - - "detect-node-es": [ - "detect-node-es@1.1.0", - "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - {}, - "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" - ], - - "get-nonce": [ - "get-nonce@1.0.1", - "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - {}, - "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==" - ], - - "radix-ui": [ - "radix-ui@1.6.2", - "https://registry.npmjs.org/radix-ui/-/radix-ui-1.6.2.tgz", - { - "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-accessible-icon": "1.1.11", - "@radix-ui/react-accordion": "1.2.16", - "@radix-ui/react-alert-dialog": "1.1.19", - "@radix-ui/react-arrow": "1.1.11", - "@radix-ui/react-aspect-ratio": "1.1.11", - "@radix-ui/react-avatar": "1.2.2", - "@radix-ui/react-checkbox": "1.3.7", - "@radix-ui/react-collapsible": "1.1.16", - "@radix-ui/react-collection": "1.1.12", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-context-menu": "2.3.3", - "@radix-ui/react-dialog": "1.1.19", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.15", - "@radix-ui/react-dropdown-menu": "2.1.20", - "@radix-ui/react-focus-guards": "1.1.4", - "@radix-ui/react-focus-scope": "1.1.12", - "@radix-ui/react-form": "0.1.12", - "@radix-ui/react-hover-card": "1.1.19", - "@radix-ui/react-label": "2.1.11", - "@radix-ui/react-menu": "2.1.20", - "@radix-ui/react-menubar": "1.1.20", - "@radix-ui/react-navigation-menu": "1.2.18", - "@radix-ui/react-one-time-password-field": "0.1.12", - "@radix-ui/react-password-toggle-field": "0.1.7", - "@radix-ui/react-popover": "1.1.19", - "@radix-ui/react-popper": "1.3.3", - "@radix-ui/react-portal": "1.1.13", - "@radix-ui/react-presence": "1.1.7", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-progress": "1.1.12", - "@radix-ui/react-radio-group": "1.4.3", - "@radix-ui/react-roving-focus": "1.1.15", - "@radix-ui/react-scroll-area": "1.2.14", - "@radix-ui/react-select": "2.3.3", - "@radix-ui/react-separator": "1.1.11", - "@radix-ui/react-slider": "1.4.3", - "@radix-ui/react-slot": "1.3.0", - "@radix-ui/react-switch": "1.3.3", - "@radix-ui/react-tabs": "1.1.17", - "@radix-ui/react-toast": "1.2.19", - "@radix-ui/react-toggle": "1.1.14", - "@radix-ui/react-toggle-group": "1.1.15", - "@radix-ui/react-toolbar": "1.1.15", - "@radix-ui/react-tooltip": "1.2.12", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-effect-event": "0.0.3", - "@radix-ui/react-use-escape-keydown": "1.1.3", - "@radix-ui/react-use-is-hydrated": "0.1.1", - "@radix-ui/react-use-layout-effect": "1.1.2", - "@radix-ui/react-use-size": "1.1.2", - "@radix-ui/react-visually-hidden": "1.2.7" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react", "@types/react-dom"] - }, - "sha512-OwYUjzMwiInCUxgAWpPsavXC3Kh4iyi/49uU1/qZTG3RQDlvegyk1GOMiGvSkjua1RDb3JD3fo3eroL9FV4GQw==" - ], - - "react": [ - "react@19.2.4", - "https://registry.npmjs.org/react/-/react-19.2.4.tgz", - {}, - "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==" - ], - - "react-dom": [ - "react-dom@19.2.4", - "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", - { - "dependencies": { "scheduler": "^0.27.0" }, - "peerDependencies": { "react": "^19.2.4" } - }, - "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==" - ], - - "react-remove-scroll": [ - "react-remove-scroll@2.7.2", - "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", - { - "dependencies": { - "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.3", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react"] - }, - "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==" - ], - - "react-remove-scroll-bar": [ - "react-remove-scroll-bar@2.3.8", - "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", - { - "dependencies": { - "react-style-singleton": "^2.2.2", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "optionalPeers": ["@types/react"] - }, - "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==" - ], - - "react-style-singleton": [ - "react-style-singleton@2.2.3", - "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", - { - "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react"] - }, - "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==" - ], - - "scheduler": [ - "scheduler@0.27.0", - "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - {}, - "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==" - ], - - "tslib": [ - "tslib@2.8.1", - "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - {}, - "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" - ], - - "typescript": [ - "typescript@5.9.3", - "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, - "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==" - ], - - "undici-types": [ - "undici-types@8.3.0", - "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", - {}, - "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==" - ], - - "use-callback-ref": [ - "use-callback-ref@1.3.3", - "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", - { - "dependencies": { "tslib": "^2.0.0" }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react"] - }, - "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==" - ], - - "use-sidecar": [ - "use-sidecar@1.1.3", - "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", - { - "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "optionalPeers": ["@types/react"] - }, - "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==" - ] + "@microvoid/convax-nexus-mcp": ["@microvoid/convax-nexus-mcp@workspace:packages/tools/nexus-mcp"], + + "@microvoid/convax-plugin-chatcut": ["@microvoid/convax-plugin-chatcut@workspace:packages/plugins/chatcut"], + + "@microvoid/convax-plugin-codex-service": ["@microvoid/convax-plugin-codex-service@workspace:packages/plugins/codex-service"], + + "@microvoid/convax-plugin-convax-pet": ["@microvoid/convax-plugin-convax-pet@workspace:packages/plugins/convax-pet"], + + "@microvoid/convax-plugin-ffmpeg-tools": ["@microvoid/convax-plugin-ffmpeg-tools@workspace:packages/plugins/ffmpeg-tools"], + + "@microvoid/convax-plugin-hello-convax": ["@microvoid/convax-plugin-hello-convax@workspace:packages/plugins/hello-convax"], + + "@microvoid/convax-plugin-jianying-editor": ["@microvoid/convax-plugin-jianying-editor@workspace:packages/plugins/jianying-editor"], + + "@microvoid/convax-plugin-multi-angle": ["@microvoid/convax-plugin-multi-angle@workspace:packages/plugins/multi-angle"], + + "@microvoid/convax-plugin-nexus-service": ["@microvoid/convax-plugin-nexus-service@workspace:packages/plugins/nexus-service"], + + "@microvoid/convax-plugin-panorama-viewer": ["@microvoid/convax-plugin-panorama-viewer@workspace:packages/plugins/panorama-viewer"], + + "@microvoid/convax-plugin-relight-studio": ["@microvoid/convax-plugin-relight-studio@workspace:packages/plugins/relight-studio"], + + "@microvoid/convax-plugin-storyai-3d-director-desk": ["@microvoid/convax-plugin-storyai-3d-director-desk@workspace:packages/plugins/storyai-3d-director-desk"], + + "@microvoid/convax-plugin-video-timeline": ["@microvoid/convax-plugin-video-timeline@workspace:packages/plugins/video-timeline"], + + "@microvoid/convax-plugin-xiaoyunque-generation": ["@microvoid/convax-plugin-xiaoyunque-generation@workspace:packages/plugins/xiaoyunque-generation"], + + "@microvoid/convax-skill-ad-idea": ["@microvoid/convax-skill-ad-idea@workspace:packages/skills/ad-idea"], + + "@microvoid/convax-skill-audiobook": ["@microvoid/convax-skill-audiobook@workspace:packages/skills/audiobook"], + + "@microvoid/convax-skill-canvas-storyboard": ["@microvoid/convax-skill-canvas-storyboard@workspace:packages/skills/canvas-storyboard"], + + "@microvoid/convax-skill-chatcut": ["@microvoid/convax-skill-chatcut@workspace:packages/skills/chatcut"], + + "@microvoid/convax-skill-clip-export": ["@microvoid/convax-skill-clip-export@workspace:packages/skills/clip-export"], + + "@microvoid/convax-skill-ecommerce-image": ["@microvoid/convax-skill-ecommerce-image@workspace:packages/skills/ecommerce-image"], + + "@microvoid/convax-skill-ffmpeg-canvas": ["@microvoid/convax-skill-ffmpeg-canvas@workspace:packages/skills/ffmpeg-canvas"], + + "@microvoid/convax-skill-film-shot": ["@microvoid/convax-skill-film-shot@workspace:packages/skills/film-shot"], + + "@microvoid/convax-skill-hello-convax-guide": ["@microvoid/convax-skill-hello-convax-guide@workspace:packages/skills/hello-convax-guide"], + + "@microvoid/convax-skill-image-remix": ["@microvoid/convax-skill-image-remix@workspace:packages/skills/image-remix"], + + "@microvoid/convax-skill-jianying-editor": ["@microvoid/convax-skill-jianying-editor@workspace:packages/skills/jianying-editor"], + + "@microvoid/convax-skill-short-drama-screenwriter": ["@microvoid/convax-skill-short-drama-screenwriter@workspace:packages/skills/short-drama-screenwriter"], + + "@microvoid/convax-skill-skill-creator": ["@microvoid/convax-skill-skill-creator@workspace:packages/skills/skill-creator"], + + "@microvoid/convax-skill-skill-reviewer": ["@microvoid/convax-skill-skill-reviewer@workspace:packages/skills/skill-reviewer"], + + "@microvoid/convax-skill-video-prompting": ["@microvoid/convax-skill-video-prompting@workspace:packages/skills/video-prompting"], + + "@microvoid/convax-xiaoyunque-mcp": ["@microvoid/convax-xiaoyunque-mcp@workspace:packages/tools/xiaoyunque-mcp"], + + "@radix-ui/number": ["@radix-ui/number@1.1.2", "", {}, "sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig=="], + + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.5", "", {}, "sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg=="], + + "@radix-ui/react-accessible-icon": ["@radix-ui/react-accessible-icon@1.1.11", "", { "dependencies": { "@radix-ui/react-visually-hidden": "1.2.7" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-HQDOFTKwSnmUij6l54wYJJtxTAnxI71+YJLOrjm2ladFB8HAV5Jt7hwaZPhWTGBkYoW4+ZAOfNZrLDh/qvxSYA=="], + + "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-collapsible": "1.1.16", "@radix-ui/react-collection": "1.1.12", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-BpZJNmetujnGgUI6OX0jEhEmlA46WPqgub8Rv09Kyquwd0cc1ndMKpiPYCjmBU6KSSRPAMtgLpEoZSG/tdNIWQ=="], + + "@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.19", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-dialog": "1.1.19", "@radix-ui/react-primitive": "2.1.7" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FA7n1f6D/DwGE0+AWxiY5LacNbbExQuEgMubeG06idEaH+mSLuf9dp/qBNqOnvbTQ+4gZ2ue1RATF1Ub91Mg5g=="], + + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.11", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.7" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Kdil9BB1rIFC/khmf4hC35bn8701AJcizTU7G7cUbEbk5XqqbjDuHW60uUfKqO5WojjZcbAW51Q7P0hRmMLw8A=="], + + "@radix-ui/react-aspect-ratio": ["@radix-ui/react-aspect-ratio@1.1.11", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.7" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IUAhIVpBUvP5NNICjlaB1OFmtRLGqQqTF3ZOSGPoq3XeLXRFtHiWTRxSVEULgOd9GQR2c7tsYqDnhUennapZnw=="], + + "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.2.2", "", { "dependencies": { "@radix-ui/react-context": "1.2.0", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-is-hydrated": "0.1.1", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sST0qh8GzOB7besQ3tMLWLyngnRuSk0gc/Hm+667KYKQFCt6Y6ZXv25WlqM7dIDK54ULCh5+CHmk4LIolzfz+A=="], + + "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-presence": "1.1.7", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-use-size": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-JroKHfQBfh+fDuzpPsBC+pESkhuq8ql4hljTguz8MWnS35cISr3d/Jhl9kYrB44FlDtxCArYdDvTx+BSsJ64rQ=="], + + "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-presence": "1.1.7", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-opfXRe6nnzyGmCDPx+l1Aqo/RbqWtQal2FnsBqF9hhePp6j0LsRoBaRxcMOlTv+uYTJVtWYZKg9t9wTe+BA/ZA=="], + + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.12", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-nb67INpE0IahJKN7EYPp9m9YGwYeKlnzxT3MwXVkgCskaSJia97kG4T0ywpjNUSSnoJk/uvk12V8vbrEHEj+/Q=="], + + "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="], + + "@radix-ui/react-context": ["@radix-ui/react-context@1.2.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg=="], + + "@radix-ui/react-context-menu": ["@radix-ui/react-context-menu@2.3.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-menu": "2.1.20", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-PS+gKE0z2prJ74Y0sM+brAGK4mYOHIR7TlcV5EJgUQ6E0xMvyswkK2X4yRqyganrzsRL+WCSKAPu0NQITICRWg=="], + + "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.19", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-dismissable-layer": "1.1.15", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.12", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-portal": "1.1.13", "@radix-ui/react-presence": "1.1.7", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-slot": "1.3.0", "@radix-ui/react-use-controllable-state": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-+HhbN2+YtkRgVirjZ2afMeutQRuGOrdkWR5+EFC58SJojGmtyNQwYzgi6tHBpOxvFHefMtPeHdgtjz0BOGxFQg=="], + + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA=="], + + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-effect-event": "0.0.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-b0XaRlzn2QKuo10XyNgi2DAJDf5XC9d1nD3FJcuvCjbR7+4Ad28zmZsLsqx+hvDEzMnRuZaZxZm9gYObV6RmRA=="], + + "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.20", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-menu": "2.1.20", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-slfm+rRaZRuQBvHq60lXvSVUPhid0IPtjSZzIuUlWZMUs01iYZNlGS3mJgRD3ChLQVBAYlKiL/tFyWGX+dz8Xw=="], + + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q=="], + + "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.12", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jjk/lqTeNL0azUx5ZYzVrl4NgaDIrdzTNE4mABV9yBFI7FQqN7pIgzV1bTleUezP2QiTGA1BFTqY8MegDgWX9A=="], + + "@radix-ui/react-form": ["@radix-ui/react-form@0.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-label": "2.1.11", "@radix-ui/react-primitive": "2.1.7" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-JTX94E4LDL91rzLg7X0mHPdxr0A8JEdVwZEmeOwZJSMDHCGW5DFtSlTSJozUyUs807IQmnvbfzKZFVCK5DmkqQ=="], + + "@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.1.19", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-dismissable-layer": "1.1.15", "@radix-ui/react-popper": "1.3.3", "@radix-ui/react-portal": "1.1.13", "@radix-ui/react-presence": "1.1.7", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-2KTgMLQtKvicznQgbindEI2RZ3QbDIwU5gabjUPwFJsormjGDz+rUvO4NANmYwzEEpTcTONUt33vBHIfTIVSfw=="], + + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="], + + "@radix-ui/react-label": ["@radix-ui/react-label@2.1.11", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.7" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3PKvDDxOn62k0oV1n4QtNtD2vpu+zYjXR7ojLBPaO6SPvhy53yg0vAmgNeBQeJW5rV3dffoRG+HYfLBZuzw0CQ=="], + + "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.20", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-collection": "1.1.12", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.15", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.12", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-popper": "1.3.3", "@radix-ui/react-portal": "1.1.13", "@radix-ui/react-presence": "1.1.7", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-roving-focus": "1.1.15", "@radix-ui/react-slot": "1.3.0", "@radix-ui/react-use-callback-ref": "1.1.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-VsUrXxFe9d2ScbZF0fR/oPR1+qjyeLs5p0jzG8h90puMoA9bq4SirYlXbE+USRg9Q2qTeJSFNqjw2nts8jJe4w=="], + + "@radix-ui/react-menubar": ["@radix-ui/react-menubar@1.1.20", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-collection": "1.1.12", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-menu": "2.1.20", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-roving-focus": "1.1.15", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-gzFZvybgmwYsFBWDqanycIoEYnhyk8MMnuLamdFVHUZYGp4COM+sqXiwbnn0VMWqGLeeU7GV7jm+dXRa+Wufag=="], + + "@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.18", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-collection": "1.1.12", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.15", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-presence": "1.1.7", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-layout-effect": "1.1.2", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-visually-hidden": "1.2.7" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-K9HiuxZ6xCwSaHcIuUpxyhy4w5gpwzWjh9dHTSbMN3Ix4qAyVObS9RlU3zMycb0PO3v9Tpk0BXMwWvXOUbVXew=="], + + "@radix-ui/react-one-time-password-field": ["@radix-ui/react-one-time-password-field@0.1.12", "", { "dependencies": { "@radix-ui/number": "1.1.2", "@radix-ui/primitive": "1.1.5", "@radix-ui/react-collection": "1.1.12", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-roving-focus": "1.1.15", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-effect-event": "0.0.3", "@radix-ui/react-use-is-hydrated": "0.1.1", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-nQLu5OAcORDQp1EHAv6k3mJGV1hjMTw2NTGVAsGE1g/mWeNqAd1R5jyaAs3U+A8ZD/W8XNPY2yKT0ZdQnqo3NA=="], + + "@radix-ui/react-password-toggle-field": ["@radix-ui/react-password-toggle-field@0.1.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-effect-event": "0.0.3", "@radix-ui/react-use-is-hydrated": "0.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-gB1Mr8vzdv1XzDjrtJTXmL0JORRs1B4g7ngUs0F+H2VvMOwXTZMTmLCl0wZZ3m7ylX8TssI7NCvgiSHmLuTm/A=="], + + "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.19", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-dismissable-layer": "1.1.15", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.12", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-popper": "1.3.3", "@radix-ui/react-portal": "1.1.13", "@radix-ui/react-presence": "1.1.7", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-slot": "1.3.0", "@radix-ui/react-use-controllable-state": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jkrTdQVxnIB8fpn0NyyxW9CTB5aCXZZelVz5z+Xmii6g5WxMqS3fInNslZ63puP39+Puu4jYohUK31y3dT87gQ=="], + + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.3.3", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.11", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.2", "@radix-ui/react-use-rect": "1.1.2", "@radix-ui/react-use-size": "1.1.2", "@radix-ui/rect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-mS7dGpyjv6b+gsDjLF7e0ia1W4Im1B1hSCy2yuXlHuvnZxHKagfDaobt/KAKt27EpZMit2pss8eJBVyVjEWM+g=="], + + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.13", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-z3oXfmaHLJTF1wktbjgD6cn9jiEbq3WSondB10LIuIt2m2Ym4iJlrW04/euMwENDdWDdE7z+OuY7Qyp1YpRSwA=="], + + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.7", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zBZ4QM5XG3JRanDmqXYf3MD6th4AFXFmgU6KNMFzUaV6F3uw9I5/zjMUvFriSEn5ewo1nxuibvyxJdmLlDcslA=="], + + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.7", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ=="], + + "@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.12", "", { "dependencies": { "@radix-ui/react-context": "1.2.0", "@radix-ui/react-primitive": "2.1.7" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ZPHyI0JyzoH/rP0tq2uRaIZTj/4s8+kAbqPz+e2N8+ejHvwPJ889dHhqn+vh7PNvNeq+boAoH9yzqeoShzwF2w=="], + + "@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.4.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-presence": "1.1.7", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-roving-focus": "1.1.15", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-use-size": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-WwZFjWV4s3aC1QtR3k04R+oANHtX2q6fgKlc7MCEiDNlnTxCZ3H8k3mHtEgVlOejystwk1WQgarQhNOQZ2bK1g=="], + + "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-collection": "1.1.12", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-is-hydrated": "0.1.1", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-40svmmugfM3mUN7VUDGVE1tQGOhyi8enlGD0CNJEcMM36C1f71PKM21DFgNHUfem0XnA+d8H8oN3Z9ZpJjSslg=="], + + "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.14", "", { "dependencies": { "@radix-ui/number": "1.1.2", "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-presence": "1.1.7", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bBODCWZK7JTbQLHs0uIP4f73wIWatakK4OS33UzkR1x897wu0PuO658a3f+6P2GEGyDzGYMuHRatMVoAk9WZTw=="], + + "@radix-ui/react-select": ["@radix-ui/react-select@2.3.3", "", { "dependencies": { "@radix-ui/number": "1.1.2", "@radix-ui/primitive": "1.1.5", "@radix-ui/react-collection": "1.1.12", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.15", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.12", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-popper": "1.3.3", "@radix-ui/react-portal": "1.1.13", "@radix-ui/react-presence": "1.1.7", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-slot": "1.3.0", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-layout-effect": "1.1.2", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-visually-hidden": "1.2.7", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-L5RQTXz6Anxsf9CCv+pTgiAsUpyVj7rJxsGtmhFaEOJ++cVfXucv4qWfsIO0AIB4NAhi3yovWGVMKKS1Xf1Wrg=="], + + "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.11", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.7" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jRhe86+8PF7VZ1u14eOWVOuh2BuAhALg/FT1VcMC4OHedMTRUazDnDlKTt+yxo5cRNKHMfmvZ4sSQtWDeMV4CQ=="], + + "@radix-ui/react-slider": ["@radix-ui/react-slider@1.4.3", "", { "dependencies": { "@radix-ui/number": "1.1.2", "@radix-ui/primitive": "1.1.5", "@radix-ui/react-collection": "1.1.12", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-layout-effect": "1.1.2", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-use-size": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-CWVVj+XaTom0SKCqw1EUgb0NuiLwS+N3OFG73mVEezKEjgNIvZiu0EevMelSSU+CbX3owbqJweG2gPU31WGC5A=="], + + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + + "@radix-ui/react-switch": ["@radix-ui/react-switch@1.3.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-use-size": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1+mlB4/lxJfk5tgJ4g+R5mUCbRpPE1T9+UsEyeLYbGgMtwiMgmuTnfKz4Mw1nHALHjuwyxw4MLd4cSHn6pNSlQ=="], + + "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.17", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-presence": "1.1.7", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-roving-focus": "1.1.15", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-nRyXnrAVCwjeXcHbvEbLS6ndbTeKHG1RqCP4A8Gw5L4cemDzPXdD8rAmr6wet0v57R69wGvuIIsFjHSVkZiMzQ=="], + + "@radix-ui/react-toast": ["@radix-ui/react-toast@1.2.19", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-collection": "1.1.12", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-dismissable-layer": "1.1.15", "@radix-ui/react-portal": "1.1.13", "@radix-ui/react-presence": "1.1.7", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-layout-effect": "1.1.2", "@radix-ui/react-visually-hidden": "1.2.7" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-SxfVZfVOibWKWdkf0Xx1awW2d09fQu4V4PXDY1j5hi4MVf7MWdJZqTBJMa1KWtOr1S6GGtCk02nniZ0Iia+dHw=="], + + "@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-QI/hB65XKWACA66P64A+aHxtLUgHJeJLkaQa+awUNXT6T3swndtY5DojeHA+vldrTspMTtFBd7HfZ9QGbM1Qrw=="], + + "@radix-ui/react-toggle-group": ["@radix-ui/react-toggle-group@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-roving-focus": "1.1.15", "@radix-ui/react-toggle": "1.1.14", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-gIC5Q+Xljg7lmUdzSuDoy0t97yZn1sZl00Ra37ZvKrYdWnQLU6sWLd09yG8cIB9jUAlQfHgJ2ACAG00MFwsqSQ=="], + + "@radix-ui/react-toolbar": ["@radix-ui/react-toolbar@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-roving-focus": "1.1.15", "@radix-ui/react-separator": "1.1.11", "@radix-ui/react-toggle-group": "1.1.15" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t/iEuVjUnXXtrsGK40AA43uIx37sn3AqZ7oAVnPICK6lFJP6dzMzWR3U9b6eCfFjb6wtSEqkJ9Rn9xDjiOx20g=="], + + "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-dismissable-layer": "1.1.15", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-popper": "1.3.3", "@radix-ui/react-portal": "1.1.13", "@radix-ui/react-presence": "1.1.7", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-slot": "1.3.0", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-visually-hidden": "1.2.7" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-U3HoftgWnmla78vzQbLvKKb7bUYJxoiiqYFzp1wu/TBMyDqMZSuCl3aRICsD6EfVEwcJD2mumGDGUXLFVqQHKA=="], + + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw=="], + + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.3", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.3", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA=="], + + "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.3", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA=="], + + "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.3", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-3wEkMiPHXha/2VadZ68rYBcmYnPINVGl4Y3gtcM7fKRjANk0OscK+cdqBgUWdozb7YJxsh0vefM7vgAMHXOjqg=="], + + "@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A=="], + + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + + "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw=="], + + "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.2", "", { "dependencies": { "@radix-ui/rect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw=="], + + "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w=="], + + "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.7" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1wNZBggTDK3GRuuQ6nP4k2yi7a6l7I5qbMPbZcRsrGsGVead/f/d5FhEzUvqFs0bcrDLx7n1zKQ3JvLR6whaaw=="], + + "@radix-ui/rect": ["@radix-ui/rect@1.1.2", "", {}, "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA=="], + + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + + "@types/node": ["@types/node@26.1.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="], + + "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], + + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + + "acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="], + + "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + + "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], + + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-uri": ["fast-uri@3.1.4", "", {}, "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw=="], + + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "radix-ui": ["radix-ui@1.6.2", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-accessible-icon": "1.1.11", "@radix-ui/react-accordion": "1.2.16", "@radix-ui/react-alert-dialog": "1.1.19", "@radix-ui/react-arrow": "1.1.11", "@radix-ui/react-aspect-ratio": "1.1.11", "@radix-ui/react-avatar": "1.2.2", "@radix-ui/react-checkbox": "1.3.7", "@radix-ui/react-collapsible": "1.1.16", "@radix-ui/react-collection": "1.1.12", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-context-menu": "2.3.3", "@radix-ui/react-dialog": "1.1.19", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.15", "@radix-ui/react-dropdown-menu": "2.1.20", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.12", "@radix-ui/react-form": "0.1.12", "@radix-ui/react-hover-card": "1.1.19", "@radix-ui/react-label": "2.1.11", "@radix-ui/react-menu": "2.1.20", "@radix-ui/react-menubar": "1.1.20", "@radix-ui/react-navigation-menu": "1.2.18", "@radix-ui/react-one-time-password-field": "0.1.12", "@radix-ui/react-password-toggle-field": "0.1.7", "@radix-ui/react-popover": "1.1.19", "@radix-ui/react-popper": "1.3.3", "@radix-ui/react-portal": "1.1.13", "@radix-ui/react-presence": "1.1.7", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-progress": "1.1.12", "@radix-ui/react-radio-group": "1.4.3", "@radix-ui/react-roving-focus": "1.1.15", "@radix-ui/react-scroll-area": "1.2.14", "@radix-ui/react-select": "2.3.3", "@radix-ui/react-separator": "1.1.11", "@radix-ui/react-slider": "1.4.3", "@radix-ui/react-slot": "1.3.0", "@radix-ui/react-switch": "1.3.3", "@radix-ui/react-tabs": "1.1.17", "@radix-ui/react-toast": "1.2.19", "@radix-ui/react-toggle": "1.1.14", "@radix-ui/react-toggle-group": "1.1.15", "@radix-ui/react-toolbar": "1.1.15", "@radix-ui/react-tooltip": "1.2.12", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-effect-event": "0.0.3", "@radix-ui/react-use-escape-keydown": "1.1.3", "@radix-ui/react-use-is-hydrated": "0.1.1", "@radix-ui/react-use-layout-effect": "1.1.2", "@radix-ui/react-use-size": "1.1.2", "@radix-ui/react-visually-hidden": "1.2.7" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-OwYUjzMwiInCUxgAWpPsavXC3Kh4iyi/49uU1/qZTG3RQDlvegyk1GOMiGvSkjua1RDb3JD3fo3eroL9FV4GQw=="], + + "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], + + "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], + + "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], + + "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + + "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], } } diff --git a/catalogs/builtin.json b/catalogs/builtin.json new file mode 100644 index 0000000..d56b82e --- /dev/null +++ b/catalogs/builtin.json @@ -0,0 +1,9 @@ +{ + "schema": "convax.builtin-config/1", + "members": [ + { + "kind": "skill", + "id": "canvas-storyboard" + } + ] +} diff --git a/catalogs/preinstalled.json b/catalogs/preinstalled.json new file mode 100644 index 0000000..b8091b5 --- /dev/null +++ b/catalogs/preinstalled.json @@ -0,0 +1,14 @@ +{ + "schema": "convax.preinstalled-config/1", + "packages": [ + { + "marketplaceId": "convax-official", + "kind": "plugin", + "id": "ffmpeg-tools", + "targets": [ + "darwin-arm64" + ], + "setup": "explicit" + } + ] +} diff --git a/docs/packaging.md b/docs/packaging.md index f02c849..d61120b 100644 --- a/docs/packaging.md +++ b/docs/packaging.md @@ -18,6 +18,8 @@ packages///convax-package.json # catalog metadata, not shipped packages///package.json # workspace dependencies/scripts, not shipped packages///package/ # exact ZIP root packages///showcase/ # optional catalog media, not shipped +packages/mcp-servers//server.json # standard MCP identity and version +packages/mcp-servers//convax-mcp.json # managed-stdio only; never HTTP ``` Plugin ZIPs require root `manifest.json`; Skill ZIPs require root `SKILL.md`. @@ -132,6 +134,7 @@ bun run build:companions bun run pack ``` +The legacy compatibility command `bun run pack -- --kind plugin --id hello-convax` writes a versioned ZIP and `registry-entry.json` below `dist/packages/`. `bun run build:index` reads those entries and writes `dist/registry/v1/index.json`. A package with `showcase` @@ -141,48 +144,56 @@ revision as the Registry. A package with `companions` additionally produces one standalone `convax-companion-*` Release asset per target; the Registry entry records its immutable URL, exact byte size, and SHA-256. -## Release - -Publish a reviewed package by pushing an annotated tag: - -```sh -git tag -a plugin-hello-convax-v0.1.0 -m "hello-convax 0.1.0" -git push origin plugin-hello-convax-v0.1.0 -``` - -Push batch tags in separate `git push` invocations and confirm that every tag -creates a **Publish package** run. GitHub does not create tag push events when more -than three tags are pushed at once, so a single bulk push can leave valid tags with -no Releases. See GitHub's -[push event documentation](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#push). - -The tag must match source metadata exactly. The workflow validates all packages, -cross-compiles the reviewed target, packs only the tagged package, attests its ZIP -and each companion, creates a draft Release, uploads the ZIP plus -`registry-entry.json`, companions, and any declared Showcase assets, and only then -publishes it. Published versions are +Neither directory is a v2 publication input. Official v2, its strict v1 +projection, Showcase v2, grouped Release assets, Builtin bundle, and product-lock +input come only from the exact `@convax/marketplace-kit` output. + +`bun run marketplace:verify` independently closes those outputs before +publication. It checks the strict v1 identity projection, every Registry Release +reference, immutable metadata copies, the Builtin reservation, and the sole +`ffmpeg-tools` preinstall with its owned Skill and darwin-arm64 companion. Product +lock reads are bounded, no-follow, single-link reads whose opened inode and size +must remain stable through hashing. + +## Protected default-branch release + +Authors change an extension's identity version and merge through the protected +default branch. The low-privilege job compares the previous and next Git trees, +rejects any changed package bytes whose version did not change, runs the complete +check, and uploads only the exact verified artifacts. A separate minimal +high-privilege job consumes those bytes, creates the deterministic tag, attests the +artifacts, and publishes the immutable Release. Pull requests never receive release +credentials and `pull_request_target` is not used. + +Plugin and Skill tags retain `--v`. Namespaced MCP Server ids are +never embedded in native paths or tags; the release tag uses the stable hashed item +key emitted by the Kit. Published versions are immutable; never move or reuse a tag. Change bytes by publishing a higher SemVer. To -disable a compromised version, add its `kind/id@version` identity to -`registry/config.json`, bump the Registry sequence, and manually run the protected -Pages workflow. This changes catalog policy without replacing the old asset. - -The Pages workflow aggregates entry documents from GitHub Releases and publishes -only valid entries. An ordinary Plugin or Skill becomes eligible as soon as its own -Release succeeds; a different package's unreleased source version does not block it. -For a Plugin-owned Skill, Pages retains the previously published owner/Skill pair -until the current source versions of the owner and all of its owned Skills have -matching Releases. This prevents a partially published ownership group from replacing -the working pair without imposing a repository-wide release barrier. The production -catalog is: +disable a compromised version for new installs, publish a reviewed higher package +version with `yanked: true`. Existing immutable assets remain available for +inventory, recovery, and audit. + +The serialized workflow fetches and strictly validates the current production v2 +Registry as an explicit sequence input. The one-time bootstrap accepts strict v1 +only after an exact v2 HTTP 404 and inherits only its sequence high-water mark. +Every other network or validation failure stops publication. The Kit then writes +one grouped directory per immutable package Release plus one content-addressed +Registry metadata Release. A changed Storyboard source also publishes the matching +Builtin bundle Release. The privileged job consumes only those verified directories, +supports exact-byte retry recovery, and invokes the reusable Pages deployment before +releasing the repository-wide publication lock. + +The production catalogs are: `https://microvoid.github.io/convax-plugins/registry/v1/index.json` +`https://microvoid.github.io/convax-plugins/registry/v2/index.json` + The matching presentation sidecar is: -`https://microvoid.github.io/convax-plugins/showcase/v1/index.json` +`https://microvoid.github.io/convax-plugins/showcase/v2/index.json` -Each Pages deployment advances `sequence` beyond the currently deployed Registry, -using `registry/config.json` as a minimum floor. Consequently, multiple package tags -from the same `main` revision may update the catalog one at a time without reusing a -sequence. `revision` continues to identify the protected `main` source used by the -builder. +Each content-changing deployment uses +`max(registry/config.json floor, previous production sequence) + 1` for both v2 and +v1. Registry v2 revision is the canonical content SHA-256; the v1 projection keeps +the explicit protected Git SHA required by existing clients. diff --git a/docs/registry-spec.md b/docs/registry-spec.md index 80de12c..0842efc 100644 --- a/docs/registry-spec.md +++ b/docs/registry-spec.md @@ -1,4 +1,32 @@ -# Registry contract (`convax.registry/1`) +# Marketplace and Registry contracts + +`marketplace.json` is the strict `convax.marketplace/1` descriptor for +`convax-official`. New Convax clients read its `convax.registry/2` and +`convax.showcase/2` links. Registry v2 is source-qualified by +`marketplaceId: "convax-official"` and supports `plugin`, `skill`, and +`mcp-server` as first-class kinds. + +Every v2 item contains `kind`, `id`, `version`, Convax compatibility, +presentation, delivery, and yanked state. Plugin items retain their complete +validated manifest and companion closure; Skill items retain `ownerPluginId`. +Artifact delivery fixes an immutable Release URL, byte size, and SHA-256. +HTTP MCP delivery embeds the reviewed standard `server.json`, its digest, and one +normalized fixed HTTPS runtime. Managed-stdio delivery additionally embeds the +strict `convax-mcp.json` and an exact target-specific companion closure. MCP +metadata is not a ZIP and HTTP MCP never has a companion. + +Each revision contains at most one current entry per `{kind,id}`. The sequence is +monotonic, and reusing one `{kind,id,version}` for changed metadata or artifact +bytes is invalid. Source history and installed immutable bytes, not a range +resolver, retain old versions. + +## Strict Official v1 projection + +The Official builder also emits the existing strict `convax.registry/1` +projection for older clients. Its schema, top-level fields, Plugin/Skill enum, and +entry fields remain compatible with `schemas/convax-registry-v1.schema.json`: it +does not gain `marketplaceId` or MCP Server entries. The projection is generated +from v2-capable source packages; it is not independently authored. The Registry is metadata, not an execution endpoint. Convax fetches it from the fixed Microvoid Pages URL, selects a compatible item, downloads the fixed HTTPS diff --git a/marketplace.json b/marketplace.json new file mode 100644 index 0000000..072b39e --- /dev/null +++ b/marketplace.json @@ -0,0 +1,31 @@ +{ + "schema": "convax.marketplace/1", + "id": "convax-official", + "name": "Convax Official", + "publisher": { + "name": "Microvoid" + }, + "repository": { + "owner": "microvoid", + "name": "convax-plugins" + }, + "registry": { + "v2": { + "url": "https://microvoid.github.io/convax-plugins/registry/v2/index.json" + }, + "v1": { + "url": "https://microvoid.github.io/convax-plugins/registry/v1/index.json" + } + }, + "showcase": { + "v2": { + "url": "https://microvoid.github.io/convax-plugins/showcase/v2/index.json" + } + }, + "compatibility": { + "convax": ">=0.1.0" + }, + "delivery": { + "kind": "github-pages-releases" + } +} diff --git a/package.json b/package.json index 2b96e9d..3d59bc2 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,14 @@ "dependencies": { "acorn": "8.17.0" }, + "devDependencies": { + "@convax/marketplace-kit": "0.1.0" + }, "packageManager": "bun@1.3.14", "workspaces": [ "packages/plugins/*", "packages/skills/*", + "packages/mcp-servers/*", "packages/tools/*" ], "scripts": { @@ -18,11 +22,17 @@ "pack": "bun tooling/pack.mjs", "render:showcases": "bun tooling/render-showcases.mjs", "build:index": "bun tooling/build-index.mjs && bun tooling/build-showcase.mjs", + "marketplace:check": "convax-marketplace check .", + "marketplace:build-index": "bun tooling/official-marketplace-build.mjs", + "marketplace:bundle": "convax-marketplace bundle . --out dist/builtin", + "marketplace:lock-input": "convax-marketplace lock-input --catalog dist/catalog --builtin dist/builtin --out dist/product-lock-input.json", + "marketplace:verify": "bun tooling/verify-marketplace-output.mjs dist/catalog && bun tooling/verify-product-lock-input.mjs dist/product-lock-input.json", + "marketplace:build": "bun run marketplace:bundle && bun run marketplace:build-index && bun run marketplace:lock-input && bun run marketplace:verify", "build:companions": "bun tooling/build-companions.mjs", "workspaces:typecheck": "bun tooling/run-workspace-script.mjs typecheck", "workspaces:test": "bun tooling/run-workspace-script.mjs test", "workspaces:build": "bun tooling/run-workspace-script.mjs build", "workspaces:build:packages": "bun tooling/run-workspace-script.mjs build skills plugins", - "check": "bun run workspaces:build:packages && bun run validate && bun run workspaces:typecheck && bun run workspaces:test && bun run build:companions && bun run test && bun run pack && bun run build:index" + "check": "bun run workspaces:build:packages && bun run validate && bun run workspaces:typecheck && bun run workspaces:test && bun run build:companions && bun run marketplace:check && bun run test && bun run marketplace:build" } } diff --git a/packages/plugins/jianying-editor/convax-package.json b/packages/plugins/jianying-editor/convax-package.json index 4b6fe08..b713cd7 100644 --- a/packages/plugins/jianying-editor/convax-package.json +++ b/packages/plugins/jianying-editor/convax-package.json @@ -4,7 +4,7 @@ "id": "jianying-editor", "name": "剪映导入", "description": "将直接连接的 Canvas 图片和视频安全导入剪映当前草稿或新草稿。", - "version": "2.0.0", + "version": "2.1.1", "license": "MIT", "compatibility": { "pluginSchema": "convax.plugin/6", @@ -13,7 +13,7 @@ "companions": [ { "command": "convax-jianying-editor-mcp", - "version": "1.0.0", + "version": "1.1.1", "source": "packages/tools/jianying-editor-mcp", "targets": [ { diff --git a/packages/plugins/jianying-editor/package.json b/packages/plugins/jianying-editor/package.json index 5c233a3..dbd5fbe 100644 --- a/packages/plugins/jianying-editor/package.json +++ b/packages/plugins/jianying-editor/package.json @@ -1,6 +1,6 @@ { "name": "@microvoid/convax-plugin-jianying-editor", - "version": "2.0.0", + "version": "2.1.1", "private": true, "type": "module", "dependencies": { diff --git a/packages/plugins/jianying-editor/package/README.md b/packages/plugins/jianying-editor/package/README.md index cddd24a..21a3d2b 100644 --- a/packages/plugins/jianying-editor/package/README.md +++ b/packages/plugins/jianying-editor/package/README.md @@ -12,3 +12,7 @@ loopback 文件传输,不包含 Canvas、Project、IPC 或 Convax Desktop 逻 Agent 工作流由插件拥有的 `jianying-editor` Skill 约束:先检查草稿,当前 草稿存在时必须让用户选择;新草稿只能从安全确认的无活动草稿状态创建。 不确定或部分传输结果不会自动重试,以免重复导入。 + +安装并完成 companion 设置后,普通 Canvas 图片和视频节点的工具栏会显示 +“导入剪映”。该动作只接受当前单选节点,由宿主暂存并立即复核素材;稳定的 +当前草稿会直接复用,没有活动草稿时安全创建新草稿,歧义或变化状态则停止。 diff --git a/packages/plugins/jianying-editor/package/manifest.json b/packages/plugins/jianying-editor/package/manifest.json index f4e81c5..9ce0723 100644 --- a/packages/plugins/jianying-editor/package/manifest.json +++ b/packages/plugins/jianying-editor/package/manifest.json @@ -3,7 +3,7 @@ "id": "jianying-editor", "name": "剪映导入", "description": "将直接连接的 Canvas 图片和视频安全导入剪映当前草稿或新草稿。", - "version": "2.0.0", + "version": "2.1.1", "entry": "index.html", "capabilities": [ "canvas.connectedInputs.read", @@ -44,6 +44,17 @@ "reference_image", "reference_video" ] + }, + { + "id": "media.import-selected", + "title": "导入所选素材到剪映", + "description": "将一个由宿主暂存的 Canvas 图片或视频导入稳定的剪映当前草稿,或安全创建新草稿。", + "output": "text", + "delivery": "return", + "acceptedInputs": [ + "reference_image", + "reference_video" + ] } ] }, @@ -52,7 +63,45 @@ "create": true, "width": 600, "height": 440 - } + }, + "selectionActions": [ + { + "id": "import-image-to-jianying", + "title": { + "default": "Import to JianYing", + "zh-CN": "导入剪映" + }, + "description": { + "default": "Import the selected image into the stable current JianYing draft, or create a new draft safely.", + "zh-CN": "将所选图片导入稳定的剪映当前草稿,或安全创建新草稿。" + }, + "target": "image", + "editor": "confirmation", + "steps": [ + { + "tool": "media.import-selected" + } + ] + }, + { + "id": "import-video-to-jianying", + "title": { + "default": "Import to JianYing", + "zh-CN": "导入剪映" + }, + "description": { + "default": "Import the selected video into the stable current JianYing draft, or create a new draft safely.", + "zh-CN": "将所选视频导入稳定的剪映当前草稿,或安全创建新草稿。" + }, + "target": "video", + "editor": "confirmation", + "steps": [ + { + "tool": "media.import-selected" + } + ] + } + ] }, "skills": [ { diff --git a/packages/plugins/jianying-editor/test/package.test.ts b/packages/plugins/jianying-editor/test/package.test.ts index 9e79985..240d95d 100644 --- a/packages/plugins/jianying-editor/test/package.test.ts +++ b/packages/plugins/jianying-editor/test/package.test.ts @@ -10,13 +10,13 @@ async function read(relativePath: string) { } describe("JianYing Plugin package", () => { - test("declares an explicit-install, return-only, direct-input integration", async () => { + test("declares direct-input Agent tools and ordinary image/video import actions separately", async () => { const manifest = JSON.parse(await read("manifest.json")) expect(manifest).toMatchObject({ capabilities: ["canvas.connectedInputs.read", "generation.execute"], id: "jianying-editor", schema: "convax.plugin/6", - version: "2.0.0", + version: "2.1.1", runtime: { command: "convax-jianying-editor-mcp", type: "mcp-stdio", @@ -30,12 +30,67 @@ describe("JianYing Plugin package", () => { id: "media.export", inputBinding: "direct-incoming", }), + expect.objectContaining({ + acceptedInputs: ["reference_image", "reference_video"], + delivery: "return", + id: "media.import-selected", + output: "text", + }), + ]) + expect(manifest.contributes.generation.tools[2]).not.toHaveProperty("inputBinding") + expect(manifest.contributes.agent.tools).toEqual([ + { id: "get_draft_status", tool: "draft.status" }, + { id: "export_connected_media", tool: "media.export" }, + ]) + expect(manifest.contributes.canvas.selectionActions).toEqual([ + { + description: { + default: "Import the selected image into the stable current JianYing draft, or create a new draft safely.", + "zh-CN": "将所选图片导入稳定的剪映当前草稿,或安全创建新草稿。", + }, + editor: "confirmation", + id: "import-image-to-jianying", + steps: [{ tool: "media.import-selected" }], + target: "image", + title: { default: "Import to JianYing", "zh-CN": "导入剪映" }, + }, + { + description: { + default: "Import the selected video into the stable current JianYing draft, or create a new draft safely.", + "zh-CN": "将所选视频导入稳定的剪映当前草稿,或安全创建新草稿。", + }, + editor: "confirmation", + id: "import-video-to-jianying", + steps: [{ tool: "media.import-selected" }], + target: "video", + title: { default: "Import to JianYing", "zh-CN": "导入剪映" }, + }, ]) expect(manifest.contributes.skills).toEqual([ { name: "jianying-editor", path: "skills/jianying-editor" }, ]) }) + test("binds the changed manifest and companion bytes to new immutable versions", async () => { + const metadata = JSON.parse(await fs.readFile( + path.resolve(import.meta.dir, "..", "convax-package.json"), + "utf8", + )) + const workspace = JSON.parse(await fs.readFile( + path.resolve(import.meta.dir, "..", "package.json"), + "utf8", + )) + + expect(metadata.version).toBe("2.1.1") + expect(workspace.version).toBe("2.1.1") + expect(metadata.companions).toEqual([ + expect.objectContaining({ + command: "convax-jianying-editor-mcp", + version: "1.1.1", + }), + ]) + }) + test("keeps the iframe offline and delegates native work through host capabilities", async () => { const html = await read("index.html") const application = await read("assets/app.js") diff --git a/packages/skills/canvas-storyboard/convax-package.json b/packages/skills/canvas-storyboard/convax-package.json new file mode 100644 index 0000000..a1f0b08 --- /dev/null +++ b/packages/skills/canvas-storyboard/convax-package.json @@ -0,0 +1,29 @@ +{ + "schema": "convax.package/1", + "kind": "skill", + "id": "canvas-storyboard", + "name": "Storyboard Builder", + "description": "Convert a script or brief into ordered, reviewable shot cards on the active Canvas.", + "version": "0.1.0", + "license": "MIT", + "compatibility": { + "skillSchema": "opencode.skill/1" + }, + "showcase": { + "poster": { + "path": "showcase/poster.png", + "alt": "Storyboard workflow arranging script beats into connected, reviewable Canvas shot cards.", + "mime": "image/png", + "width": 1280, + "height": 720 + }, + "animation": { + "path": "showcase/animation.mp4", + "alt": "Animated storyboard workflow turning a creative brief into ordered Canvas shot cards.", + "mime": "video/mp4", + "width": 1280, + "height": 720 + } + }, + "yanked": false +} diff --git a/packages/skills/canvas-storyboard/package.json b/packages/skills/canvas-storyboard/package.json new file mode 100644 index 0000000..a640b42 --- /dev/null +++ b/packages/skills/canvas-storyboard/package.json @@ -0,0 +1,10 @@ +{ + "name": "@microvoid/convax-skill-canvas-storyboard", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "validate": "bun ../../../tooling/validate.mjs --kind skill --id canvas-storyboard", + "pack": "bun ../../../tooling/pack.mjs --kind skill --id canvas-storyboard" + } +} diff --git a/packages/skills/canvas-storyboard/package/LICENSE b/packages/skills/canvas-storyboard/package/LICENSE new file mode 100644 index 0000000..184b0cb --- /dev/null +++ b/packages/skills/canvas-storyboard/package/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Microvoid contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/skills/canvas-storyboard/package/SKILL.md b/packages/skills/canvas-storyboard/package/SKILL.md new file mode 100644 index 0000000..0eceaba --- /dev/null +++ b/packages/skills/canvas-storyboard/package/SKILL.md @@ -0,0 +1,32 @@ +--- +name: canvas-storyboard +description: Convert a script or creative brief into ordered, reviewable shot cards on the active Convax Canvas. +--- + +# Storyboard Builder + +Use this Skill when the user provides a script, scene, or creative brief and wants +an editable shot plan on Canvas. The deliverable is a connected sequence of shot +cards, not just advice in chat. + +1. Confirm the active Canvas from the authoritative Convax host context. +2. Query existing nodes and the latest revision before changing anything. Reuse + relevant reference images or notes instead of duplicating them. +3. Break the input into 3–12 shots unless the user specifies another count. Each + markdown card should include the shot number, framing, subject/action, camera + movement, and dialogue or audio cue when present. +4. Add the cards with `canvas_add_resources` as `new-text` sources. Each source is + first published as a normal Markdown file below the active Project's `Notes/` + directory, then referenced by its Canvas card. This business operation owns file + publication, card sizing, placement, persistence, refresh, and optional view + effects; do not approximate those rules with raw node JSON. +5. Using only the returned node ids and current revision, arrange the cards in + narrative order and connect each shot to the next with `canvas_apply_primitive`. + Re-query after each revision-changing command. +6. Reveal, select, and fit the completed sequence with `canvas_view`, then summarize + any continuity, coverage, or pacing risk that still needs a creative decision. + +If the request is specifically about semantics inside a Plugin node that provides +its own companion Skill, use that Plugin's companion Skill instead. + +Do not edit `.convax` files, invent Canvas ids, or recreate Canvas business rules in the Skill. diff --git a/packages/skills/canvas-storyboard/showcase/animation.mp4 b/packages/skills/canvas-storyboard/showcase/animation.mp4 new file mode 100644 index 0000000..f684bc3 Binary files /dev/null and b/packages/skills/canvas-storyboard/showcase/animation.mp4 differ diff --git a/packages/skills/canvas-storyboard/showcase/poster.png b/packages/skills/canvas-storyboard/showcase/poster.png new file mode 100644 index 0000000..e992627 Binary files /dev/null and b/packages/skills/canvas-storyboard/showcase/poster.png differ diff --git a/packages/tools/ffmpeg-mcp/scripts/build-release.ts b/packages/tools/ffmpeg-mcp/scripts/build-release.ts index 5a1781a..eb98f76 100644 --- a/packages/tools/ffmpeg-mcp/scripts/build-release.ts +++ b/packages/tools/ffmpeg-mcp/scripts/build-release.ts @@ -32,7 +32,8 @@ const outputDirectory = host await mkdir(outputDirectory, { recursive: true }) const outfile = path.join(outputDirectory, "convax-ffmpeg-mcp") const buildDirectory = await mkdtemp(path.join(os.tmpdir(), "convax-ffmpeg-native-")) -const temporaryOutput = path.join(outputDirectory, `.convax-ffmpeg-mcp-${process.pid}.tmp`) +const outputStagingDirectory = await mkdtemp(path.join(outputDirectory, ".convax-ffmpeg-release-")) +const temporaryOutput = path.join(outputStagingDirectory, "convax-ffmpeg-mcp") try { const [{ stdout: clangOutput }, { stdout: swiftOutput }, { stdout: sdkOutput }] = await Promise.all([ execute("/usr/bin/xcrun", ["--find", "clang"], { encoding: "utf8" }), @@ -75,6 +76,7 @@ try { bridgeObject, "-o", temporaryOutput, "-Xlinker", "-dead_strip", + "-Xlinker", "-no_uuid", "-Xlinker", "-sectcreate", "-Xlinker", "__DATA", "-Xlinker", "__ffmpeg", @@ -106,6 +108,7 @@ try { await rename(temporaryOutput, outfile) } finally { await rm(temporaryOutput, { force: true }) + await rm(outputStagingDirectory, { force: true, recursive: true }) await rm(buildDirectory, { force: true, recursive: true }) } console.log(`Built ${outfile}`) diff --git a/packages/tools/ffmpeg-mcp/test/release-build.test.ts b/packages/tools/ffmpeg-mcp/test/release-build.test.ts new file mode 100644 index 0000000..d3da007 --- /dev/null +++ b/packages/tools/ffmpeg-mcp/test/release-build.test.ts @@ -0,0 +1,11 @@ +import { expect, test } from "bun:test" +import { readFile } from "node:fs/promises" +import path from "node:path" + +test("native release build disables the nondeterministic Mach-O UUID", async () => { + const source = await readFile(path.join(import.meta.dir, "..", "scripts", "build-release.ts"), "utf8") + + expect(source).toContain('"-Xlinker", "-no_uuid"') + expect(source).not.toContain("process.pid") + expect(source).toContain('const temporaryOutput = path.join(outputStagingDirectory, "convax-ffmpeg-mcp")') +}) diff --git a/packages/tools/jianying-editor-mcp/README.md b/packages/tools/jianying-editor-mcp/README.md index ab92df2..b1c5c78 100644 --- a/packages/tools/jianying-editor-mcp/README.md +++ b/packages/tools/jianying-editor-mcp/README.md @@ -1,11 +1,14 @@ # Convax JianYing companion Reviewed local MCP companion for the `jianying-editor` Convax Plugin. It supports -macOS only and exposes two operations: +macOS only and exposes three operations: - `draft.status`: stable observation of the running JianYing draft; - `media.export`: import host-staged image/video references through a short-lived loopback server and the JianYing Deep Link. +- `media.import-selected`: import exactly one host-staged toolbar selection using + the safe automatic policy: a stable active draft is reused, no active draft + creates a new one, and ambiguous or changed state fails closed. This package intentionally has no Canvas, Project, Electron, IPC, renderer, registry, credential, or persistent staging logic. diff --git a/packages/tools/jianying-editor-mcp/package.json b/packages/tools/jianying-editor-mcp/package.json index a1c3a48..b95412b 100644 --- a/packages/tools/jianying-editor-mcp/package.json +++ b/packages/tools/jianying-editor-mcp/package.json @@ -1,6 +1,6 @@ { "name": "@microvoid/convax-jianying-editor-mcp", - "version": "1.0.0", + "version": "1.1.1", "private": true, "license": "MIT", "type": "module", diff --git a/packages/tools/jianying-editor-mcp/src/contracts.ts b/packages/tools/jianying-editor-mcp/src/contracts.ts index 17d4e1a..ecc6431 100644 --- a/packages/tools/jianying-editor-mcp/src/contracts.ts +++ b/packages/tools/jianying-editor-mcp/src/contracts.ts @@ -15,6 +15,7 @@ export interface StagedReference { export interface GenerationCall { draftToken?: string + operationId: string output: "text" outputDirectory: string prompt: string @@ -76,23 +77,34 @@ function text(value: unknown, label: string, maximum: number) { return value } -export function parseGenerationCall(value: unknown, operation: "draft.status" | "media.export"): GenerationCall { +export function parseGenerationCall( + value: unknown, + operation: "draft.status" | "media.export" | "media.import-selected", +): GenerationCall { const input = record(value, "generation call") exactKeys( input, - ["draft_token", "output", "output_directory", "prompt", "references", "schema", "target"], + ["draft_token", "operation_id", "output", "output_directory", "prompt", "references", "schema", "target"], "generation call", ) if (input.schema !== generationCallSchema || input.output !== "text") { throw new InputError("generation call contract is not supported.") } + const operationId = text(input.operation_id, "operation_id", 71) + if (!/^convax-[0-9a-f]{64}$/u.test(operationId)) { + throw new InputError("operation_id is invalid.") + } if (!Array.isArray(input.references)) throw new InputError("generation references must be an array.") - const minimum = operation === "media.export" ? 1 : 0 - const maximum = operation === "media.export" ? 32 : 0 + const minimum = operation === "draft.status" ? 0 : 1 + const maximum = operation === "media.export" ? 32 : operation === "media.import-selected" ? 1 : 0 if (input.references.length < minimum || input.references.length > maximum) { - throw new InputError(operation === "media.export" - ? "JianYing export requires between one and 32 staged references." - : "JianYing status does not accept media references.") + throw new InputError( + operation === "draft.status" + ? "JianYing status does not accept media references." + : operation === "media.import-selected" + ? "JianYing toolbar import requires exactly one staged reference." + : "JianYing export requires between one and 32 staged references.", + ) } const references = input.references.map((raw, index): StagedReference => { const reference = record(raw, `reference ${index}`) @@ -123,11 +135,15 @@ export function parseGenerationCall(value: unknown, operation: "draft.status" | if (operation === "draft.status" && (target !== "auto" || draftToken !== undefined)) { throw new InputError("JianYing status does not accept target fields.") } + if (operation === "media.import-selected" && (target !== "auto" || draftToken !== undefined)) { + throw new InputError("JianYing toolbar import does not accept target fields.") + } if (operation === "media.export" && target !== "auto" && draftToken === undefined) { throw new InputError("Explicit JianYing export requires draft_token.") } return { ...(draftToken ? { draftToken } : {}), + operationId, output: "text", outputDirectory: text(input.output_directory, "output_directory", 4_096), prompt: text(input.prompt, "prompt", 20_000), diff --git a/packages/tools/jianying-editor-mcp/src/inspector.ts b/packages/tools/jianying-editor-mcp/src/inspector.ts index d470164..b8a78d7 100644 --- a/packages/tools/jianying-editor-mcp/src/inspector.ts +++ b/packages/tools/jianying-editor-mcp/src/inspector.ts @@ -49,7 +49,14 @@ function processIds(stdout: string) { const ids: number[] = [] for (const line of stdout.split(/\r?\n/u)) { const match = /^\s*(\d+)\s+(.+?)\s*$/u.exec(line) - if (!match || path.basename(match[2]!) !== executableName) continue + const command = match?.[2] + if ( + !command || + path.basename(command) !== executableName || + command.includes("/Contents/Frameworks/") + ) { + continue + } const pid = Number(match[1]) if (Number.isSafeInteger(pid) && pid > 0) ids.push(pid) } diff --git a/packages/tools/jianying-editor-mcp/src/mcp-server.ts b/packages/tools/jianying-editor-mcp/src/mcp-server.ts index 27bf860..b5d45c2 100644 --- a/packages/tools/jianying-editor-mcp/src/mcp-server.ts +++ b/packages/tools/jianying-editor-mcp/src/mcp-server.ts @@ -59,6 +59,11 @@ export const tools = [ inputSchema: envelope({ maximum: 32, minimum: 1 }, true), name: "media.export", }, + { + description: "Import one toolbar-selected, host-staged image or video using the safe automatic draft policy.", + inputSchema: envelope({ maximum: 1, minimum: 1 }, false), + name: "media.import-selected", + }, ] as const interface Request { @@ -163,7 +168,7 @@ export class McpServer { return this.result(value.id, { capabilities: { tools: {} }, protocolVersion, - serverInfo: { name: "convax-jianying-editor-mcp", version: "1.0.0" }, + serverInfo: { name: "convax-jianying-editor-mcp", version: "1.1.1" }, }) } if (value.method === "tools/list") return this.result(value.id, { tools }) @@ -172,7 +177,11 @@ export class McpServer { this.#inflight.set(value.id, controller) try { const params = record(value.params, "tools/call params") - if (params.name !== "draft.status" && params.name !== "media.export") { + if ( + params.name !== "draft.status" && + params.name !== "media.export" && + params.name !== "media.import-selected" + ) { return this.error(value.id, -32602, "Unknown tool") } const call = parseGenerationCall(params.arguments, params.name) diff --git a/packages/tools/jianying-editor-mcp/test/contracts.test.ts b/packages/tools/jianying-editor-mcp/test/contracts.test.ts index 86104aa..2f6edb2 100644 --- a/packages/tools/jianying-editor-mcp/test/contracts.test.ts +++ b/packages/tools/jianying-editor-mcp/test/contracts.test.ts @@ -3,6 +3,7 @@ import { describe, expect, test } from "bun:test" import { InputError, parseGenerationCall } from "../src/contracts.ts" const envelope = { + operation_id: `convax-${"a".repeat(64)}`, output: "text", output_directory: "/tmp/output", prompt: "Import media", @@ -50,4 +51,33 @@ describe("generation-call contract", () => { expect(parseGenerationCall(status, "draft.status")).toMatchObject({ references: [], target: "auto" }) expect(() => parseGenerationCall({ ...status, target: "current" }, "draft.status")).toThrow("target fields") }) + + test("accepts exactly one toolbar-selected image or video with automatic safe targeting", () => { + expect(parseGenerationCall(envelope, "media.import-selected")).toMatchObject({ + operationId: envelope.operation_id, + references: [{ role: "reference_image" }], + target: "auto", + }) + expect(() => parseGenerationCall({ + ...envelope, + references: [...envelope.references, { + ...envelope.references[0], + node_id: "node-2", + }], + }, "media.import-selected")).toThrow("exactly one") + expect(() => parseGenerationCall({ + ...envelope, + draft_token: "hidden-token", + target: "current", + }, "media.import-selected")).toThrow("does not accept target fields") + }) + + test("requires the exact host operation identity and still rejects unknown fields", () => { + expect(() => parseGenerationCall({ ...envelope, operation_id: "operation-1" }, "media.import-selected")).toThrow( + "operation_id", + ) + expect(() => parseGenerationCall({ ...envelope, result_mode: "return" }, "media.import-selected")).toThrow( + "unsupported fields", + ) + }) }) diff --git a/packages/tools/jianying-editor-mcp/test/inspector.test.ts b/packages/tools/jianying-editor-mcp/test/inspector.test.ts index 1a2c246..08ed10e 100644 --- a/packages/tools/jianying-editor-mcp/test/inspector.test.ts +++ b/packages/tools/jianying-editor-mcp/test/inspector.test.ts @@ -59,4 +59,32 @@ describe("JianYing draft inspection", () => { }).inspect() expect(result).toMatchObject({ processIds: [42], status: "ambiguous" }) }) + + test("ignores the same-named Framework helper and inspects only the app main process", async () => { + const inspectedPids: string[] = [] + const run: CommandRunner = async (executable, args) => { + if (executable === "/bin/ps") { + return { + exitCode: 0, + stderr: "", + stdout: [ + " 42 /Applications/VideoFusion-macOS.app/Contents/MacOS/VideoFusion-macOS", + " 43 /Applications/VideoFusion-macOS.app/Contents/Frameworks/VideoFusion-macOS.app/Contents/MacOS/VideoFusion-macOS", + ].join("\n"), + } + } + inspectedPids.push(args.at(-1)!) + return { exitCode: 0, stderr: "", stdout: "" } + } + + const result = await new JianyingDraftInspector({ + platform: "darwin", + roots: [], + run, + sleep: async () => undefined, + }).inspect() + + expect(result).toEqual({ processIds: [42], status: "no_active_draft" }) + expect(inspectedPids).toEqual(["42", "42"]) + }) }) diff --git a/packages/tools/jianying-editor-mcp/test/mcp-server.test.ts b/packages/tools/jianying-editor-mcp/test/mcp-server.test.ts new file mode 100644 index 0000000..3215a3e --- /dev/null +++ b/packages/tools/jianying-editor-mcp/test/mcp-server.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, mock, test } from "bun:test" + +import { McpServer, tools } from "../src/mcp-server.ts" + +const argumentsEnvelope = { + operation_id: `convax-${"a".repeat(64)}`, + output: "text", + output_directory: "/tmp/output", + prompt: "Import toolbar selection", + references: [{ + kind: "file", + mime_type: "video/mp4", + name: "clip.mp4", + node_id: "node-1", + path: "/tmp/staged/clip.mp4", + role: "reference_video", + }], + schema: "convax.generation-call/1", +} + +describe("JianYing MCP server", () => { + test("advertises and routes the toolbar-only import through the safe export service", async () => { + expect(tools.map(({ name }) => name)).toEqual([ + "draft.status", + "media.export", + "media.import-selected", + ]) + let exportedCall: unknown + const exportMedia = mock(async (call: unknown) => { + exportedCall = call + return { + createdDraft: false, + draftName: "Demo", + importedMediaCount: 1, + schema: "convax.jianying-export-result/1" as const, + transferStatus: "verified" as const, + } + }) + const responses: string[] = [] + const server = new McpServer( + { export: exportMedia, status: mock(async () => ({})) } as never, + (value) => responses.push(value), + ) + + await (server as unknown as { + handle(value: unknown): Promise + }).handle({ + id: 1, + jsonrpc: "2.0", + method: "tools/call", + params: { + arguments: argumentsEnvelope, + name: "media.import-selected", + }, + }) + + expect(exportMedia).toHaveBeenCalledTimes(1) + expect(exportedCall).toMatchObject({ + references: [{ role: "reference_video" }], + target: "auto", + }) + const response = JSON.parse(responses[0]!) + expect(response.result.structuredContent).toMatchObject({ + draftName: "Demo", + importedMediaCount: 1, + transferStatus: "verified", + }) + }) +}) diff --git a/packages/tools/jianying-editor-mcp/test/service.test.ts b/packages/tools/jianying-editor-mcp/test/service.test.ts index b18a490..85a1c26 100644 --- a/packages/tools/jianying-editor-mcp/test/service.test.ts +++ b/packages/tools/jianying-editor-mcp/test/service.test.ts @@ -11,6 +11,7 @@ const active: DraftObservation = { function call(overrides: Partial = {}): GenerationCall { return { + operationId: `convax-${"a".repeat(64)}`, output: "text", outputDirectory: "/tmp/output", prompt: "Import", @@ -73,4 +74,21 @@ describe("JianYing service", () => { await expect(service.export(call())).rejects.toThrow("changed before import") expect(importMedia).not.toHaveBeenCalled() }) + + test("imports one toolbar selection with the existing safe automatic draft policy", async () => { + const inspect = mock(async () => active) + const importMedia = mock(async () => ({ completed: 1 })) + const service = new JianyingService( + { inspect }, + { createDraft: mock(async () => undefined), import: importMedia }, + ) + + await expect(service.export(call())).resolves.toMatchObject({ + createdDraft: false, + draftName: "Demo", + importedMediaCount: 1, + transferStatus: "verified", + }) + expect(importMedia).toHaveBeenCalledTimes(1) + }) }) diff --git a/schemas/convax-plugin-manifest-v6.schema.json b/schemas/convax-plugin-manifest-v6.schema.json index fc90140..a639e7b 100644 --- a/schemas/convax-plugin-manifest-v6.schema.json +++ b/schemas/convax-plugin-manifest-v6.schema.json @@ -49,9 +49,7 @@ "additionalProperties": false, "properties": { "agent": { "$ref": "#/$defs/agent" }, - "canvas": { - "$ref": "./convax-plugin-manifest-v4.schema.json#/$defs/canvas" - }, + "canvas": { "$ref": "#/$defs/canvas" }, "generation": { "$ref": "#/$defs/generation" }, "llm": { "$ref": "./convax-plugin-manifest-v5.schema.json#/$defs/llm" }, "service": { @@ -196,6 +194,86 @@ } ], "$defs": { + "canvas": { + "type": "object", + "additionalProperties": false, + "properties": { + "renderer": { + "$ref": "./convax-plugin-manifest-v4.schema.json#/$defs/renderer" + }, + "selectionActions": { + "type": "array", + "minItems": 1, + "maxItems": 32, + "items": { "$ref": "#/$defs/selectionAction" } + }, + "toolbar": { + "type": "array", + "uniqueItems": true, + "maxItems": 32, + "items": { + "$ref": "./convax-plugin-manifest-v4.schema.json#/$defs/toolbarItem" + } + } + }, + "anyOf": [{ "required": ["renderer"] }, { "required": ["selectionActions"] }], + "allOf": [ + { + "if": { "required": ["toolbar"] }, + "then": { "required": ["renderer"] } + } + ] + }, + "selectionAction": { + "type": "object", + "additionalProperties": false, + "required": ["id", "title", "description", "target", "editor", "steps"], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z0-9]+(?:[._-][a-z0-9]+)*$", + "maxLength": 80 + }, + "title": { + "$ref": "./convax-plugin-manifest-v4.schema.json#/$defs/localizedTitle" + }, + "description": { + "$ref": "./convax-plugin-manifest-v4.schema.json#/$defs/localizedText" + }, + "target": { "enum": ["image", "video"] }, + "editor": { + "enum": ["time-point", "time-range", "crop-region", "confirmation"] + }, + "steps": { + "type": "array", + "minItems": 1, + "maxItems": 16, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["tool"], + "properties": { + "tool": { + "type": "string", + "pattern": "^[a-z0-9]+(?:[._-][a-z0-9]+)*$", + "maxLength": 80 + } + } + } + } + }, + "allOf": [ + { + "if": { + "properties": { + "editor": { "enum": ["time-point", "time-range", "crop-region"] } + }, + "required": ["editor"] + }, + "then": { "properties": { "steps": { "maxItems": 1 } } } + } + ] + }, "generation": { "type": "object", "additionalProperties": false, diff --git a/schemas/convax-plugin-manifest-v7.schema.json b/schemas/convax-plugin-manifest-v7.schema.json index ea2e826..85b845d 100644 --- a/schemas/convax-plugin-manifest-v7.schema.json +++ b/schemas/convax-plugin-manifest-v7.schema.json @@ -174,7 +174,7 @@ "maxItems": 32, "items": { "oneOf": [ - { "$ref": "./convax-plugin-manifest-v4.schema.json#/$defs/selectionAction" }, + { "$ref": "./convax-plugin-manifest-v6.schema.json#/$defs/selectionAction" }, { "$ref": "#/$defs/materializeOwnPluginNodeAction" } ] } diff --git a/tooling/fetch-marketplace-previous.mjs b/tooling/fetch-marketplace-previous.mjs new file mode 100644 index 0000000..3afb1a4 --- /dev/null +++ b/tooling/fetch-marketplace-previous.mjs @@ -0,0 +1,145 @@ +import { promises as fs } from "node:fs" +import path from "node:path" + +const maximumBytes = 8 * 1024 * 1024 + +async function responseBytes(response, label) { + const declared = response.headers.get("content-length") + if (declared !== null) { + const size = Number(declared) + if (!Number.isSafeInteger(size) || size < 0 || size > maximumBytes) { + throw new Error(`${label} exceeds the 8 MiB limit`) + } + } + if (!response.body) throw new Error(`${label} has no response body`) + const reader = response.body.getReader() + const chunks = [] + let total = 0 + while (true) { + const { done, value } = await reader.read() + if (done) break + total += value.byteLength + if (total > maximumBytes) { + await reader.cancel() + throw new Error(`${label} exceeds the 8 MiB limit`) + } + chunks.push(value) + } + const result = new Uint8Array(total) + let offset = 0 + for (const chunk of chunks) { + result.set(chunk, offset) + offset += chunk.byteLength + } + return result +} + +function parseSequenceInput(bytes, mode, parser) { + let registry + try { + registry = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)) + } catch (cause) { + throw new Error(`${mode} is not valid UTF-8 JSON`, { cause }) + } + try { + registry = parser(registry) + } catch (cause) { + throw new Error(`${mode} strict validation failed`, { cause }) + } + const revisionPattern = mode === "v2" ? /^[a-f0-9]{64}$/ : /^[a-f0-9]{40}$/ + if ( + !registry || + typeof registry !== "object" || + registry.schema !== `convax.registry/${mode === "v2" ? "2" : "1"}` || + !Number.isSafeInteger(registry.sequence) || + registry.sequence <= 0 || + typeof registry.revision !== "string" || + !revisionPattern.test(registry.revision) || + !Array.isArray(registry.packages) || + (mode === "v2" && registry.marketplaceId !== "convax-official") + ) { + throw new Error(`${mode} is not a strict sequence input`) + } + return registry +} + +async function fetchExact(fetchImpl, url, label) { + return fetchImpl(url, { + headers: { accept: "application/json" }, + redirect: "error", + signal: AbortSignal.timeout(30_000), + }).catch((cause) => { + throw new Error(`${label} fetch failed`, { cause }) + }) +} + +async function writeSnapshot(outputDirectory, name, bytes) { + await fs.mkdir(outputDirectory, { recursive: true }) + const output = path.join(outputDirectory, name) + const temporary = `${output}.tmp-${process.pid}` + await fs.writeFile(temporary, bytes, { mode: 0o600 }) + await fs.rename(temporary, output) + return output +} + +export async function fetchPreviousRegistry({ + fetchImpl = fetch, + outputDirectory, + parseV1, + parseV2, + v1Url, + v2Url, +}) { + if (typeof parseV1 !== "function" || typeof parseV2 !== "function") { + throw new Error("strict Registry v1 and v2 parsers are required") + } + const v2Response = await fetchExact(fetchImpl, v2Url, "production Registry v2") + if (v2Response.status === 200) { + const bytes = await responseBytes(v2Response, "production Registry v2") + const registry = parseSequenceInput(bytes, "v2", parseV2) + const snapshot = await writeSnapshot(outputDirectory, "registry-v2.json", bytes) + return { mode: "v2", registry, snapshot } + } + if (v2Response.status !== 404) { + throw new Error(`production Registry v2 returned HTTP ${v2Response.status}`) + } + + const v1Response = await fetchExact(fetchImpl, v1Url, "production Registry v1 bootstrap") + if (v1Response.status !== 200) { + throw new Error(`production Registry v1 bootstrap returned HTTP ${v1Response.status}`) + } + const bytes = await responseBytes(v1Response, "production Registry v1 bootstrap") + const registry = parseSequenceInput(bytes, "v1", parseV1) + const snapshot = await writeSnapshot(outputDirectory, "registry-v1.json", bytes) + return { mode: "bootstrap-v1", registry, snapshot } +} + +async function main() { + const kit = await import("@convax/marketplace-kit") + const run = encodeURIComponent(process.env.GITHUB_RUN_ID ?? "local") + const result = await fetchPreviousRegistry({ + outputDirectory: path.resolve("dist/production"), + parseV1: kit.parseRegistryV1, + parseV2: kit.parseRegistryV2, + v1Url: `https://microvoid.github.io/convax-plugins/registry/v1/index.json?run=${run}`, + v2Url: `https://microvoid.github.io/convax-plugins/registry/v2/index.json?run=${run}`, + }) + const environment = process.env.GITHUB_ENV + if (!environment) throw new Error("GITHUB_ENV is required for the publication workflow") + const variable = result.mode === "v2" + ? "CONVAX_MARKETPLACE_PREVIOUS" + : "CONVAX_MARKETPLACE_BOOTSTRAP_PREVIOUS_V1" + await fs.appendFile(environment, `${variable}=${path.relative(process.cwd(), result.snapshot)}\n`) + console.log( + result.mode === "v2" + ? `Using strict production Registry v2 sequence ${result.registry.sequence}.` + : `Bootstrapping Registry v2 from strict Registry v1 sequence ${result.registry.sequence}.`, + ) +} + +if (import.meta.main) { + await main().catch((error) => { + console.error(error instanceof Error ? error.message : String(error)) + process.exitCode = 1 + }) +} diff --git a/tooling/fixtures/mcp-servers/official-http-example/LICENSE b/tooling/fixtures/mcp-servers/official-http-example/LICENSE new file mode 100644 index 0000000..184b0cb --- /dev/null +++ b/tooling/fixtures/mcp-servers/official-http-example/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Microvoid contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tooling/fixtures/mcp-servers/official-http-example/README.md b/tooling/fixtures/mcp-servers/official-http-example/README.md new file mode 100644 index 0000000..595a928 --- /dev/null +++ b/tooling/fixtures/mcp-servers/official-http-example/README.md @@ -0,0 +1,5 @@ +# Official HTTP Example + +This inert package exercises one fixed HTTPS Streamable HTTP MCP endpoint. +Installing or listing it must not connect to the endpoint. Connection and +standard OAuth or anonymous confirmation happen only during Convax setup. diff --git a/tooling/fixtures/mcp-servers/official-http-example/server.json b/tooling/fixtures/mcp-servers/official-http-example/server.json new file mode 100644 index 0000000..0970406 --- /dev/null +++ b/tooling/fixtures/mcp-servers/official-http-example/server.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "name": "io.github.microvoid/official-http-example", + "title": "Official HTTP Example", + "description": "A fixed HTTPS MCP fixture for Convax Marketplace acceptance.", + "version": "1.0.0", + "repository": { + "url": "https://github.com/microvoid/convax-plugins", + "source": "github", + "subfolder": "packages/mcp-servers/official-http-example" + }, + "remotes": [ + { + "type": "streamable-http", + "url": "https://mcp.example.com/v1" + } + ] +} diff --git a/tooling/fixtures/mcp-servers/official-managed-example/LICENSE b/tooling/fixtures/mcp-servers/official-managed-example/LICENSE new file mode 100644 index 0000000..184b0cb --- /dev/null +++ b/tooling/fixtures/mcp-servers/official-managed-example/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Microvoid contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tooling/fixtures/mcp-servers/official-managed-example/README.md b/tooling/fixtures/mcp-servers/official-managed-example/README.md new file mode 100644 index 0000000..d23c173 --- /dev/null +++ b/tooling/fixtures/mcp-servers/official-managed-example/README.md @@ -0,0 +1,5 @@ +# Official Managed Example + +This inert package exercises managed-stdio publication. Its companion is a +separate target-specific artifact. Convax Desktop owns setup, exact-byte launch, +process lifecycle, loopback transport, and the bounded product-action mapping. diff --git a/tooling/fixtures/mcp-servers/official-managed-example/convax-mcp.json b/tooling/fixtures/mcp-servers/official-managed-example/convax-mcp.json new file mode 100644 index 0000000..e10baea --- /dev/null +++ b/tooling/fixtures/mcp-servers/official-managed-example/convax-mcp.json @@ -0,0 +1,22 @@ +{ + "schema": "convax.mcp-server-extension/1", + "runtime": { + "kind": "managed-stdio", + "command": "convax-managed-example-mcp", + "argv": [], + "compatibility": { + "targets": [ + "darwin-arm64" + ] + } + }, + "productActions": [ + { + "action": "project.files.read", + "tool": "fixture_read" + } + ], + "grants": [ + "project.files.read" + ] +} diff --git a/tooling/fixtures/mcp-servers/official-managed-example/server.json b/tooling/fixtures/mcp-servers/official-managed-example/server.json new file mode 100644 index 0000000..ea1d496 --- /dev/null +++ b/tooling/fixtures/mcp-servers/official-managed-example/server.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "name": "io.github.microvoid/official-managed-example", + "title": "Official Managed Example", + "description": "A managed stdio MCP fixture for Convax Marketplace acceptance.", + "version": "1.0.0", + "repository": { + "url": "https://github.com/microvoid/convax-plugins", + "source": "github", + "subfolder": "packages/mcp-servers/official-managed-example" + } +} diff --git a/tooling/lib.mjs b/tooling/lib.mjs index 9187268..db67d39 100644 --- a/tooling/lib.mjs +++ b/tooling/lib.mjs @@ -885,7 +885,12 @@ function parseAgentV3(value, generation, label) { return { tools }; } -function parseSelectionActionsV3(value, generation, label) { +function parseSelectionActionsV3( + value, + generation, + label, + { allowReturnSelectionActions = false } = {}, +) { if (!Array.isArray(value) || value.length < 1 || value.length > 32) { error(label, "must be a non-empty array with at most 32 items"); } @@ -897,7 +902,18 @@ function parseSelectionActionsV3(value, generation, label) { ["description", "editor", "id", "steps", "target", "title"], itemLabel, ); - if (item.target !== "video") error(itemLabel, "target must be video"); + if ( + item.target !== "video" && + !(allowReturnSelectionActions && item.target === "image") + ) { + error( + itemLabel, + allowReturnSelectionActions + ? "target must be image or video" + : "target must be video", + ); + } + const target = item.target; if (!selectionActionEditors.has(item.editor)) error(itemLabel, "unsupported editor"); if ( @@ -934,19 +950,55 @@ function parseSelectionActionsV3(value, generation, label) { const returned = steps.find( (step) => generationTools.get(step.tool).delivery === "return", ); - if (returned) + if (returned && !allowReturnSelectionActions) { error( itemLabel, `cannot reference return-delivery operation ${returned.tool}`, ); + } + if (allowReturnSelectionActions) { + const inputBound = steps.find( + (step) => generationTools.get(step.tool).inputBinding !== undefined, + ); + if (inputBound) { + error( + itemLabel, + `cannot reference an input-bound operation ${inputBound.tool}`, + ); + } + } + if (returned && allowReturnSelectionActions) { + if (item.editor !== "confirmation") { + error( + itemLabel, + `return-delivery operation ${returned.tool} requires a confirmation editor`, + ); + } + if (steps.length !== 1) { + error( + itemLabel, + `return-delivery operation ${returned.tool} requires exactly one step`, + ); + } + if (generationTools.get(returned.tool).output !== "text") { + error( + itemLabel, + `return-delivery operation ${returned.tool} must return text`, + ); + } + } else if (allowReturnSelectionActions && target === "image") { + error(itemLabel, "image selection action requires a return-delivery operation"); + } + const referenceRole = + target === "image" ? "reference_image" : "reference_video"; const incompatible = steps.find( (step) => !generationTools .get(step.tool) - .acceptedInputs.includes("reference_video"), + .acceptedInputs.includes(referenceRole), ); if (incompatible) - error(itemLabel, `tool ${incompatible.tool} must accept reference_video`); + error(itemLabel, `tool ${incompatible.tool} must accept ${referenceRole}`); return { description: parseLocalizedText( item.description, @@ -956,7 +1008,7 @@ function parseSelectionActionsV3(value, generation, label) { editor: item.editor, id: parsePluginReferenceId(item.id, `${itemLabel} id`), steps, - target: "video", + target, title: parseLocalizedText(item.title, `${itemLabel} title`, 120), }; }); @@ -1009,7 +1061,9 @@ function parseSelectionActionsV7(value, generation, label) { itemLabel, "generation-backed selection action requires a generation contribution", ); - return parseSelectionActionsV3([item], generation, itemLabel)[0]; + return parseSelectionActionsV3([item], generation, itemLabel, { + allowReturnSelectionActions: true, + })[0]; }); if (new Set(actions.map((action) => action.id)).size !== actions.length) error(label, "contains duplicate ids"); @@ -1053,7 +1107,12 @@ function parseCanvasV7(value, generation, label) { }; } -function parseCanvasV3(value, generation, label) { +function parseCanvasV3( + value, + generation, + label, + { allowReturnSelectionActions = false } = {}, +) { exactKeys(value, ["renderer", "selectionActions", "toolbar"], [], label); if (value.toolbar !== undefined && value.renderer === undefined) { error(label, "toolbar requires a renderer"); @@ -1072,6 +1131,7 @@ function parseCanvasV3(value, generation, label) { value.selectionActions, generation, `${label} selectionActions`, + { allowReturnSelectionActions }, ); const toolbar = parseToolbar(value.toolbar, `${label} toolbar`); return { @@ -1433,7 +1493,12 @@ function parsePluginManifestV4Plus(value, label) { const canvas = hasCanvas ? v7 ? parseCanvasV7(value.contributes.canvas, generation, `${label} canvas`) - : parseCanvasV3(value.contributes.canvas, generation, `${label} canvas`) + : parseCanvasV3( + value.contributes.canvas, + generation, + `${label} canvas`, + { allowReturnSelectionActions: v6 }, + ) : undefined; const hasRenderer = canvas?.renderer !== undefined; const hasProjectCanvasCapability = capabilities.some((capability) => @@ -1962,6 +2027,20 @@ export async function readJson(file, label = file) { } } +export async function readJsonc(file, label = file) { + let text; + try { + text = await fs.readFile(file, "utf8"); + } catch (cause) { + throw new Error(`${label}: cannot read`, { cause }); + } + try { + return Bun.JSONC.parse(text); + } catch (cause) { + throw new Error(`${label}: invalid JSONC`, { cause }); + } +} + export async function collectFiles(directory, label = directory) { const files = []; const seen = new Map(); diff --git a/tooling/marketplace-output.test.js b/tooling/marketplace-output.test.js new file mode 100644 index 0000000..3596b91 --- /dev/null +++ b/tooling/marketplace-output.test.js @@ -0,0 +1,347 @@ +import { afterAll, describe, expect, test } from "bun:test" +import { createHash } from "node:crypto" +import { promises as fs } from "node:fs" +import os from "node:os" +import path from "node:path" +import { verifyMarketplaceOutput } from "./verify-marketplace-output.mjs" + +const temporaryDirectories = [] + +async function temporaryDirectory() { + const directory = await fs.mkdtemp(path.join(os.tmpdir(), "convax-marketplace-output-")) + temporaryDirectories.push(directory) + return directory +} + +function sha256(bytes) { + return createHash("sha256").update(bytes).digest("hex") +} + +function mcpTag(id, version) { + const key = sha256(Buffer.from(`mcp-server\0${id}`, "utf8")) + return `mcp-server-${key.slice(0, 16)}-v${version}` +} + +async function writeFixture() { + const catalogDirectory = await temporaryDirectory() + const repository = "https://github.com/microvoid/convax-plugins/releases/download" + const definitions = [ + { kind: "plugin", id: "fixture-plugin", version: "1.0.0" }, + { kind: "skill", id: "fixture-skill", version: "2.0.0" }, + { kind: "mcp-server", id: "io.example/fixture", version: "2026.07" }, + ] + const releasePlan = [] + const packages = [] + for (const definition of definitions) { + const tag = definition.kind === "mcp-server" + ? mcpTag(definition.id, definition.version) + : `${definition.kind}-${definition.id}-v${definition.version}` + const name = `${definition.kind}-${definition.version}.bin` + const bytes = Buffer.from(`${definition.kind}/${definition.id}@${definition.version}`) + const relativePath = `releases/${tag}/${name}` + const url = `${repository}/${tag}/${name}` + await fs.mkdir(path.join(catalogDirectory, "releases", tag), { recursive: true }) + await fs.writeFile(path.join(catalogDirectory, relativePath), bytes) + const artifact = { + path: relativePath, + name, + size: bytes.length, + sha256: sha256(bytes), + url, + } + releasePlan.push({ tag, assets: [artifact] }) + packages.push({ + ...definition, + compatibility: { convax: ">=0.1.0" }, + presentation: { name: definition.id }, + yanked: false, + delivery: definition.kind === "mcp-server" + ? { + kind: "mcp-managed-stdio", + serverJson: {}, + serverJsonSha256: "0".repeat(64), + extension: {}, + extensionSha256: "1".repeat(64), + companions: [{ + target: "darwin-arm64", + command: name, + url, + size: bytes.length, + sha256: sha256(bytes), + }], + } + : { + kind: "artifact", + url, + size: bytes.length, + sha256: sha256(bytes), + }, + }) + } + const registryV2Bytes = Buffer.from( + `${JSON.stringify({ + schema: "convax.registry/2", + marketplaceId: "convax-official", + sequence: 1, + revision: "a".repeat(64), + packages, + })}\n`, + ) + const registryV1Bytes = Buffer.from( + `${JSON.stringify({ + schema: "convax.registry/1", + sequence: 1, + revision: "a".repeat(40), + packages: packages + .filter((entry) => entry.kind !== "mcp-server") + .map(({ kind, id, version }) => ({ kind, id, version })), + })}\n`, + ) + const marketplaceBytes = Buffer.from(`${JSON.stringify({ + schema: "convax.marketplace/1", + id: "convax-official", + name: "Convax Official", + publisher: { name: "Microvoid" }, + repository: { owner: "microvoid", name: "convax-plugins" }, + registry: { + v1: { url: "https://microvoid.github.io/convax-plugins/registry/v1/index.json" }, + v2: { url: "https://microvoid.github.io/convax-plugins/registry/v2/index.json" }, + }, + showcase: { + v2: { url: "https://microvoid.github.io/convax-plugins/showcase/v2/index.json" }, + }, + compatibility: { convax: ">=0.1.0" }, + delivery: { kind: "github-pages-releases" }, + })}\n`) + const showcaseBytes = Buffer.from(`${JSON.stringify({ + schema: "convax.showcase/2", + marketplaceId: "convax-official", + revision: "a".repeat(64), + packages: [], + })}\n`) + await fs.writeFile(path.join(catalogDirectory, "marketplace.json"), marketplaceBytes) + await fs.writeFile(path.join(catalogDirectory, "registry-v2.json"), registryV2Bytes) + await fs.writeFile(path.join(catalogDirectory, "registry-v1.json"), registryV1Bytes) + await fs.writeFile(path.join(catalogDirectory, "showcase-v2.json"), showcaseBytes) + const metadataTag = `registry-v2-${"a".repeat(64)}` + const metadataAssets = [] + for (const [name, bytes] of [ + ["marketplace.json", marketplaceBytes], + ["registry-v2.json", registryV2Bytes], + ["showcase-v2.json", showcaseBytes], + ]) { + const relativePath = `releases/${metadataTag}/${name}` + const url = `${repository}/${metadataTag}/${name}` + await fs.mkdir(path.join(catalogDirectory, "releases", metadataTag), { recursive: true }) + await fs.writeFile(path.join(catalogDirectory, relativePath), bytes) + metadataAssets.push({ + path: relativePath, + name, + size: bytes.length, + sha256: sha256(bytes), + url, + }) + } + releasePlan.push({ tag: metadataTag, assets: metadataAssets }) + await fs.writeFile( + path.join(catalogDirectory, "release-plan.json"), + `${JSON.stringify({ + schema: "convax.release-plan/1", + releases: releasePlan, + })}\n`, + ) + for (const [relativePath, bytes] of [ + ["marketplace.json", marketplaceBytes], + ["registry/v1/index.json", registryV1Bytes], + ["registry/v2/index.json", registryV2Bytes], + ["showcase/v2/index.json", showcaseBytes], + ]) { + const output = path.join(catalogDirectory, "site", ...relativePath.split("/")) + await fs.mkdir(path.dirname(output), { recursive: true }) + await fs.writeFile(output, bytes) + } + return { catalogDirectory, releasePlan } +} + +async function rewriteMetadataFixture(catalogDirectory, name, value, sitePath) { + const bytes = Buffer.from(`${JSON.stringify(value)}\n`) + const registry = JSON.parse(await fs.readFile( + path.join(catalogDirectory, "registry-v2.json"), + "utf8", + )) + const tag = `registry-v2-${registry.revision}` + await fs.writeFile(path.join(catalogDirectory, name), bytes) + await fs.writeFile(path.join(catalogDirectory, sitePath), bytes) + await fs.writeFile(path.join(catalogDirectory, "releases", tag, name), bytes) + const planPath = path.join(catalogDirectory, "release-plan.json") + const plan = JSON.parse(await fs.readFile(planPath, "utf8")) + const asset = plan.releases + .find((release) => release.tag === tag) + .assets.find((candidate) => candidate.name === name) + asset.size = bytes.length + asset.sha256 = sha256(bytes) + await fs.writeFile(planPath, `${JSON.stringify(plan)}\n`) +} + +afterAll(async () => { + await Promise.all(temporaryDirectories.map((directory) => + fs.rm(directory, { recursive: true, force: true }))) +}) + +describe("published Marketplace output closure", () => { + test("binds v1 identities and every Registry Release URL to exact local bytes", async () => { + const fixture = await writeFixture() + await expect(verifyMarketplaceOutput(fixture.catalogDirectory)).resolves.toEqual({ + packages: 3, + releaseAssets: 6, + releaseTags: 4, + v1Packages: 2, + }) + }) + + test("rejects silent v1 projection loss", async () => { + const fixture = await writeFixture() + const registryPath = path.join(fixture.catalogDirectory, "registry-v1.json") + const registry = JSON.parse(await fs.readFile(registryPath, "utf8")) + registry.packages.pop() + const bytes = `${JSON.stringify(registry)}\n` + await fs.writeFile(registryPath, bytes) + await fs.writeFile( + path.join(fixture.catalogDirectory, "site/registry/v1/index.json"), + bytes, + ) + await expect(verifyMarketplaceOutput(fixture.catalogDirectory)) + .rejects.toThrow("v1 Plugin/Skill identity set differs from Registry v2") + }) + + test("rejects a non-strict or lossy v1 projection", async () => { + const fixture = await writeFixture() + const registryPath = path.join(fixture.catalogDirectory, "registry-v1.json") + const sitePath = path.join(fixture.catalogDirectory, "site/registry/v1/index.json") + const registry = JSON.parse(await fs.readFile(registryPath, "utf8")) + + registry.schema = "convax.registry/2" + let bytes = `${JSON.stringify(registry)}\n` + await fs.writeFile(registryPath, bytes) + await fs.writeFile(sitePath, bytes) + await expect(verifyMarketplaceOutput(fixture.catalogDirectory)) + .rejects.toThrow("Registry v1 is not a strict Official projection") + + registry.schema = "convax.registry/1" + registry.packages[0].version = "9.9.9" + bytes = `${JSON.stringify(registry)}\n` + await fs.writeFile(registryPath, bytes) + await fs.writeFile(sitePath, bytes) + await expect(verifyMarketplaceOutput(fixture.catalogDirectory)) + .rejects.toThrow("v1 Plugin/Skill versions differ from Registry v2") + + registry.packages[0].version = "1.0.0" + registry.packages.push({ + kind: "mcp-server", + id: "io.example/fixture", + version: "2026.07", + }) + bytes = `${JSON.stringify(registry)}\n` + await fs.writeFile(registryPath, bytes) + await fs.writeFile(sitePath, bytes) + await expect(verifyMarketplaceOutput(fixture.catalogDirectory)) + .rejects.toThrow("Registry v1 contains unsupported kind mcp-server") + }) + + test("requires every current Registry package Release and referenced asset in the plan", async () => { + const fixture = await writeFixture() + const planPath = path.join(fixture.catalogDirectory, "release-plan.json") + const plan = JSON.parse(await fs.readFile(planPath, "utf8")) + plan.releases.shift() + await fs.writeFile(planPath, `${JSON.stringify(plan)}\n`) + + await expect(verifyMarketplaceOutput(fixture.catalogDirectory)) + .rejects.toThrow("release-plan tags differ from Registry v2") + }) + + test("rejects a descriptor or Showcase that changes the Official publication identity", async () => { + const descriptorFixture = await writeFixture() + const descriptor = JSON.parse(await fs.readFile( + path.join(descriptorFixture.catalogDirectory, "marketplace.json"), + "utf8", + )) + descriptor.repository = { owner: "attacker", name: "mirror" } + await rewriteMetadataFixture( + descriptorFixture.catalogDirectory, + "marketplace.json", + descriptor, + "site/marketplace.json", + ) + await expect(verifyMarketplaceOutput(descriptorFixture.catalogDirectory)) + .rejects.toThrow("Official descriptor, Registry, Showcase, and release-plan are inconsistent") + + const showcaseFixture = await writeFixture() + const showcase = JSON.parse(await fs.readFile( + path.join(showcaseFixture.catalogDirectory, "showcase-v2.json"), + "utf8", + )) + showcase.schema = "convax.showcase/999" + await rewriteMetadataFixture( + showcaseFixture.catalogDirectory, + "showcase-v2.json", + showcase, + "site/showcase/v2/index.json", + ) + await expect(verifyMarketplaceOutput(showcaseFixture.catalogDirectory)) + .rejects.toThrow("Official descriptor, Registry, Showcase, and release-plan are inconsistent") + }) + + test("rejects missing, misplaced, or changed Release bytes", async () => { + const fixture = await writeFixture() + const asset = fixture.releasePlan[0].assets[0] + await fs.writeFile(path.join(fixture.catalogDirectory, asset.path), "changed") + await expect(verifyMarketplaceOutput(fixture.catalogDirectory)) + .rejects.toThrow("size does not match") + + await fs.writeFile( + path.join(fixture.catalogDirectory, asset.path), + Buffer.from("plugin/fixture-plugin@1.0.0"), + ) + const registryPath = path.join(fixture.catalogDirectory, "registry-v2.json") + const registry = JSON.parse(await fs.readFile(registryPath, "utf8")) + registry.packages[0].delivery.url = registry.packages[0].delivery.url.replace( + "plugin-fixture-plugin-v1.0.0", + "plugin-wrong-v1.0.0", + ) + const changedRegistry = `${JSON.stringify(registry)}\n` + await fs.writeFile(registryPath, changedRegistry) + await fs.writeFile( + path.join(fixture.catalogDirectory, "site/registry/v2/index.json"), + changedRegistry, + ) + await fs.writeFile( + path.join( + fixture.catalogDirectory, + "releases", + `registry-v2-${"a".repeat(64)}`, + "registry-v2.json", + ), + changedRegistry, + ) + const planPath = path.join(fixture.catalogDirectory, "release-plan.json") + const plan = JSON.parse(await fs.readFile(planPath, "utf8")) + const registryAsset = plan.releases + .find((entry) => entry.tag === `registry-v2-${"a".repeat(64)}`) + .assets.find((entry) => entry.name === "registry-v2.json") + registryAsset.size = Buffer.byteLength(changedRegistry) + registryAsset.sha256 = sha256(Buffer.from(changedRegistry)) + await fs.writeFile(planPath, `${JSON.stringify(plan)}\n`) + await expect(verifyMarketplaceOutput(fixture.catalogDirectory)) + .rejects.toThrow("has no exact local Release asset") + }) + + test("rejects a Pages tree that differs from the descriptor-addressed flat catalogs", async () => { + const fixture = await writeFixture() + await fs.writeFile( + path.join(fixture.catalogDirectory, "site/registry/v2/index.json"), + "changed", + ) + await expect(verifyMarketplaceOutput(fixture.catalogDirectory)) + .rejects.toThrow("Pages registry v2 differs from its verified flat catalog") + }) +}) diff --git a/tooling/marketplace-release.mjs b/tooling/marketplace-release.mjs new file mode 100644 index 0000000..7378ec1 --- /dev/null +++ b/tooling/marketplace-release.mjs @@ -0,0 +1,382 @@ +import { createHash } from "node:crypto" +import { execFileSync } from "node:child_process" +import { promises as fs } from "node:fs" +import path from "node:path" +import { fileURLToPath } from "node:url" + +const collections = [ + { directory: "mcp-servers", kind: "mcp-server", metadata: "server.json" }, + { directory: "plugins", kind: "plugin", metadata: "convax-package.json" }, + { directory: "skills", kind: "skill", metadata: "convax-package.json" }, +] + +function sha256(input) { + return createHash("sha256").update(input).digest("hex") +} + +function itemKey(kind, id) { + return sha256(Buffer.from(`${kind}\0${id}`, "utf8")) +} + +async function collectPackageFiles(directory, relative = "") { + const entries = await fs.readdir(directory, { withFileTypes: true }) + const files = [] + for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name, "en"))) { + if (entry.name === "dist" || entry.name === "node_modules" || entry.name === ".DS_Store") continue + const absolute = path.join(directory, entry.name) + const nextRelative = relative ? `${relative}/${entry.name}` : entry.name + if (entry.isDirectory()) files.push(...await collectPackageFiles(absolute, nextRelative)) + else if (entry.isFile()) files.push({ path: nextRelative, bytes: await fs.readFile(absolute) }) + else throw new Error(`${absolute}: package source must contain only regular files and directories`) + } + return files +} + +async function collectOptionalFiles(directory, label) { + const state = await fs.lstat(directory).catch((cause) => { + if (cause?.code === "ENOENT") return undefined + throw cause + }) + if (!state) return [] + if (!state.isDirectory() || state.isSymbolicLink()) { + throw new Error(`${label} must be a real directory`) + } + return collectPackageFiles(directory) +} + +function digestFiles(files) { + const hash = createHash("sha256") + for (const file of files) { + const pathBytes = Buffer.from(file.path, "utf8") + const size = Buffer.alloc(8) + size.writeBigUInt64BE(BigInt(file.bytes.length)) + hash.update(pathBytes) + hash.update(Buffer.from([0])) + hash.update(size) + hash.update(file.bytes) + } + return hash.digest("hex") +} + +function parseIdentity(kind, metadata, label) { + const id = kind === "mcp-server" ? metadata.name : metadata.id + if (typeof id !== "string" || id.length === 0) throw new Error(`${label}: missing package identity`) + if (typeof metadata.version !== "string" || metadata.version.length === 0) { + throw new Error(`${label}: missing package version`) + } + if (kind !== "mcp-server" && metadata.kind !== kind) { + throw new Error(`${label}: metadata kind does not match its collection`) + } + return { id, version: metadata.version } +} + +function pluginCompanionSourceRoots(metadata, label) { + if (metadata.companions === undefined) return [] + if (!Array.isArray(metadata.companions)) throw new Error(`${label}: companions must be an array`) + return metadata.companions.map((companion, index) => { + const source = companion?.source + if ( + typeof source !== "string" || + !/^packages\/tools\/[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(source) + ) { + throw new Error(`${label}: companion ${index} source must name one packages/tools directory`) + } + return source + }) +} + +function pluginOwnedSkillNames(manifest, label) { + const skills = manifest?.contributes?.skills + if (skills === undefined) return [] + if (!Array.isArray(skills)) throw new Error(`${label}: contributes.skills must be an array`) + return skills.map((skill, index) => { + if ( + typeof skill?.name !== "string" || + !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(skill.name) + ) { + throw new Error(`${label}: owned Skill ${index} has an invalid name`) + } + return skill.name + }) +} + +async function readOptionalJson(file) { + return JSON.parse(await fs.readFile(file, "utf8").catch((cause) => { + if (cause?.code === "ENOENT") return "null" + throw cause + })) +} + +async function pluginLinkedSourceRoots(workspaceRoot, packageRoot, metadata, label) { + const roots = pluginCompanionSourceRoots(metadata, label) + const manifest = await readOptionalJson(path.join(packageRoot, "package", "manifest.json")) + for (const skillName of pluginOwnedSkillNames(manifest, label)) { + const skillRoot = `packages/skills/${skillName}` + const skillMetadata = await readOptionalJson(path.join( + workspaceRoot, + skillRoot, + "convax-package.json", + )) + if ( + skillMetadata?.kind !== "skill" || + skillMetadata.id !== skillName || + skillMetadata.ownerPluginId !== metadata.id + ) { + throw new Error(`${label}: owned Skill ${skillName} does not bind back to ${metadata.id}`) + } + roots.push(skillRoot) + } + return [...new Set(roots)].sort((left, right) => left.localeCompare(right, "en")) +} + +export async function packageVersionSnapshot(workspaceRoot) { + const result = new Map() + for (const collection of collections) { + const collectionRoot = path.join(workspaceRoot, "packages", collection.directory) + const directories = await fs.readdir(collectionRoot, { withFileTypes: true }).catch((cause) => { + if (cause?.code === "ENOENT") return [] + throw cause + }) + for (const directory of directories.sort((left, right) => left.name.localeCompare(right.name, "en"))) { + if (!directory.isDirectory() || directory.name.startsWith(".")) continue + const packageRoot = path.join(collectionRoot, directory.name) + const metadataPath = path.join(packageRoot, collection.metadata) + const metadata = JSON.parse(await fs.readFile(metadataPath, "utf8")) + const label = `${collection.directory}/${directory.name}` + const identity = parseIdentity(collection.kind, metadata, label) + const key = `${collection.kind}\0${identity.id}` + if (result.has(key)) throw new Error(`${label}: duplicate package identity`) + const sourceFiles = (await collectPackageFiles(packageRoot)).map((file) => ({ + ...file, + path: `${label}/${file.path}`, + })) + if (collection.kind === "plugin") { + for (const linkedRoot of await pluginLinkedSourceRoots( + workspaceRoot, + packageRoot, + metadata, + label, + )) { + sourceFiles.push(...(await collectPackageFiles(path.join(workspaceRoot, linkedRoot))) + .map((file) => ({ ...file, path: `${linkedRoot}/${file.path}` }))) + } + } + const keyDigest = itemKey(collection.kind, identity.id) + if (collection.kind === "mcp-server") { + const companionRoot = `.marketplace/companion-inputs/${keyDigest}` + sourceFiles.push(...(await collectOptionalFiles( + path.join(workspaceRoot, companionRoot), + "managed MCP companion input", + )) + .map((file) => ({ ...file, path: `${companionRoot}/${file.path}` }))) + } + result.set(key, { + digest: digestFiles(sourceFiles.sort((left, right) => + left.path.localeCompare(right.path, "en"))), + directory: label, + id: identity.id, + itemKey: keyDigest, + kind: collection.kind, + version: identity.version, + }) + } + } + return result +} + +function releaseTagFor(item) { + return item.kind === "mcp-server" + ? `mcp-server-${item.itemKey.slice(0, 16)}-v${item.version}` + : `${item.kind}-${item.id}-v${item.version}` +} + +export function changedPackageVersions(previous, current) { + for (const [key, item] of previous) { + if (!current.has(key)) { + throw new Error( + `${item.kind}/${item.id}@${item.version} was removed; publish a reviewed yanked version instead`, + ) + } + } + const changes = [] + for (const [key, item] of current) { + const old = previous.get(key) + if (old?.version === item.version) { + if (old.digest !== item.digest) { + throw new Error(`${item.kind}/${item.id}@${item.version} changed without a version change`) + } + continue + } + changes.push({ + id: item.id, + itemKey: item.itemKey, + kind: item.kind, + previousVersion: old?.version, + releaseTag: releaseTagFor(item), + version: item.version, + }) + } + return changes.sort((left, right) => + `${left.kind}\0${left.id}`.localeCompare(`${right.kind}\0${right.id}`, "en")) +} + +function git(repositoryRoot, args) { + return execFileSync("git", args, { + cwd: repositoryRoot, + encoding: args.includes("-z") ? "buffer" : "utf8", + maxBuffer: 64 * 1024 * 1024, + }) +} + +export function gitTreePackageSnapshot(repositoryRoot, revision) { + const tree = git(repositoryRoot, [ + "ls-tree", + "-r", + "-z", + revision, + "--", + "packages/plugins", + "packages/skills", + "packages/mcp-servers", + "packages/tools", + ".marketplace/companion-inputs", + ]) + const filesByPackage = new Map() + const companionFilesByItemKey = new Map() + for (const record of tree.toString("utf8").split("\0").filter(Boolean)) { + const match = /^[0-7]{6} blob ([a-f0-9]{40})\t(.+)$/.exec(record) + if (!match) continue + const [, blob, file] = match + const parts = file.split("/") + if ( + parts[0] === ".marketplace" && + parts[1] === "companion-inputs" && + parts.length >= 5 + ) { + const files = companionFilesByItemKey.get(parts[2]) ?? [] + files.push({ blob, path: parts.slice(3).join("/") }) + companionFilesByItemKey.set(parts[2], files) + continue + } + if (parts.length < 4 || parts[2].startsWith(".")) continue + const packageRoot = parts.slice(0, 3).join("/") + const files = filesByPackage.get(packageRoot) ?? [] + files.push({ blob, path: parts.slice(3).join("/") }) + filesByPackage.set(packageRoot, files) + } + const result = new Map() + for (const [packageRoot, files] of filesByPackage) { + const [, collection, directory] = packageRoot.split("/") + const definition = collections.find((item) => item.directory === collection) + if (!definition) continue + const metadataRecord = files.find((item) => item.path === definition.metadata) + if (!metadataRecord) { + throw new Error(`${packageRoot}: missing ${definition.metadata}`) + } + const metadata = JSON.parse(git(repositoryRoot, [ + "show", + `${revision}:${packageRoot}/${definition.metadata}`, + ])) + const identity = parseIdentity(definition.kind, metadata, packageRoot) + const key = `${definition.kind}\0${identity.id}` + if (result.has(key)) throw new Error(`${packageRoot}: duplicate package identity`) + const sourceFiles = files.map((file) => ({ + ...file, + path: `${packageRoot}/${file.path}`, + })) + if (definition.kind === "plugin") { + for (const linkedRoot of pluginCompanionSourceRoots(metadata, packageRoot)) { + const linked = filesByPackage.get(linkedRoot) + if (!linked) throw new Error(`${packageRoot}: missing linked companion source ${linkedRoot}`) + sourceFiles.push(...linked.map((file) => ({ + ...file, + path: `${linkedRoot}/${file.path}`, + }))) + } + const manifestRecord = files.find((file) => file.path === "package/manifest.json") + const manifest = manifestRecord + ? JSON.parse(git(repositoryRoot, [ + "show", + `${revision}:${packageRoot}/package/manifest.json`, + ])) + : undefined + for (const skillName of pluginOwnedSkillNames(manifest, packageRoot)) { + const skillRoot = `packages/skills/${skillName}` + const linked = filesByPackage.get(skillRoot) + if (!linked) throw new Error(`${packageRoot}: missing owned Skill ${skillName}`) + const skillMetadataRecord = linked.find((file) => file.path === "convax-package.json") + if (!skillMetadataRecord) throw new Error(`${skillRoot}: missing convax-package.json`) + const skillMetadata = JSON.parse(git(repositoryRoot, [ + "show", + `${revision}:${skillRoot}/convax-package.json`, + ])) + if ( + skillMetadata.kind !== "skill" || + skillMetadata.id !== skillName || + skillMetadata.ownerPluginId !== metadata.id + ) { + throw new Error(`${packageRoot}: owned Skill ${skillName} does not bind back to ${metadata.id}`) + } + sourceFiles.push(...linked.map((file) => ({ + ...file, + path: `${skillRoot}/${file.path}`, + }))) + } + } + const keyDigest = itemKey(definition.kind, identity.id) + if (definition.kind === "mcp-server") { + const companionRoot = `.marketplace/companion-inputs/${keyDigest}` + sourceFiles.push(...(companionFilesByItemKey.get(keyDigest) ?? []).map((file) => ({ + ...file, + path: `${companionRoot}/${file.path}`, + }))) + } + const digest = sha256(sourceFiles + .sort((left, right) => left.path.localeCompare(right.path, "en")) + .map((file) => `${file.path}\0${file.blob}\n`) + .join("")) + result.set(key, { + digest, + directory: `${collection}/${directory}`, + id: identity.id, + itemKey: keyDigest, + kind: definition.kind, + version: identity.version, + }) + } + return result +} + +function parseCliArgs(argv) { + const result = {} + for (let index = 0; index < argv.length; index += 1) { + const argument = argv[index] + if (!argument.startsWith("--")) throw new Error(`Unexpected argument ${argument}`) + const key = argument.slice(2) + if (!["base", "head", "output"].includes(key) || result[key] !== undefined) { + throw new Error(`Unsupported or duplicate argument ${argument}`) + } + const value = argv[index + 1] + if (!value || value.startsWith("--")) throw new Error(`Missing value for ${argument}`) + result[key] = value + index += 1 + } + if (!result.base || !result.output) throw new Error("Usage: marketplace-release --base --output [--head ]") + return result +} + +async function main(argv) { + const args = parseCliArgs(argv) + const repositoryRoot = path.resolve(fileURLToPath(new URL("..", import.meta.url))) + const head = args.head ?? "HEAD" + const changes = changedPackageVersions( + gitTreePackageSnapshot(repositoryRoot, args.base), + gitTreePackageSnapshot(repositoryRoot, head), + ) + const output = path.resolve(repositoryRoot, args.output) + await fs.mkdir(path.dirname(output), { recursive: true }) + await fs.writeFile(output, `${JSON.stringify(changes, null, 2)}\n`) + console.log(`Selected ${changes.length} version-change release${changes.length === 1 ? "" : "s"}.`) +} + +if (import.meta.main) await main(process.argv.slice(2)) diff --git a/tooling/marketplace-release.test.js b/tooling/marketplace-release.test.js new file mode 100644 index 0000000..d03ec79 --- /dev/null +++ b/tooling/marketplace-release.test.js @@ -0,0 +1,362 @@ +import { afterAll, describe, expect, test } from "bun:test" +import { createHash } from "node:crypto" +import { execFileSync } from "node:child_process" +import { promises as fs } from "node:fs" +import os from "node:os" +import path from "node:path" +import { + changedPackageVersions, + gitTreePackageSnapshot, + packageVersionSnapshot, +} from "./marketplace-release.mjs" +import { composePublicationPlan } from "./publication-plan.mjs" + +const temporaryDirectories = [] + +async function temporaryDirectory() { + const directory = await fs.mkdtemp(path.join(os.tmpdir(), "convax-marketplace-release-")) + temporaryDirectories.push(directory) + return directory +} + +async function writePackage(root, kind, id, version, body = {}) { + const directory = path.join(root, "packages", kind, id) + await fs.mkdir(directory, { recursive: true }) + const marker = kind === "mcp-servers" + ? { name: `io.github.microvoid/${id}`, description: `${id} server`, version, ...body } + : { + schema: "convax.package/2", + kind: kind === "plugins" ? "plugin" : "skill", + id, + name: id, + description: `${id} package`, + version, + ...body, + } + await fs.writeFile( + path.join(directory, kind === "mcp-servers" ? "server.json" : "convax-package.json"), + `${JSON.stringify(marker, null, 2)}\n`, + ) +} + +async function writePluginClosure(root, version = "1.0.0") { + await writePackage(root, "plugins", "closed-plugin", version, { + companions: [{ + command: "closed-tool", + version: "1.0.0", + source: "packages/tools/closed-tool", + targets: [{ platform: "darwin", arch: "arm64", path: "dist/closed-tool" }], + }], + }) + await fs.mkdir(path.join(root, "packages/plugins/closed-plugin/package"), { recursive: true }) + await fs.writeFile( + path.join(root, "packages/plugins/closed-plugin/package/manifest.json"), + `${JSON.stringify({ + schema: "convax.plugin/4", + id: "closed-plugin", + version, + name: "Closed Plugin", + contributes: { skills: [{ name: "closed-skill", path: "skills/closed-skill" }] }, + }, null, 2)}\n`, + ) + await writePackage(root, "skills", "closed-skill", version, { + ownerPluginId: "closed-plugin", + }) + await fs.mkdir(path.join(root, "packages/skills/closed-skill/package"), { recursive: true }) + await fs.writeFile( + path.join(root, "packages/skills/closed-skill/package/SKILL.md"), + "---\nname: closed-skill\n---\n\n# Closed Skill\n", + ) + await fs.mkdir(path.join(root, "packages/tools/closed-tool/src"), { recursive: true }) + await fs.writeFile(path.join(root, "packages/tools/closed-tool/src/main.ts"), "export const version = 1\n") +} + +async function writeManagedMcpCompanion(root, id, bytes) { + const itemKey = createHash("sha256") + .update(Buffer.from(`mcp-server\0${id}`, "utf8")) + .digest("hex") + const companion = path.join( + root, + ".marketplace", + "companion-inputs", + itemKey, + "darwin-arm64", + "fixture-mcp", + ) + await fs.mkdir(path.dirname(companion), { recursive: true }) + await fs.writeFile(companion, bytes) +} + +afterAll(async () => { + await Promise.all(temporaryDirectories.map((directory) => + fs.rm(directory, { recursive: true, force: true }))) +}) + +describe("default-branch version-change release selection", () => { + test("returns an exact empty plan when every package version and byte is unchanged", async () => { + const unchanged = await temporaryDirectory() + await writePackage(unchanged, "plugins", "example-plugin", "1.0.0") + await writePackage(unchanged, "skills", "example-skill", "2.0.0") + await writePackage(unchanged, "mcp-servers", "example-http", "2026.07") + const snapshot = await packageVersionSnapshot(unchanged) + + expect(changedPackageVersions(snapshot, snapshot)).toEqual([]) + }) + + test("selects Plugin, Skill, and MCP Server version changes with stable release identities", async () => { + const previous = await temporaryDirectory() + const current = await temporaryDirectory() + for (const root of [previous, current]) { + await writePackage(root, "plugins", "example-plugin", root === previous ? "1.0.0" : "1.1.0") + await writePackage(root, "skills", "example-skill", "2.0.0") + await writePackage(root, "mcp-servers", "example-http", root === previous ? "2026.07" : "2026.08") + } + + const changes = changedPackageVersions( + await packageVersionSnapshot(previous), + await packageVersionSnapshot(current), + ) + + expect(changes).toEqual([ + { + id: "io.github.microvoid/example-http", + itemKey: expect.stringMatching(/^[a-f0-9]{64}$/), + kind: "mcp-server", + previousVersion: "2026.07", + releaseTag: expect.stringMatching(/^mcp-server-[a-f0-9]{16}-v2026\.08$/), + version: "2026.08", + }, + { + id: "example-plugin", + itemKey: expect.stringMatching(/^[a-f0-9]{64}$/), + kind: "plugin", + previousVersion: "1.0.0", + releaseTag: "plugin-example-plugin-v1.1.0", + version: "1.1.0", + }, + ]) + }) + + test("rejects changed immutable package bytes without a version change", async () => { + const previous = await temporaryDirectory() + const current = await temporaryDirectory() + await writePackage(previous, "skills", "example-skill", "1.0.0", { description: "old bytes" }) + await writePackage(current, "skills", "example-skill", "1.0.0", { description: "new bytes" }) + + const previousSnapshot = await packageVersionSnapshot(previous) + const currentSnapshot = await packageVersionSnapshot(current) + expect(() => changedPackageVersions(previousSnapshot, currentSnapshot)) + .toThrow("changed without a version change") + }) + + test("requires a reviewed yanked version instead of silently removing a package", async () => { + const previous = await temporaryDirectory() + const current = await temporaryDirectory() + await writePackage(previous, "skills", "removed-skill", "1.0.0") + + const previousSnapshot = await packageVersionSnapshot(previous) + const currentSnapshot = await packageVersionSnapshot(current) + expect(() => changedPackageVersions(previousSnapshot, currentSnapshot)) + .toThrow("skill/removed-skill@1.0.0 was removed") + }) + + test("binds linked companion and owned Skill sources to the Plugin version", async () => { + const previous = await temporaryDirectory() + const current = await temporaryDirectory() + await writePluginClosure(previous) + await writePluginClosure(current) + + await fs.writeFile( + path.join(current, "packages/tools/closed-tool/src/main.ts"), + "export const version = 2\n", + ) + const previousSnapshot = await packageVersionSnapshot(previous) + let currentSnapshot = await packageVersionSnapshot(current) + expect(() => changedPackageVersions(previousSnapshot, currentSnapshot)) + .toThrow("plugin/closed-plugin@1.0.0 changed without a version change") + + await fs.writeFile( + path.join(current, "packages/tools/closed-tool/src/main.ts"), + "export const version = 1\n", + ) + await fs.writeFile( + path.join(current, "packages/skills/closed-skill/package/SKILL.md"), + "---\nname: closed-skill\n---\n\n# Changed Skill\n", + ) + currentSnapshot = await packageVersionSnapshot(current) + expect(() => changedPackageVersions(previousSnapshot, currentSnapshot)) + .toThrow("plugin/closed-plugin@1.0.0 changed without a version change") + }) + + test("binds scaffold-owned managed MCP companion inputs to the MCP Server version", async () => { + const previous = await temporaryDirectory() + const current = await temporaryDirectory() + const id = "io.github.microvoid/managed-example" + for (const root of [previous, current]) { + await writePackage(root, "mcp-servers", "managed-example", "1.0.0", { + name: id, + }) + } + await writeManagedMcpCompanion(previous, id, "previous companion bytes") + await writeManagedMcpCompanion(current, id, "changed companion bytes") + + const previousSnapshot = await packageVersionSnapshot(previous) + const currentSnapshot = await packageVersionSnapshot(current) + expect(() => changedPackageVersions( + previousSnapshot, + currentSnapshot, + )).toThrow("mcp-server/io.github.microvoid/managed-example@1.0.0 changed without a version change") + }) + + test("uses the same managed MCP companion closure for the production Git-tree selector", async () => { + const repository = await temporaryDirectory() + const id = "io.github.microvoid/git-managed-example" + await writePackage(repository, "mcp-servers", "git-managed-example", "1.0.0", { + name: id, + }) + await writeManagedMcpCompanion(repository, id, "previous companion bytes") + const git = (args) => execFileSync("git", args, { + cwd: repository, + encoding: "utf8", + }).trim() + git(["init"]) + git(["config", "user.email", "fixture@example.test"]) + git(["config", "user.name", "Fixture"]) + git(["add", "."]) + git(["commit", "-m", "initial"]) + const previousRevision = git(["rev-parse", "HEAD"]) + + await writeManagedMcpCompanion(repository, id, "changed companion bytes") + git(["add", "."]) + git(["commit", "-m", "change companion"]) + const currentRevision = git(["rev-parse", "HEAD"]) + + expect(() => changedPackageVersions( + gitTreePackageSnapshot(repository, previousRevision), + gitTreePackageSnapshot(repository, currentRevision), + )).toThrow("mcp-server/io.github.microvoid/git-managed-example@1.0.0 changed without a version change") + }) + + test("does not follow a scaffold-owned managed MCP companion root outside the repository", async () => { + const repository = await temporaryDirectory() + const outside = await temporaryDirectory() + const id = "io.github.microvoid/symlinked-managed-example" + await writePackage(repository, "mcp-servers", "symlinked-managed-example", "1.0.0", { + name: id, + }) + await fs.writeFile(path.join(outside, "outside-companion"), "outside bytes") + const itemKey = createHash("sha256") + .update(Buffer.from(`mcp-server\0${id}`, "utf8")) + .digest("hex") + const inputs = path.join(repository, ".marketplace", "companion-inputs") + await fs.mkdir(inputs, { recursive: true }) + await fs.symlink(outside, path.join(inputs, itemKey)) + + await expect(packageVersionSnapshot(repository)) + .rejects.toThrow("managed MCP companion input must be a real directory") + }) + + test("publishes only from a protected default-branch version change", async () => { + const workflow = await fs.readFile(path.join( + import.meta.dir, + "..", + ".github/workflows/release-on-main.yml", + ), "utf8") + const pages = await fs.readFile(path.join( + import.meta.dir, + "..", + ".github/workflows/pages.yml", + ), "utf8") + expect(workflow).toContain("branches: [main]") + expect(workflow).not.toContain("tags:") + expect(workflow).toContain("bun tooling/marketplace-release.mjs") + expect(workflow).toContain("--base \"$BASE_SHA\"") + expect(workflow).toContain("permissions:\n contents: read") + expect(workflow).toContain("attestations: write") + expect(workflow).toContain("contents: write") + expect(workflow).toContain("id-token: write") + expect(workflow).toContain("dist/catalog/releases/$tag") + expect(workflow).toContain("uses: ./.github/workflows/pages.yml") + expect(workflow).toContain("fetch-marketplace-previous.mjs") + expect(workflow).toContain("publication-plan.mjs") + expect(workflow).toContain("gh release download") + expect(workflow).toContain("cmp \"$asset\"") + expect(workflow).not.toContain("already exists; immutable versions are never overwritten") + expect(workflow.match(/if: steps\.plan\.outputs\.count != '0'/g)?.length).toBeGreaterThanOrEqual(5) + expect(workflow).toContain("if: needs.verify.outputs.count != '0'") + expect(workflow).toContain("needs.publish.result == 'success'") + expect(workflow).not.toContain("needs.publish.result == 'skipped'") + expect(workflow).not.toContain("pull_request_target") + expect(pages).toContain("workflow_call:") + expect(pages).not.toContain("workflow_run:") + expect(pages).not.toContain("concurrency:") + }) + + test("publishes changed packages with one metadata Release and the changed Builtin bundle", () => { + const selected = [ + { + kind: "plugin", + id: "ffmpeg-tools", + releaseTag: "plugin-ffmpeg-tools-v0.3.1", + }, + { + kind: "skill", + id: "canvas-storyboard", + releaseTag: "skill-canvas-storyboard-v0.1.0", + }, + ] + const catalog = { + schema: "convax.release-plan/1", + releases: [ + { + tag: "plugin-ffmpeg-tools-v0.3.1", + assets: [{ path: "releases/plugin-ffmpeg-tools-v0.3.1/plugin.zip" }], + }, + { + tag: "skill-canvas-storyboard-v0.1.0", + assets: [{ path: "releases/skill-canvas-storyboard-v0.1.0/skill.zip" }], + }, + { + tag: `registry-v2-${"a".repeat(64)}`, + assets: [{ path: `releases/registry-v2-${"a".repeat(64)}/registry-v2.json` }], + }, + ], + } + const builtin = { + schema: "convax.release-plan/1", + releases: [{ + tag: "builtin-release", + assets: [{ path: "releases/builtin-release/convax-builtin-bundle.zip" }], + }], + } + expect(composePublicationPlan({ builtin, catalog, selected })).toEqual({ + schema: "convax.publication-plan/1", + releases: [ + { + directory: "builtin/releases/builtin-release", + tag: "builtin-release", + }, + { + directory: "catalog/releases/plugin-ffmpeg-tools-v0.3.1", + tag: "plugin-ffmpeg-tools-v0.3.1", + }, + { + directory: `catalog/releases/registry-v2-${"a".repeat(64)}`, + tag: `registry-v2-${"a".repeat(64)}`, + }, + { + directory: "catalog/releases/skill-canvas-storyboard-v0.1.0", + tag: "skill-canvas-storyboard-v0.1.0", + }, + ], + }) + expect(() => composePublicationPlan({ + builtin, + catalog: { + ...catalog, + releases: catalog.releases.filter((entry) => !entry.tag.startsWith("registry-v2-")), + }, + selected, + })).toThrow("exactly one Registry metadata Release") + }) +}) diff --git a/tooling/official-marketplace-build.mjs b/tooling/official-marketplace-build.mjs new file mode 100644 index 0000000..c8d38de --- /dev/null +++ b/tooling/official-marketplace-build.mjs @@ -0,0 +1,65 @@ +import { spawnSync } from "node:child_process" +import path from "node:path" +import { fileURLToPath } from "node:url" + +export function officialBuildArgs({ bootstrapPreviousV1, changed, previous, v1Revision }) { + if (bootstrapPreviousV1 && previous) { + throw new Error("Official build accepts exactly one previous Registry mode") + } + if (typeof v1Revision !== "string" || !/^[a-f0-9]{40}$/.test(v1Revision)) { + throw new Error("Official v1 revision must be an exact Git SHA") + } + const args = [ + "build-index", + ".", + "--out", + "dist/catalog", + "--official", + ] + if (changed) args.push("--changed", changed) + if (previous) return [...args, "--previous", previous, "--v1-revision", v1Revision] + if (bootstrapPreviousV1) { + return [ + ...args, + "--bootstrap-previous-v1", + bootstrapPreviousV1, + "--v1-revision", + v1Revision, + ] + } + return [...args, "--initial", "--v1-revision", v1Revision] +} + +function main() { + const root = path.resolve(fileURLToPath(new URL("..", import.meta.url))) + const v1Revision = process.env.GITHUB_SHA ?? spawnSync( + "git", + ["rev-parse", "HEAD"], + { cwd: root, encoding: "utf8" }, + ).stdout?.trim() + const args = officialBuildArgs({ + bootstrapPreviousV1: process.env.CONVAX_MARKETPLACE_BOOTSTRAP_PREVIOUS_V1, + changed: process.env.CONVAX_MARKETPLACE_CHANGED, + previous: process.env.CONVAX_MARKETPLACE_PREVIOUS, + v1Revision, + }) + const result = spawnSync("convax-marketplace", args, { + cwd: root, + encoding: "utf8", + env: process.env, + stdio: "inherit", + }) + if (result.error) throw result.error + if (result.status !== 0) { + throw new Error(`convax-marketplace exited with status ${String(result.status)}`) + } +} + +if (import.meta.main) { + try { + main() + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)) + process.exitCode = 1 + } +} diff --git a/tooling/official-marketplace.mjs b/tooling/official-marketplace.mjs new file mode 100644 index 0000000..2d4c123 --- /dev/null +++ b/tooling/official-marketplace.mjs @@ -0,0 +1,94 @@ +import { promises as fs } from "node:fs" +import path from "node:path" + +function isObject(value) { + return value !== null && typeof value === "object" && !Array.isArray(value) +} + +function exactKeys(value, allowed, required, label) { + if (!isObject(value)) throw new Error(`${label}: must be an object`) + const unknown = Object.keys(value).find((key) => !allowed.includes(key)) + if (unknown) throw new Error(`${label}: unsupported field ${unknown}`) + const missing = required.find((key) => !Object.hasOwn(value, key)) + if (missing) throw new Error(`${label}: missing field ${missing}`) +} + +function assertDescriptor(descriptor) { + exactKeys( + descriptor, + ["schema", "id", "name", "publisher", "repository", "registry", "showcase", "compatibility", "delivery"], + ["schema", "id", "name", "publisher", "repository", "registry", "showcase", "compatibility", "delivery"], + "marketplace.json", + ) + if (descriptor.schema !== "convax.marketplace/1" || descriptor.id !== "convax-official") { + throw new Error("marketplace.json: invalid Official identity") + } + exactKeys(descriptor.publisher, ["name"], ["name"], "marketplace.json publisher") + exactKeys(descriptor.repository, ["owner", "name"], ["owner", "name"], "marketplace.json repository") + if (descriptor.repository.owner !== "microvoid" || descriptor.repository.name !== "convax-plugins") { + throw new Error("marketplace.json: repository must remain microvoid/convax-plugins") + } + exactKeys(descriptor.registry, ["v1", "v2"], ["v1", "v2"], "marketplace.json registry") + exactKeys(descriptor.registry.v1, ["url"], ["url"], "marketplace.json registry v1") + exactKeys(descriptor.registry.v2, ["url"], ["url"], "marketplace.json registry v2") + exactKeys(descriptor.showcase, ["v2"], ["v2"], "marketplace.json showcase") + exactKeys(descriptor.showcase.v2, ["url"], ["url"], "marketplace.json showcase v2") + exactKeys(descriptor.compatibility, ["convax"], ["convax"], "marketplace.json compatibility") + exactKeys(descriptor.delivery, ["kind"], ["kind"], "marketplace.json delivery") + if (descriptor.delivery.kind !== "github-pages-releases") { + throw new Error("marketplace.json: Official delivery must use GitHub Pages and Releases") + } +} + +function assertBuiltin(builtin) { + exactKeys(builtin, ["schema", "members"], ["schema", "members"], "catalogs/builtin.json") + if (builtin.schema !== "convax.builtin-config/1") { + throw new Error("catalogs/builtin.json: unsupported schema") + } + if ( + !Array.isArray(builtin.members) || + builtin.members.length !== 1 || + builtin.members[0]?.kind !== "skill" || + builtin.members[0]?.id !== "canvas-storyboard" + ) { + throw new Error("catalogs/builtin.json: v1 contains only skill/canvas-storyboard") + } +} + +function assertPreinstalled(preinstalled) { + exactKeys(preinstalled, ["schema", "packages"], ["schema", "packages"], "catalogs/preinstalled.json") + const item = preinstalled.packages?.[0] + if ( + preinstalled.schema !== "convax.preinstalled-config/1" || + preinstalled.packages?.length !== 1 || + item.marketplaceId !== "convax-official" || + item.kind !== "plugin" || + item.id !== "ffmpeg-tools" || + item.setup !== "explicit" || + item.targets?.length !== 1 || + item.targets[0] !== "darwin-arm64" + ) { + throw new Error("catalogs/preinstalled.json: v1 contains only darwin-arm64 Official ffmpeg-tools") + } +} + +export function assertOfficialMarketplaceSource(source) { + assertDescriptor(source.descriptor) + assertBuiltin(source.builtin) + assertPreinstalled({ schema: "convax.preinstalled-config/1", packages: source.preinstalled }) +} + +export async function loadOfficialMarketplaceSource(workspaceRoot) { + const readJson = async (relative) => + JSON.parse(await fs.readFile(path.join(workspaceRoot, relative), "utf8")) + const descriptor = await readJson("marketplace.json") + const builtin = await readJson("catalogs/builtin.json") + const preinstalledConfig = await readJson("catalogs/preinstalled.json") + const source = { + descriptor, + builtin, + preinstalled: preinstalledConfig.packages, + } + assertOfficialMarketplaceSource(source) + return source +} diff --git a/tooling/official-marketplace.test.js b/tooling/official-marketplace.test.js new file mode 100644 index 0000000..0f527a3 --- /dev/null +++ b/tooling/official-marketplace.test.js @@ -0,0 +1,285 @@ +import { describe, expect, test } from "bun:test" +import { promises as fs } from "node:fs" +import os from "node:os" +import path from "node:path" +import { + assertOfficialMarketplaceSource, + loadOfficialMarketplaceSource, +} from "./official-marketplace.mjs" +import { officialBuildArgs } from "./official-marketplace-build.mjs" +import { fetchPreviousRegistry } from "./fetch-marketplace-previous.mjs" +import { root, sha256 } from "./lib.mjs" + +describe("Official and Builtin marketplace source", () => { + const strictRegistryParser = (version) => (value) => { + const topLevel = version === 2 + ? ["schema", "marketplaceId", "sequence", "revision", "packages"] + : ["schema", "sequence", "revision", "packages"] + const unknown = Object.keys(value).find((key) => !topLevel.includes(key)) + if (unknown) throw new Error(`unknown field ${unknown}`) + const identities = new Set() + for (const entry of value.packages) { + if (typeof entry?.kind !== "string" || typeof entry.id !== "string") { + throw new Error("bad package") + } + const identity = `${entry.kind}/${entry.id}` + if (identities.has(identity)) throw new Error(`duplicate ${identity}`) + identities.add(identity) + } + return value + } + + test("owns the approved descriptor, Builtin member, and preinstalled closure", async () => { + const source = await loadOfficialMarketplaceSource(root) + expect(source.descriptor).toEqual({ + schema: "convax.marketplace/1", + id: "convax-official", + name: "Convax Official", + publisher: { + name: "Microvoid", + }, + repository: { owner: "microvoid", name: "convax-plugins" }, + registry: { + v1: { url: "https://microvoid.github.io/convax-plugins/registry/v1/index.json" }, + v2: { url: "https://microvoid.github.io/convax-plugins/registry/v2/index.json" }, + }, + showcase: { + v2: { url: "https://microvoid.github.io/convax-plugins/showcase/v2/index.json" }, + }, + compatibility: { convax: ">=0.1.0" }, + delivery: { kind: "github-pages-releases" }, + }) + expect(source.builtin.members).toEqual([ + { kind: "skill", id: "canvas-storyboard" }, + ]) + expect(source.preinstalled).toEqual([ + { + marketplaceId: "convax-official", + kind: "plugin", + id: "ffmpeg-tools", + targets: ["darwin-arm64"], + setup: "explicit", + }, + ]) + expect(() => assertOfficialMarketplaceSource(source)).not.toThrow() + }) + + test("keeps the approved standalone Storyboard Skill bytes fixed in its sole source", async () => { + const sourceSkill = await fs.readFile(path.join( + root, + "packages/skills/canvas-storyboard/package/SKILL.md", + )) + expect(sha256(sourceSkill)).toBe("76efa86e73ae8ca0581f3000ec6a622ee8479ec42a6d1e15892ec0051506b9d8") + }) + + test("publishes HTTP and managed-stdio MCP Server fixtures without mixing profiles", async () => { + const http = JSON.parse(await fs.readFile( + path.join(root, "tooling/fixtures/mcp-servers/official-http-example/server.json"), + "utf8", + )) + expect(http.remotes).toEqual([ + { + type: "streamable-http", + url: "https://mcp.example.com/v1", + }, + ]) + await expect(fs.stat(path.join( + root, + "tooling/fixtures/mcp-servers/official-http-example/convax-mcp.json", + ))).rejects.toMatchObject({ code: "ENOENT" }) + + const managedRoot = path.join(root, "tooling/fixtures/mcp-servers/official-managed-example") + const managed = JSON.parse(await fs.readFile(path.join(managedRoot, "server.json"), "utf8")) + const extension = JSON.parse(await fs.readFile(path.join(managedRoot, "convax-mcp.json"), "utf8")) + expect(managed.remotes ?? []).toEqual([]) + expect(managed.packages ?? []).toEqual([]) + expect(extension.runtime.kind).toBe("managed-stdio") + expect(extension.runtime.command).toBe("convax-managed-example-mcp") + expect(extension.runtime.argv).toEqual([]) + expect(extension).not.toHaveProperty("id") + expect(extension).not.toHaveProperty("version") + }) + + test("uses an explicit production snapshot in CI and an explicit initial candidate locally", () => { + expect(officialBuildArgs({ + changed: "dist/release-plan.json", + previous: "dist/production/registry-v2.json", + v1Revision: "a".repeat(40), + })).toEqual([ + "build-index", + ".", + "--out", + "dist/catalog", + "--official", + "--changed", + "dist/release-plan.json", + "--previous", + "dist/production/registry-v2.json", + "--v1-revision", + "a".repeat(40), + ]) + expect(officialBuildArgs({ + bootstrapPreviousV1: "dist/production/registry-v1.json", + v1Revision: "a".repeat(40), + })).toEqual([ + "build-index", + ".", + "--out", + "dist/catalog", + "--official", + "--bootstrap-previous-v1", + "dist/production/registry-v1.json", + "--v1-revision", + "a".repeat(40), + ]) + expect(officialBuildArgs({ v1Revision: "a".repeat(40) })).toEqual([ + "build-index", + ".", + "--out", + "dist/catalog", + "--official", + "--initial", + "--v1-revision", + "a".repeat(40), + ]) + expect(() => officialBuildArgs({ + bootstrapPreviousV1: "dist/production/registry-v1.json", + previous: "dist/production/registry-v2.json", + v1Revision: "a".repeat(40), + })).toThrow("exactly one previous Registry mode") + expect(() => officialBuildArgs({ v1Revision: "bad" })) + .toThrow("v1 revision must be an exact Git SHA") + }) + + test("prefers production v2 and bootstraps from strict v1 only after an exact v2 404", async () => { + const output = await fs.mkdtemp(path.join(os.tmpdir(), "convax-marketplace-previous-")) + try { + const v2Bytes = JSON.stringify({ + schema: "convax.registry/2", + marketplaceId: "convax-official", + sequence: 45, + revision: "a".repeat(64), + packages: [], + }) + let requests = [] + const v2 = await fetchPreviousRegistry({ + fetchImpl: async (url) => { + requests.push(url) + return new Response(v2Bytes, { status: 200 }) + }, + outputDirectory: output, + parseV1: strictRegistryParser(1), + parseV2: strictRegistryParser(2), + v1Url: "https://example.test/v1", + v2Url: "https://example.test/v2", + }) + expect(v2.mode).toBe("v2") + expect(requests).toEqual(["https://example.test/v2"]) + + requests = [] + const bootstrap = await fetchPreviousRegistry({ + fetchImpl: async (url) => { + requests.push(url) + return url.endsWith("/v2") + ? new Response("", { status: 404 }) + : new Response(JSON.stringify({ + schema: "convax.registry/1", + sequence: 44, + revision: "b".repeat(40), + packages: [], + }), { status: 200 }) + }, + outputDirectory: output, + parseV1: strictRegistryParser(1), + parseV2: strictRegistryParser(2), + v1Url: "https://example.test/v1", + v2Url: "https://example.test/v2", + }) + expect(bootstrap.mode).toBe("bootstrap-v1") + expect(requests).toEqual(["https://example.test/v2", "https://example.test/v1"]) + + requests = [] + await expect(fetchPreviousRegistry({ + fetchImpl: async (url) => { + requests.push(url) + return new Response("", { status: 503 }) + }, + outputDirectory: output, + parseV1: strictRegistryParser(1), + parseV2: strictRegistryParser(2), + v1Url: "https://example.test/v1", + v2Url: "https://example.test/v2", + })).rejects.toThrow("v2 returned HTTP 503") + expect(requests).toEqual(["https://example.test/v2"]) + + await expect(fetchPreviousRegistry({ + fetchImpl: async (url) => url.endsWith("/v2") + ? new Response("", { status: 404 }) + : new Response(JSON.stringify({ + schema: "convax.registry/1", + sequence: 0, + revision: "bad", + packages: [], + }), { status: 200 }), + outputDirectory: output, + parseV1: strictRegistryParser(1), + parseV2: strictRegistryParser(2), + v1Url: "https://example.test/v1", + v2Url: "https://example.test/v2", + })).rejects.toThrow("v1 is not a strict sequence input") + + await expect(fetchPreviousRegistry({ + fetchImpl: async () => new Response(JSON.stringify({ + schema: "convax.registry/2", + marketplaceId: "convax-official", + sequence: 45, + revision: "a".repeat(64), + packages: [ + { kind: "skill", id: "duplicate" }, + { kind: "skill", id: "duplicate" }, + ], + unexpected: true, + }), { status: 200 }), + outputDirectory: output, + parseV1: strictRegistryParser(1), + parseV2: strictRegistryParser(2), + v1Url: "https://example.test/v1", + v2Url: "https://example.test/v2", + })).rejects.toThrow("strict validation failed") + } finally { + await fs.rm(output, { recursive: true, force: true }) + } + }) + + test("keeps no-op publication inert and deploys only reverified low-privilege bytes", async () => { + const releaseWorkflow = await fs.readFile( + path.join(root, ".github/workflows/release-on-main.yml"), + "utf8", + ) + const pagesWorkflow = await fs.readFile( + path.join(root, ".github/workflows/pages.yml"), + "utf8", + ) + expect(releaseWorkflow).toContain("branches: [main]") + expect(releaseWorkflow).not.toContain("pull_request_target") + expect(releaseWorkflow).toContain("if: steps.plan.outputs.count != '0'") + expect(releaseWorkflow).toContain("if: needs.verify.outputs.count != '0'") + expect(releaseWorkflow).toContain("needs: [verify, publish]") + expect(releaseWorkflow).toContain("uses: ./.github/workflows/pages.yml") + expect(pagesWorkflow).toContain( + "bun tooling/verify-marketplace-output.mjs dist/catalog", + ) + expect(pagesWorkflow).toContain( + "bun tooling/verify-product-lock-input.mjs dist/product-lock-input.json", + ) + expect(releaseWorkflow).toContain( + "cp schemas/*.json dist/catalog/site/schemas/", + ) + expect(pagesWorkflow).toContain('cmp "$schema" "dist/catalog/site/schemas/$(basename "$schema")"') + expect(pagesWorkflow).toContain("path: dist/catalog/site") + expect(pagesWorkflow).not.toContain("cp schemas/*.json") + expect(pagesWorkflow).not.toContain("cp dist/catalog/registry-v2.json") + expect(pagesWorkflow).not.toContain("cp dist/catalog/showcase-v2.json") + expect(pagesWorkflow).not.toContain("path: dist/site") + }) +}) diff --git a/tooling/plugin-v4.test.js b/tooling/plugin-v4.test.js index f66ac0d..d032f53 100644 --- a/tooling/plugin-v4.test.js +++ b/tooling/plugin-v4.test.js @@ -121,4 +121,35 @@ describe("convax.plugin/4 owned Skill contributions", () => { yanked: false, })).toThrow("ownerPluginId is available only to Skills") }) + + test("does not backport v6 image return-selection actions", () => { + const imageAction = manifest({ + entry: "index.html", + contributes: { + canvas: { + renderer: { create: true }, + selectionActions: [{ + description: { default: "Import one image." }, + editor: "confirmation", + id: "import-image", + steps: [{ tool: "image.import" }], + target: "image", + title: { default: "Import image" }, + }], + }, + generation: { + models: [], + tools: [{ + acceptedInputs: ["reference_image"], + description: "Import one selected image.", + id: "image.import", + output: "image", + title: "Import image", + }], + }, + }, + }) + + expect(() => parsePluginManifest(imageAction)).toThrow("target must be video") + }) }) diff --git a/tooling/plugin-v5-v6.test.js b/tooling/plugin-v5-v6.test.js index 57bc10e..bbe208e 100644 --- a/tooling/plugin-v5-v6.test.js +++ b/tooling/plugin-v5-v6.test.js @@ -106,6 +106,39 @@ describe("convax.plugin/5 capability host contract", () => { capabilities: ["canvas.connectedInputs.read"], }))).toThrow("invalid or duplicate capability") }) + + test("does not backport v6 image return-selection actions", () => { + const imageAction = v5Manifest({ + capabilities: [], + contributes: { + canvas: { + renderer: { create: true }, + selectionActions: [{ + description: { default: "Import one image." }, + editor: "confirmation", + id: "import-image", + steps: [{ tool: "image.import" }], + target: "image", + title: { default: "Import image" }, + }], + }, + generation: { + models: [], + tools: [{ + acceptedInputs: ["reference_image"], + description: "Import one selected image.", + id: "image.import", + output: "image", + title: "Import image", + }], + }, + }, + entry: "index.html", + runtime: { command: "image-import-mcp", type: "mcp-stdio" }, + }) + + expect(() => parsePluginManifest(imageAction)).toThrow("target must be video") + }) }) describe("convax.plugin/6 remote Agent MCP contract", () => { @@ -304,6 +337,69 @@ describe("convax.plugin/6 remote Agent MCP contract", () => { }))).toThrow("cannot reference direct-incoming operation") }) + test("admits bounded image and video selection sinks without exposing them to Agent tools", () => { + const manifest = v6Manifest(undefined, { + capabilities: ["generation.execute"], + contributes: { + canvas: { + renderer: { create: true }, + selectionActions: [ + { + description: { default: "Import the selected image." }, + editor: "confirmation", + id: "import-image", + steps: [{ tool: "media.import-selected" }], + target: "image", + title: { default: "Import image" }, + }, + { + description: { default: "Import the selected video." }, + editor: "confirmation", + id: "import-video", + steps: [{ tool: "media.import-selected" }], + target: "video", + title: { default: "Import video" }, + }, + ], + }, + generation: { + models: [], + tools: [{ + acceptedInputs: ["reference_image", "reference_video"], + delivery: "return", + description: "Import one host-staged selection.", + id: "media.import-selected", + output: "text", + title: "Import selection", + }], + }, + }, + entry: "index.html", + runtime: { command: "selection-import-mcp", type: "mcp-stdio" }, + }) + + const parsed = parsePluginManifest(manifest) + expect(parsed.contributes.canvas.selectionActions.map(({ target }) => target)).toEqual([ + "image", + "video", + ]) + expect(parsed.contributes.agent).toBeUndefined() + + const inputBound = structuredClone(manifest) + inputBound.contributes.generation.tools[0].inputBinding = "direct-incoming" + expect(() => parsePluginManifest(inputBound)).toThrow("cannot reference an input-bound operation") + + const nonConfirmation = structuredClone(manifest) + nonConfirmation.contributes.canvas.selectionActions[0].editor = "crop-region" + expect(() => parsePluginManifest(nonConfirmation)).toThrow("requires a confirmation editor") + + const canvasDelivery = structuredClone(manifest) + canvasDelivery.contributes.generation.tools[0].delivery = "canvas" + expect(() => parsePluginManifest(canvasDelivery)).toThrow( + "image selection action requires a return-delivery operation", + ) + }) + test("keeps Plugin-owned Skill registry links valid for v6", () => { const plugin = v6Manifest(undefined, { contributes: { diff --git a/tooling/plugin-v7.test.js b/tooling/plugin-v7.test.js index 2bd7069..3972ba7 100644 --- a/tooling/plugin-v7.test.js +++ b/tooling/plugin-v7.test.js @@ -75,4 +75,46 @@ describe("convax.plugin/7 capability host contract", () => { }), ).toThrow("convax.plugin-capability/2") }) + + test("retains the v6 bounded return-selection contract without widening v4 or v5", () => { + const parsed = parsePluginManifest({ + capabilities: ["generation.execute"], + contributes: { + canvas: { + renderer: { create: true }, + selectionActions: [{ + description: { default: "Import one selected image." }, + editor: "confirmation", + id: "import-image", + steps: [{ tool: "media.import-selected" }], + target: "image", + title: { default: "Import image" }, + }], + }, + generation: { + models: [], + tools: [{ + acceptedInputs: ["reference_image"], + delivery: "return", + description: "Import one staged image.", + id: "media.import-selected", + output: "text", + title: "Import image", + }], + }, + }, + description: "Exercises the inherited bounded return-selection contract.", + entry: "index.html", + id: "return-selection", + name: "Return Selection", + runtime: { command: "return-selection-mcp", type: "mcp-stdio" }, + schema: "convax.plugin/7", + version: "1.0.0", + }) + + expect(parsed.contributes.canvas.selectionActions[0]).toMatchObject({ + editor: "confirmation", + target: "image", + }) + }) }) diff --git a/tooling/product-lock-output.test.js b/tooling/product-lock-output.test.js new file mode 100644 index 0000000..2e3fe15 --- /dev/null +++ b/tooling/product-lock-output.test.js @@ -0,0 +1,278 @@ +import { afterAll, describe, expect, test } from "bun:test" +import { createHash } from "node:crypto" +import { promises as fs } from "node:fs" +import os from "node:os" +import path from "node:path" +import { verifyProductLockInput } from "./verify-product-lock-input.mjs" + +const temporaryDirectories = [] +const repository = "https://github.com/microvoid/convax-plugins/releases/download" + +function sha256(bytes) { + return createHash("sha256").update(bytes).digest("hex") +} + +async function writeRelease(root, area, tag, name, bytes) { + const relativePath = `${area}/releases/${tag}/${name}` + await fs.mkdir(path.join(root, area, "releases", tag), { recursive: true }) + await fs.writeFile(path.join(root, relativePath), bytes) + return { + path: relativePath, + url: `${repository}/${tag}/${name}`, + size: bytes.length, + sha256: sha256(bytes), + } +} + +async function writeFixture() { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "convax-product-lock-output-")) + temporaryDirectories.push(root) + const revision = "a".repeat(64) + const metadataTag = `registry-v2-${revision}` + const pluginTag = "plugin-ffmpeg-tools-v0.3.1" + const skillTag = "skill-ffmpeg-canvas-v0.3.1" + const builtinTag = `builtin-${"b".repeat(64)}` + const plugin = await writeRelease(root, "catalog", pluginTag, "convax-plugin-ffmpeg-tools-0.3.1.zip", Buffer.from("plugin")) + const skill = await writeRelease(root, "catalog", skillTag, "convax-skill-ffmpeg-canvas-0.3.1.zip", Buffer.from("skill")) + const companion = await writeRelease(root, "catalog", pluginTag, "convax-companion-convax-ffmpeg-mcp-0.2.0-darwin-arm64", Buffer.from("companion")) + const descriptorValue = { + schema: "convax.marketplace/1", + id: "convax-official", + registry: { + v2: { url: "https://microvoid.github.io/convax-plugins/registry/v2/index.json" }, + v1: { url: "https://microvoid.github.io/convax-plugins/registry/v1/index.json" }, + }, + showcase: { + v2: { url: "https://microvoid.github.io/convax-plugins/showcase/v2/index.json" }, + }, + } + const registryValue = { + schema: "convax.registry/2", + marketplaceId: "convax-official", + sequence: 45, + revision, + packages: [ + { + kind: "plugin", + id: "ffmpeg-tools", + version: "0.3.1", + delivery: { kind: "artifact", url: plugin.url, size: plugin.size, sha256: plugin.sha256 }, + companions: [{ + command: "convax-ffmpeg-mcp", + version: "0.2.0", + targets: [{ + platform: "darwin", + arch: "arm64", + artifact: { url: companion.url, size: companion.size, sha256: companion.sha256 }, + }], + }], + manifest: { contributes: { skills: [{ name: "ffmpeg-canvas", path: "skills/ffmpeg-canvas" }] } }, + }, + { + kind: "skill", + id: "ffmpeg-canvas", + version: "0.3.1", + delivery: { kind: "artifact", url: skill.url, size: skill.size, sha256: skill.sha256 }, + }, + ], + } + const showcaseValue = { schema: "convax.showcase/2", revision, packages: [] } + const metadata = {} + for (const [name, value] of [ + ["marketplace.json", descriptorValue], + ["registry-v2.json", registryValue], + ["showcase-v2.json", showcaseValue], + ]) { + const bytes = Buffer.from(`${JSON.stringify(value)}\n`) + await fs.mkdir(path.join(root, "catalog"), { recursive: true }) + await fs.writeFile(path.join(root, "catalog", name), bytes) + metadata[name] = await writeRelease(root, "catalog", metadataTag, name, bytes) + } + const builtin = await writeRelease( + root, + "builtin", + builtinTag, + "convax-builtin-bundle.zip", + Buffer.from("builtin"), + ) + await fs.writeFile( + path.join(root, "builtin", "bundle.json"), + `${JSON.stringify({ + schema: "convax.builtin-bundle/1", + release: { id: "b".repeat(64) }, + members: [{ kind: "skill", id: "canvas-storyboard", version: "0.1.0" }], + })}\n`, + ) + const writePlan = async (area, artifacts) => { + const releases = new Map() + for (const artifact of artifacts) { + const { tag, name } = (() => { + const suffix = artifact.url.slice(`${repository}/`.length) + const separator = suffix.indexOf("/") + return { + tag: suffix.slice(0, separator), + name: suffix.slice(separator + 1), + } + })() + const release = releases.get(tag) ?? { tag, assets: [] } + release.assets.push({ + path: artifact.path.slice(`${area}/`.length), + name, + size: artifact.size, + sha256: artifact.sha256, + url: artifact.url, + }) + releases.set(tag, release) + } + await fs.writeFile( + path.join(root, area, "release-plan.json"), + `${JSON.stringify({ + schema: "convax.release-plan/1", + releases: [...releases.values()], + })}\n`, + ) + } + await writePlan("catalog", [ + plugin, + skill, + companion, + metadata["marketplace.json"], + metadata["registry-v2.json"], + metadata["showcase-v2.json"], + ]) + await writePlan("builtin", [builtin]) + await fs.writeFile( + path.join(root, "product-lock-input.json"), + `${JSON.stringify({ + schema: "convax.product-lock-input/1", + builtinBundle: { path: builtin.path, url: builtin.url }, + builtinManifestPath: "builtin/bundle.json", + builtinReservations: [{ kind: "skill", id: "canvas-storyboard" }], + official: { + descriptor: { path: metadata["marketplace.json"].path, url: metadata["marketplace.json"].url }, + registry: { path: metadata["registry-v2.json"].path, url: metadata["registry-v2.json"].url }, + revision, + showcase: { path: metadata["showcase-v2.json"].path, url: metadata["showcase-v2.json"].url }, + }, + packages: [{ + marketplaceId: "convax-official", + kind: "plugin", + id: "ffmpeg-tools", + version: "0.3.1", + setup: "explicit", + artifact: { path: plugin.path, url: plugin.url }, + ownedSkills: [{ path: skill.path, url: skill.url }], + companions: [{ + path: companion.path, + url: companion.url, + platform: "darwin", + arch: "arm64", + }], + }], + })}\n`, + ) + return { root } +} + +afterAll(async () => { + await Promise.all(temporaryDirectories.map((directory) => + fs.rm(directory, { recursive: true, force: true }))) +}) + +describe("Convax product lock input closure", () => { + test("closes the Builtin reservation, Official metadata, and only approved preinstall", async () => { + const fixture = await writeFixture() + await expect(verifyProductLockInput(path.join(fixture.root, "product-lock-input.json"))) + .resolves.toEqual({ + builtinReservations: 1, + preinstalledPackages: 1, + verifiedArtifacts: 7, + }) + }) + + test("rejects a source swap or widened preinstall policy", async () => { + const fixture = await writeFixture() + const lockPath = path.join(fixture.root, "product-lock-input.json") + const lock = JSON.parse(await fs.readFile(lockPath, "utf8")) + lock.packages[0].artifact.url = lock.packages[0].artifact.url.replace( + "plugin-ffmpeg-tools-v0.3.1", + "plugin-other-v0.3.1", + ) + await fs.writeFile(lockPath, `${JSON.stringify(lock)}\n`) + await expect(verifyProductLockInput(lockPath)) + .rejects.toThrow("preinstalled Plugin artifact differs from Registry v2") + + const clean = await writeFixture() + const cleanPath = path.join(clean.root, "product-lock-input.json") + const widened = JSON.parse(await fs.readFile(cleanPath, "utf8")) + widened.packages.push(widened.packages[0]) + await fs.writeFile(cleanPath, `${JSON.stringify(widened)}\n`) + await expect(verifyProductLockInput(cleanPath)) + .rejects.toThrow("exactly one ffmpeg-tools preinstall") + }) + + test("rejects metadata or Builtin paths that escape the composed output", async () => { + const fixture = await writeFixture() + const lockPath = path.join(fixture.root, "product-lock-input.json") + const lock = JSON.parse(await fs.readFile(lockPath, "utf8")) + lock.official.registry.path = "../registry-v2.json" + await fs.writeFile(lockPath, `${JSON.stringify(lock)}\n`) + await expect(verifyProductLockInput(lockPath)) + .rejects.toThrow("must stay below the product lock output") + }) + + test("rejects filesystem aliases and oversized immutable inputs before hashing", async () => { + const hardlinked = await writeFixture() + const descriptor = path.join( + hardlinked.root, + "catalog", + "releases", + `registry-v2-${"a".repeat(64)}`, + "marketplace.json", + ) + await fs.link(descriptor, path.join(hardlinked.root, "descriptor-hardlink.json")) + await expect(verifyProductLockInput(path.join(hardlinked.root, "product-lock-input.json"))) + .rejects.toThrow("single-link regular no-follow file") + + const symlinked = await writeFixture() + const pluginPath = path.join( + symlinked.root, + "catalog", + "releases", + "plugin-ffmpeg-tools-v0.3.1", + "convax-plugin-ffmpeg-tools-0.3.1.zip", + ) + const replacement = path.join(symlinked.root, "same-plugin-bytes.zip") + await fs.writeFile(replacement, "plugin") + await fs.unlink(pluginPath) + await fs.symlink(replacement, pluginPath) + await expect(verifyProductLockInput(path.join(symlinked.root, "product-lock-input.json"))) + .rejects.toThrow("single-link regular no-follow file") + + const oversized = await writeFixture() + const builtinPath = path.join( + oversized.root, + "builtin", + "releases", + `builtin-${"b".repeat(64)}`, + "convax-builtin-bundle.zip", + ) + await fs.truncate(builtinPath, 256 * 1024 * 1024 + 1) + await expect(verifyProductLockInput(path.join(oversized.root, "product-lock-input.json"))) + .rejects.toThrow("exceeds its maximum admitted size") + }) + + test("rejects a same-size in-place byte rewrite", async () => { + const fixture = await writeFixture() + const pluginPath = path.join( + fixture.root, + "catalog", + "releases", + "plugin-ffmpeg-tools-v0.3.1", + "convax-plugin-ffmpeg-tools-0.3.1.zip", + ) + await fs.writeFile(pluginPath, "PLUGIN") + await expect(verifyProductLockInput(path.join(fixture.root, "product-lock-input.json"))) + .rejects.toThrow("bytes differ from the immutable release-plan") + }) +}) diff --git a/tooling/publication-plan.mjs b/tooling/publication-plan.mjs new file mode 100644 index 0000000..32e50ad --- /dev/null +++ b/tooling/publication-plan.mjs @@ -0,0 +1,92 @@ +import { promises as fs } from "node:fs" +import path from "node:path" + +function parsePlan(value, label) { + if ( + !value || + value.schema !== "convax.release-plan/1" || + !Array.isArray(value.releases) + ) { + throw new Error(`${label} is not a release plan`) + } + const tags = new Set() + for (const release of value.releases) { + if ( + typeof release?.tag !== "string" || + !Array.isArray(release.assets) || + release.assets.length === 0 + ) { + throw new Error(`${label} contains an incomplete Release`) + } + if (tags.has(release.tag)) throw new Error(`${label} contains duplicate tag ${release.tag}`) + tags.add(release.tag) + for (const asset of release.assets) { + if ( + typeof asset?.path !== "string" || + !asset.path.startsWith(`releases/${release.tag}/`) || + asset.path.slice(`releases/${release.tag}/`.length).includes("/") + ) { + throw new Error(`${label} ${release.tag} contains an asset outside its exact directory`) + } + } + } + return value.releases +} + +export function composePublicationPlan({ builtin, catalog, selected }) { + if (!Array.isArray(selected)) throw new Error("selected version changes must be an array") + const selectedTags = new Set() + let publishesBuiltin = false + for (const entry of selected) { + if (typeof entry?.releaseTag !== "string") throw new Error("selected version change has no releaseTag") + if (selectedTags.has(entry.releaseTag)) throw new Error(`duplicate selected tag ${entry.releaseTag}`) + selectedTags.add(entry.releaseTag) + publishesBuiltin ||= entry.kind === "skill" && entry.id === "canvas-storyboard" + } + const catalogReleases = parsePlan(catalog, "Catalog release plan") + const metadata = catalogReleases.filter((release) => + /^registry-v2-[a-f0-9]{64}$/.test(release.tag)) + if (metadata.length !== 1) throw new Error("Catalog must contain exactly one Registry metadata Release") + const packageReleases = catalogReleases.filter((release) => release !== metadata[0]) + const packageTags = new Set(packageReleases.map((release) => release.tag)) + if ( + packageTags.size !== selectedTags.size || + [...selectedTags].some((tag) => !packageTags.has(tag)) + ) { + throw new Error("Catalog package Releases differ from selected version changes") + } + + const releases = catalogReleases.map((release) => ({ + directory: `catalog/releases/${release.tag}`, + tag: release.tag, + })) + if (publishesBuiltin) { + const builtinReleases = parsePlan(builtin, "Builtin release plan") + if (builtinReleases.length !== 1) throw new Error("Builtin must contain exactly one Release") + releases.push({ + directory: `builtin/releases/${builtinReleases[0].tag}`, + tag: builtinReleases[0].tag, + }) + } + releases.sort((left, right) => left.tag.localeCompare(right.tag, "en")) + return { schema: "convax.publication-plan/1", releases } +} + +async function main() { + const [selected, catalog, builtin] = await Promise.all([ + fs.readFile("dist/release-plan.json", "utf8").then(JSON.parse), + fs.readFile("dist/catalog/release-plan.json", "utf8").then(JSON.parse), + fs.readFile("dist/builtin/release-plan.json", "utf8").then(JSON.parse), + ]) + const plan = composePublicationPlan({ builtin, catalog, selected }) + const output = path.resolve("dist/publication-plan.json") + await fs.writeFile(output, `${JSON.stringify(plan, null, 2)}\n`) + console.log(`Prepared ${plan.releases.length} immutable Releases for publication.`) +} + +if (import.meta.main) { + await main().catch((error) => { + console.error(error instanceof Error ? error.message : String(error)) + process.exitCode = 1 + }) +} diff --git a/tooling/registry.test.js b/tooling/registry.test.js index 53392e8..b12e325 100644 --- a/tooling/registry.test.js +++ b/tooling/registry.test.js @@ -55,6 +55,7 @@ describe("source packages", () => { "plugin/xiaoyunque-generation", "skill/ad-idea", "skill/audiobook", + "skill/canvas-storyboard", "skill/chatcut", "skill/clip-export", "skill/ecommerce-image", @@ -125,7 +126,7 @@ describe("source packages", () => { type: "mcp-stdio", }, schema: "convax.plugin/6", - version: "2.0.0", + version: "2.1.1", }), ); expect(jianying.manifest.contributes.generation.tools).toEqual([ @@ -140,11 +141,30 @@ describe("source packages", () => { id: "media.export", inputBinding: "direct-incoming", }), + expect.objectContaining({ + acceptedInputs: ["reference_image", "reference_video"], + delivery: "return", + id: "media.import-selected", + }), + ]); + expect(jianying.manifest.contributes.canvas.selectionActions).toEqual([ + expect.objectContaining({ + editor: "confirmation", + id: "import-image-to-jianying", + steps: [{ tool: "media.import-selected" }], + target: "image", + }), + expect.objectContaining({ + editor: "confirmation", + id: "import-video-to-jianying", + steps: [{ tool: "media.import-selected" }], + target: "video", + }), ]); expect(jianying.metadata.companions).toEqual([ { command: "convax-jianying-editor-mcp", - version: "1.0.0", + version: "1.1.1", source: "packages/tools/jianying-editor-mcp", targets: [ { @@ -670,12 +690,17 @@ describe("source packages", () => { ); }); - test("publishes Pages without a whole-repository Release gate", async () => { + test("publishes the exact verified Kit output before releasing the publication lock", async () => { const workflow = await fs.readFile(path.join(root, ".github", "workflows", "pages.yml"), "utf8"); - expect(workflow).toContain("prepare-release-catalog.mjs"); - expect(workflow).toContain("--previous dist/production/index.json"); + expect(workflow).toContain("workflow_call:"); + expect(workflow).toContain("verify-marketplace-output.mjs dist/catalog"); + expect(workflow).toContain("path: dist/catalog/site"); + expect(workflow).toContain("dist/catalog/site/schemas"); + expect(workflow).not.toContain("cp dist/catalog/registry-v2.json"); + expect(workflow).not.toContain("cp dist/catalog/registry-v1.json"); + expect(workflow).not.toContain("cp dist/catalog/showcase-v2.json"); + expect(workflow).not.toContain("workflow_run:"); expect(workflow).not.toContain("check-release-coverage.mjs"); - expect(workflow).not.toContain("steps.coverage.outputs.ready"); }); test("builds the strict client Registry with only the latest stable version", async () => { diff --git a/tooling/run-workspace-script.mjs b/tooling/run-workspace-script.mjs index a8a481b..93efcc5 100644 --- a/tooling/run-workspace-script.mjs +++ b/tooling/run-workspace-script.mjs @@ -4,7 +4,7 @@ import path from "node:path" import { isObject, readJson, root } from "./lib.mjs" -const collections = ["plugins", "skills", "tools"] +const collections = ["plugins", "skills", "mcp-servers", "tools"] async function discoverWorkspaces(workspaceRoot = root) { const workspaces = [] @@ -15,7 +15,7 @@ async function discoverWorkspaces(workspaceRoot = root) { throw cause }) for (const entry of entries) { - if (!entry.isDirectory()) continue + if (!entry.isDirectory() || entry.name.startsWith(".")) continue const workspaceDirectory = path.join(directory, entry.name) const packageJson = await readJson( path.join(workspaceDirectory, "package.json"), @@ -73,7 +73,7 @@ export async function runWorkspaceScript(script, requestedCollections = collecti if (import.meta.main) { const [script, ...requestedCollections] = process.argv.slice(2) - if (!script) throw new Error("Usage: bun tooling/run-workspace-script.mjs