diff --git a/.cargo/audit.toml b/.cargo/audit.toml index 48fd8895..bafe7519 100644 --- a/.cargo/audit.toml +++ b/.cargo/audit.toml @@ -1,8 +1,6 @@ [advisories] ignore = [ "RUSTSEC-2025-0140", # gix-date UTF-8 contract issue dependency of cargo-generate - "RUSTSEC-2026-0001", # rkyv undefined behavior on OOM dependency of byte-unit - "RUSTSEC-2026-0037", # quinn-proto DoS - transitive via reqwest/ic-agent, quinn feature not used # Unmaintained crates (transitive dependencies) "RUSTSEC-2021-0127", # serde_cbor - dependency of ic-agent/ic-transport-types diff --git a/.claude/skills/release/task6-docs.md b/.claude/skills/release/task6-docs.md index 08ebb7f8..018d1ede 100644 --- a/.claude/skills/release/task6-docs.md +++ b/.claude/skills/release/task6-docs.md @@ -2,7 +2,9 @@ *Skip if `$ARGUMENTS` is a beta release. Requires Task 2. Runs concurrently with Task 3.* -The tag push triggers a docs deployment workflow that builds and publishes the versioned docs to `/icp-cli/X.Y/`. The `versions.json` PR must not be merged until that deployment succeeds, otherwise the root redirect will point to a path that does not exist yet. +The tag push triggers a docs deployment workflow that builds and publishes the versioned docs to `/X.Y/` on the `docs-deployment` branch (served at `https://cli.internetcomputer.org/X.Y/`). The `versions.json` PR must not be merged until that deployment succeeds, otherwise the root redirect will point to a path that does not exist yet. + +Once the `versions.json` PR merges to `main`, the `publish-root-files` CI job runs automatically and copies `og-image.png`, `llms.txt`, `llms-full.txt`, and `feed.xml` from the new version's folder to the deployment root — no manual step needed. **1. Wait for the docs deployment triggered by the tag** ```bash diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 72162989..b31fa863 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -80,7 +80,7 @@ If you want to submit a pull request to fix an issue or add a feature, here's a ## Contributing to Documentation -The documentation lives in the `docs/` directory and is deployed to https://dfinity.github.io/icp-cli/. +The documentation lives in the `docs/` directory and is deployed to https://cli.internetcomputer.org. ### Documentation Structure @@ -97,7 +97,7 @@ The documentation site uses [Astro](https://astro.build/) with [Starlight](https 1. **Source files** (`docs/`) are Markdown with minimal YAML frontmatter (title + description) 2. **Starlight** reads directly from `docs/` via the glob content loader 3. **Rehype plugin** (`docs-site/plugins/rehype-rewrite-links.mjs`) rewrites `.md` links at build time for Starlight's clean URLs -4. **GitHub Actions** automatically deploys to GitHub Pages on push to main +4. **GitHub Actions** automatically builds and deploys to an IC asset canister on push to main This architecture keeps source docs GitHub-friendly (`.md` links work on GitHub) while producing clean URLs on the documentation site. diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 2e66f83d..5070eafe 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -38,7 +38,7 @@ jobs: issues: write steps: - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup image (Linux) run: ./.github/scripts/provision-linux-build.sh - - uses: actions-rust-lang/audit@410bbe6de17ca06c0a60070cca18c88b485ca5a1 # v1.2.6 + - uses: actions-rust-lang/audit@72c09e02f132669d52284a3323acdb503cfc1a24 # v1.2.7 diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 4dc0bc66..c6239281 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -14,12 +14,13 @@ jobs: permissions: pull-requests: read steps: - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 id: filter with: - # 'src' is true when any changed file matches a positive pattern - # and does not match a negative pattern (! prefix). + # With 'every', a changed file is matched only when it satisfies + # ALL rules: the positive pattern AND every negated pattern. + predicate-quantifier: 'every' filters: | src: - '**' @@ -34,12 +35,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup image (Linux) run: ./.github/scripts/provision-linux-build.sh - - uses: actions-rust-lang/setup-rust-toolchain@a0b538fa0b742a6aa35d6e2c169b4bd06d225a98 # v1.15.3 + - uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1.15.4 with: cache-shared-key: ${{ runner.os }}-checks @@ -55,12 +56,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup image (Linux) run: ./.github/scripts/provision-linux-build.sh - - uses: actions-rust-lang/setup-rust-toolchain@a0b538fa0b742a6aa35d6e2c169b4bd06d225a98 # v1.15.3 + - uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1.15.4 with: cache-shared-key: ${{ runner.os }}-checks @@ -76,12 +77,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup image (Linux) run: ./.github/scripts/provision-linux-build.sh - - uses: actions-rust-lang/setup-rust-toolchain@a0b538fa0b742a6aa35d6e2c169b4bd06d225a98 # v1.15.3 + - uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1.15.4 with: cache-shared-key: ${{ runner.os }}-checks @@ -95,7 +96,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install taplo run: | @@ -123,12 +124,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup image (Linux) run: ./.github/scripts/provision-linux-build.sh - - uses: actions-rust-lang/setup-rust-toolchain@a0b538fa0b742a6aa35d6e2c169b4bd06d225a98 # v1.15.3 + - uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1.15.4 with: cache-shared-key: ${{ runner.os }}-checks @@ -158,12 +159,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup image (Linux) run: ./.github/scripts/provision-linux-build.sh - - uses: actions-rust-lang/setup-rust-toolchain@a0b538fa0b742a6aa35d6e2c169b4bd06d225a98 # v1.15.3 + - uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1.15.4 with: cache-shared-key: ${{ runner.os }}-checks diff --git a/.github/workflows/deny.yml b/.github/workflows/deny.yml index 51e5f294..cfd4b46a 100644 --- a/.github/workflows/deny.yml +++ b/.github/workflows/deny.yml @@ -29,10 +29,10 @@ jobs: name: license-check:required runs-on: ubuntu-latest steps: - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup image (Linux) run: ./.github/scripts/provision-linux-build.sh - run: rm rust-toolchain.toml - - uses: EmbarkStudios/cargo-deny-action@f2ba7abc2abebaf185c833c3961145a3c275caad # v2.0.13 + - uses: EmbarkStudios/cargo-deny-action@3fd3802e88374d3fe9159b834c7714ec57d6c979 # v2.0.15 with: command: check bans licenses sources # skip advisories, which are handled by audit.yml diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index c7d35011..fabf6551 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -16,7 +16,7 @@ jobs: contents: read steps: - name: Checkout docs-deployment branch - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: docs-deployment diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index b0cd820a..149dee7f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -4,9 +4,11 @@ on: push: tags: - 'v*' + - '!v*-*' # exclude pre-release tags (e.g. v0.2.0-beta.0) branches: - main - 'docs/v*' + - '!docs/v*-*' # exclude pre-release doc branches paths: - 'docs/**' - 'docs-site/**' @@ -36,10 +38,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 - name: Setup Node - uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v4.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: '22' cache: 'npm' @@ -58,15 +62,15 @@ jobs: # Publish root index, versions list, and IC config files - only runs on main branch publish-root-files: - if: github.ref == 'refs/heads/main' && github.event_name == 'push' + if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') needs: build runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Node - uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v4.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: '22' @@ -113,12 +117,44 @@ jobs: echo "LATEST_VERSION=${LATEST_VERSION}" >> $GITHUB_ENV - - name: Copy llms.txt from latest version on docs-deployment branch + # Generate robots.txt — allow only the latest version, block old versions. + # /main/ is disallowed unless it IS the latest version (no releases yet fallback). + { + echo "User-agent: *" + echo "Allow: /${LATEST_VERSION}/" + for version in $(jq -r '.versions[].version' docs-site/versions.json); do + if [[ "$version" != "$LATEST_VERSION" ]]; then + echo "Disallow: /${version}/" + fi + done + if [[ "$LATEST_VERSION" != "main" ]]; then echo "Disallow: /main/"; fi + echo "" + echo "Sitemap: ${PUBLIC_SITE}/sitemap.xml" + echo "" + echo "# LLM and AI agent discovery (llmstxt.org)" + echo "# ${PUBLIC_SITE}/llms.txt" + echo "# ${PUBLIC_SITE}/llms-full.txt" + } > root/robots.txt + echo "✅ Generated robots.txt (latest: ${LATEST_VERSION})" + + # Generate root sitemap.xml pointing directly to the latest version's sitemap. + # Points to sitemap-0.xml (not sitemap-index.xml) to stay spec-compliant: + # a sitemapindex must reference sitemaps, not other sitemapindex files. + { + echo '' + echo '' + echo ' ' + echo " ${PUBLIC_SITE}/${LATEST_VERSION}/sitemap-0.xml" + echo ' ' + echo '' + } > root/sitemap.xml + echo "✅ Generated sitemap.xml → /${LATEST_VERSION}/sitemap-0.xml" + + - name: Copy files from latest version on docs-deployment branch run: | - # Fetch the llms.txt from the latest version's folder on docs-deployment. - # This file was deployed by publish-versioned-docs and is always in sync - # with the versioned .md endpoints it links to. git fetch origin docs-deployment --depth=1 + + # llms.txt — agent discovery index (versioned copy is always in sync with .md endpoints) if git show "origin/docs-deployment:${LATEST_VERSION}/llms.txt" > root/llms.txt 2>/dev/null; then echo "✅ Copied llms.txt from /${LATEST_VERSION}/llms.txt to root" else @@ -126,6 +162,30 @@ jobs: rm -f root/llms.txt fi + # llms-full.txt — full content dump for bulk ingestion / RAG pipelines + if git show "origin/docs-deployment:${LATEST_VERSION}/llms-full.txt" > root/llms-full.txt 2>/dev/null; then + echo "✅ Copied llms-full.txt from /${LATEST_VERSION}/llms-full.txt to root" + else + echo "⚠️ No llms-full.txt found at /${LATEST_VERSION}/llms-full.txt on docs-deployment — skipping" + rm -f root/llms-full.txt + fi + + # og-image.png — social sharing preview image + if git show "origin/docs-deployment:${LATEST_VERSION}/og-image.png" > root/og-image.png 2>/dev/null; then + echo "✅ Copied og-image.png from /${LATEST_VERSION}/og-image.png to root" + else + echo "⚠️ No og-image.png found at /${LATEST_VERSION}/og-image.png on docs-deployment — skipping" + rm -f root/og-image.png + fi + + # feed.xml — RSS feed + if git show "origin/docs-deployment:${LATEST_VERSION}/feed.xml" > root/feed.xml 2>/dev/null; then + echo "✅ Copied feed.xml from /${LATEST_VERSION}/feed.xml to root" + else + echo "⚠️ No feed.xml found at /${LATEST_VERSION}/feed.xml on docs-deployment — skipping" + rm -f root/feed.xml + fi + - name: Prepend version navigation to root llms.txt if: hashFiles('root/llms.txt') != '' run: | @@ -156,15 +216,17 @@ jobs: # Publish main branch docs for preview (always available at /main/) publish-main-docs: - if: github.ref == 'refs/heads/main' && github.event_name == 'push' + if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') needs: build runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 - name: Setup Node - uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v4.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: '22' cache: 'npm' @@ -200,10 +262,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 - name: Setup Node - uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v4.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: '22' cache: 'npm' diff --git a/.github/workflows/release-npm.yml b/.github/workflows/release-npm.yml index ff81265b..728577eb 100644 --- a/.github/workflows/release-npm.yml +++ b/.github/workflows/release-npm.yml @@ -25,12 +25,12 @@ jobs: contents: read id-token: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ inputs.version }} - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: '24' registry-url: 'https://registry.npmjs.org' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3c59af53..d9aa406b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,7 +56,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: persist-credentials: false submodules: recursive @@ -64,9 +64,9 @@ jobs: # we specify bash to get pipefail; it guards against the `curl` command # failing. otherwise `sh` won't catch that `curl` returned non-0 shell: bash - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.30.3/cargo-dist-installer.sh | sh" + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.31.0/cargo-dist-installer.sh | sh" - name: Cache dist - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: cargo-dist-cache path: ~/.cargo/bin/dist @@ -82,7 +82,7 @@ jobs: cat plan-dist-manifest.json echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" - name: "Upload dist-manifest.json" - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: artifacts-plan-dist-manifest path: plan-dist-manifest.json @@ -116,7 +116,7 @@ jobs: - name: enable windows longpaths run: | git config --global core.longpaths true - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: persist-credentials: false submodules: recursive @@ -131,7 +131,7 @@ jobs: run: ${{ matrix.install_dist.run }} # Get the dist-manifest - name: Fetch local artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: pattern: artifacts-* path: target/distrib/ @@ -158,7 +158,7 @@ jobs: cp dist-manifest.json "$BUILD_MANIFEST_NAME" - name: "Upload artifacts" - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: artifacts-build-local-${{ join(matrix.targets, '_') }} path: | @@ -175,19 +175,19 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: persist-credentials: false submodules: recursive - name: Install cached dist - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: name: cargo-dist-cache path: ~/.cargo/bin/ - run: chmod +x ~/.cargo/bin/dist # Get all the local artifacts for the global tasks to use (for e.g. checksums) - name: Fetch local artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: pattern: artifacts-* path: target/distrib/ @@ -205,7 +205,7 @@ jobs: cp dist-manifest.json "$BUILD_MANIFEST_NAME" - name: "Upload artifacts" - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: artifacts-build-global path: | @@ -225,19 +225,19 @@ jobs: outputs: val: ${{ steps.host.outputs.manifest }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: persist-credentials: false submodules: recursive - name: Install cached dist - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: name: cargo-dist-cache path: ~/.cargo/bin/ - run: chmod +x ~/.cargo/bin/dist # Fetch artifacts from scratch-storage - name: Fetch artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: pattern: artifacts-* path: target/distrib/ @@ -250,14 +250,14 @@ jobs: cat dist-manifest.json echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" - name: "Upload dist-manifest.json" - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: # Overwrite the previous copy name: artifacts-dist-manifest path: dist-manifest.json # Create a GitHub Release while uploading all files to it - name: "Download GitHub Artifacts" - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: pattern: artifacts-* path: artifacts @@ -290,7 +290,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: persist-credentials: false submodules: recursive diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9bbc9771..f7c3c7c7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,12 +28,13 @@ jobs: permissions: pull-requests: read steps: - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 id: filter with: - # 'src' is true when any changed file matches a positive pattern - # and does not match a negative pattern (! prefix). + # With 'every', a changed file is matched only when it satisfies + # ALL rules: the positive pattern AND every negated pattern. + predicate-quantifier: 'every' filters: | src: - '**' @@ -49,7 +50,7 @@ jobs: outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} steps: - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - id: set-matrix run: echo "matrix=$(python3 .github/scripts/test-matrix.py)" >> $GITHUB_OUTPUT @@ -66,13 +67,13 @@ jobs: os: [ubuntu-22.04, macos-15, windows-2025] steps: - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup image (Linux) if: ${{ contains(matrix.os, 'ubuntu') }} run: ./.github/scripts/provision-linux-build.sh - - uses: actions-rust-lang/setup-rust-toolchain@a0b538fa0b742a6aa35d6e2c169b4bd06d225a98 # v1.15.3 + - uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1.15.4 with: cache-shared-key: ${{ runner.os }}-test @@ -95,9 +96,9 @@ jobs: matrix: ${{fromJson(needs.discover.outputs.matrix)}} steps: - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions-rust-lang/setup-rust-toolchain@a0b538fa0b742a6aa35d6e2c169b4bd06d225a98 # v1.15.3 + - uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1.15.4 with: cache-shared-key: ${{ runner.os }}-test @@ -129,7 +130,7 @@ jobs: if: ${{ contains(matrix.os, 'ubuntu') }} - name: Install mops - uses: dfinity/setup-mops@v1 + uses: dfinity/setup-mops@3e94e453352269b34137b5ce49f09a8df81bed7d # v1.4.1 - name: Verify mops installation run: | @@ -145,7 +146,7 @@ jobs: ic-wasm --version - name: Install wasm-tools - uses: taiki-e/install-action@3ae7038495f79cd771208b38319ff728a8b24538 # v2.67.3 + uses: taiki-e/install-action@e9e8e031bcd90cdbe8ac6bb1d376f8596e587fbf # v2.70.2 with: tool: wasm-tools diff --git a/.github/workflows/validate-examples.yml b/.github/workflows/validate-examples.yml index b622e33b..1aa4a359 100644 --- a/.github/workflows/validate-examples.yml +++ b/.github/workflows/validate-examples.yml @@ -20,12 +20,13 @@ jobs: permissions: pull-requests: read steps: - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 id: filter with: - # 'src' is true when any changed file matches a positive pattern - # and does not match a negative pattern (! prefix). + # With 'every', a changed file is matched only when it satisfies + # ALL rules: the positive pattern AND every negated pattern. + predicate-quantifier: 'every' filters: | src: - '**' @@ -45,13 +46,13 @@ jobs: os: [ubuntu-latest] steps: - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup image (Linux) if: ${{ contains(matrix.os, 'ubuntu') }} run: ./.github/scripts/provision-linux-build.sh - - uses: actions-rust-lang/setup-rust-toolchain@a0b538fa0b742a6aa35d6e2c169b4bd06d225a98 # v1.15.3 + - uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1.15.4 with: cache-shared-key: ${{ runner.os }}-validate diff --git a/.gitignore b/.gitignore index eeb96dad..172c621c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,13 +2,11 @@ .DS_Store .cursor -# Local (directories) -.icp +# icp-cli internal cache directory +.icp/cache -# This directory contains canister ID mappings which are encouraged -# to be included in source control of icp canister projects. -# They are ignored here because we are developing icp-cli itself. -.icpdata +# icp-cli state in examples (not source-controlled in this repo) +examples/*/.icp/ # often-used binaries pocket-ic diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f03bf92..c1afab6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Unreleased +# v0.2.3 + +* feat: Add `--proxy` to `icp canister` subcommands and `icp deploy` to route management canister calls through a proxy canister +* feat: Add `--args`, `--args-file`, and `--args-format` flags to `icp deploy` to pass install arguments at the command line, overriding `init_args` in the manifest + +# v0.2.2 + +Important: A network launcher more recent than v12.0.0-83c3f95e8c4ce28e02493df83df5f84a166451c0 is +required to use internet identity. + +* feat: Many more commands support `--json` and `--quiet`. +* feat: When a local network is started internet identity is available at id.ai.localhost +* fix: Network would fail to start if a stale descriptor was present + # v0.2.1 * feat: icp-cli will now inform you if a new version is released. This can be disabled with `icp settings update-check` diff --git a/Cargo.lock b/Cargo.lock index 7688665d..ac6269a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,7 +91,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", - "anstyle-parse", + "anstyle-parse 0.2.7", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse 1.0.0", "anstyle-query", "anstyle-wincon", "colorchoice", @@ -101,9 +116,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" @@ -114,6 +129,15 @@ dependencies = [ "utf8parse", ] +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + [[package]] name = "anstyle-query" version = "1.1.5" @@ -136,9 +160,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "anymap2" @@ -163,9 +187,9 @@ checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" [[package]] name = "arc-swap" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d03449bb8ca2cc2ef70869af31463d1ae5ccc8fa3e334b307203fbf815207e" +checksum = "a07d1f37ff60921c83bdfc7407723bdefe89b44b98a9b772f225c8f9d67141a6" dependencies = [ "rustversion", ] @@ -202,9 +226,9 @@ dependencies = [ [[package]] name = "assert_cmd" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c5bcfa8749ac45dd12cb11055aeeb6b27a3895560d60d71e3c23bf979e60514" +checksum = "9a686bbee5efb88a82df0621b236e74d925f470e5445d3220a5648b892ec99c9" dependencies = [ "anstyle", "bstr", @@ -272,7 +296,7 @@ dependencies = [ "async-trait", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "tokio", ] @@ -344,7 +368,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -390,7 +414,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -427,9 +451,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.16.1" +version = "1.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" +checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" dependencies = [ "aws-lc-sys", "zeroize", @@ -437,9 +461,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.38.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" +checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a" dependencies = [ "cc", "cmake", @@ -644,9 +668,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "bitvec" @@ -711,9 +735,9 @@ dependencies = [ [[package]] name = "bollard" -version = "0.20.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "227aa051deec8d16bd9c34605e7aaf153f240e35483dd42f6f78903847934738" +checksum = "ee04c4c84f1f811b017f2fbb7dd8815c976e7ca98593de9c1e2afad0f636bff4" dependencies = [ "base64", "bollard-stubs", @@ -760,25 +784,26 @@ checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" [[package]] name = "borsh" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" dependencies = [ "borsh-derive", + "bytes", "cfg_aliases", ] [[package]] name = "borsh-derive" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" dependencies = [ "once_cell", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -824,9 +849,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byte-unit" @@ -916,7 +941,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -946,9 +971,9 @@ dependencies = [ [[package]] name = "candid" -version = "0.10.20" +version = "0.10.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8037a01ec09d6c06883a38bad4f47b8d06158ad360b841e0ae5707c9884dfaf6" +checksum = "846adba6d1b4a00eeb8d4a0e4b88dfab570c25ad150cd52e51dfdc4f9d0a66cd" dependencies = [ "anyhow", "binread", @@ -969,14 +994,14 @@ dependencies = [ [[package]] name = "candid_derive" -version = "0.10.20" +version = "0.10.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb45f4d5eff3805598ee633dd80f8afb306c023249d34b5b7dfdc2080ea1df2e" +checksum = "4c367110b158be2edc335a4ad70f3467fa588d5c5675e149ef38638a8e281fc4" dependencies = [ "lazy_static", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1015,7 +1040,7 @@ dependencies = [ "auth-git2", "cargo-util-schemas", "clap", - "console 0.16.2", + "console 0.16.3", "dialoguer 0.12.0", "env_logger", "fs-err", @@ -1043,7 +1068,7 @@ dependencies = [ "tempfile", "thiserror 2.0.18", "time", - "toml 0.9.11+spec-1.1.0", + "toml 0.9.12+spec-1.1.0", "walkdir", ] @@ -1058,7 +1083,7 @@ dependencies = [ "serde-untagged", "serde-value", "thiserror 2.0.18", - "toml 0.9.11+spec-1.1.0", + "toml 0.9.12+spec-1.1.0", "unicode-xid", "url", ] @@ -1074,9 +1099,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.55" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "jobserver", @@ -1125,9 +1150,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.56" +version = "4.5.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75ca66430e33a14957acc24c5077b503e7d374151b2b4b3a10c83b4ceb4be0e" +checksum = "52fa72306bb30daf11bc97773431628e5b4916e97aaa74b7d3f625d4d495da02" dependencies = [ "clap_builder", "clap_derive", @@ -1144,11 +1169,11 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.56" +version = "4.5.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793207c7fa6300a0608d1080b858e5fdbe713cdc1c8db9fb17777d8a13e63df0" +checksum = "2071365c5c56eae7d77414029dde2f4f4ba151cf68d5a3261c9a40de428ace93" dependencies = [ - "anstream", + "anstream 1.0.0", "anstyle", "clap_lex", "strsim", @@ -1157,21 +1182,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.5.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "dec5be1eea072311774b7b84ded287adbd9f293f9d23456817605c6042f4f5e0" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "clap_lex" -version = "0.7.7" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cmake" @@ -1194,9 +1219,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "combine" @@ -1232,13 +1257,12 @@ dependencies = [ [[package]] name = "console" -version = "0.16.2" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" dependencies = [ "encode_unicode", "libc", - "once_cell", "unicode-width 0.2.2", "windows-sys 0.61.2", ] @@ -1399,7 +1423,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff765b99fc49f3116c9a908484486a2b92fd73c48da45c3a69716471c6cc56c6" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cryptoki-sys", "libloading 0.8.9", "log", @@ -1417,12 +1441,12 @@ dependencies = [ [[package]] name = "ctrlc" -version = "3.5.1" +version = "3.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73736a89c4aff73035ba2ed2e565061954da00d4970fc9ac25dcc85a2a20d790" +checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" dependencies = [ "dispatch2", - "nix 0.30.1", + "nix 0.31.2", "windows-sys 0.61.2", ] @@ -1452,7 +1476,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1498,7 +1522,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1509,7 +1533,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1560,9 +1584,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", ] @@ -1596,7 +1620,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1606,7 +1630,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1627,7 +1651,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25f104b501bf2364e78d0d3974cbc774f738f5865306ed128e1e0d7499c0ad96" dependencies = [ - "console 0.16.2", + "console 0.16.3", "shell-words", "tempfile", "zeroize", @@ -1713,11 +1737,11 @@ dependencies = [ [[package]] name = "dispatch2" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2", "libc", "objc2", @@ -1731,7 +1755,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1839,9 +1863,9 @@ dependencies = [ [[package]] name = "ena" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5" +checksum = "eabffdaee24bd1bf95c5ef7cec31260444317e72ea56c4c91750e8b7ee58d5f1" dependencies = [ "log", ] @@ -1885,14 +1909,14 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "env_filter" -version = "0.1.4" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" dependencies = [ "log", "regex", @@ -1900,11 +1924,11 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" dependencies = [ - "anstream", + "anstream 0.6.21", "anstyle", "env_filter", "jiff", @@ -1928,9 +1952,9 @@ dependencies = [ [[package]] name = "erased-serde" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" dependencies = [ "serde", "serde_core", @@ -2053,9 +2077,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -2190,9 +2214,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -2205,9 +2229,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -2215,15 +2239,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -2243,9 +2267,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-lite" @@ -2262,32 +2286,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -2297,7 +2321,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -2359,7 +2382,7 @@ version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "libc", "libgit2-sys", "log", @@ -2379,7 +2402,7 @@ dependencies = [ "gix-utils", "itoa", "thiserror 2.0.18", - "winnow", + "winnow 0.7.15", ] [[package]] @@ -2400,7 +2423,7 @@ dependencies = [ "smallvec", "thiserror 2.0.18", "unicode-bom", - "winnow", + "winnow 0.7.15", ] [[package]] @@ -2409,7 +2432,7 @@ version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c489abb061c74b0c3ad790e24a606ef968cebab48ec673d6a891ece7d5aef64" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "bstr", "gix-path", "libc", @@ -2463,7 +2486,7 @@ version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b947db8366823e7a750c254f6bb29e27e17f27e457bf336ba79b32423db62cd5" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "bstr", "gix-features", "gix-path", @@ -2521,7 +2544,7 @@ dependencies = [ "itoa", "smallvec", "thiserror 2.0.18", - "winnow", + "winnow 0.7.15", ] [[package]] @@ -2554,7 +2577,7 @@ dependencies = [ "gix-validate", "memmap2", "thiserror 2.0.18", - "winnow", + "winnow 0.7.15", ] [[package]] @@ -2563,7 +2586,7 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea9962ed6d9114f7f100efe038752f41283c225bb507a2888903ac593dffa6be" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "gix-path", "libc", "windows-sys 0.61.2", @@ -2584,9 +2607,9 @@ dependencies = [ [[package]] name = "gix-trace" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e42a4c2583357721ba2d887916e78df504980f22f1182df06997ce197b89504" +checksum = "f69a13643b8437d4ca6845e08143e847a36ca82903eed13303475d0ae8b162e0" [[package]] name = "gix-utils" @@ -2922,14 +2945,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64", "bytes", "futures-channel", - "futures-core", "futures-util", "http", "http-body", @@ -2963,9 +2985,9 @@ dependencies = [ [[package]] name = "ic-agent" -version = "0.46.2" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "223e34e74ee20df849226a45e14f6b47f4d53a97f34d571e7b6111728c965489" +checksum = "087c953695a2581a1e58a23a88e16a19e5eb2638ecefeea0bde22f23d8896786" dependencies = [ "arc-swap", "async-channel 2.5.0", @@ -2994,7 +3016,7 @@ dependencies = [ "p256", "pem", "pkcs8", - "rand 0.10.0", + "rand 0.10.1", "rangemap", "reqwest", "sec1", @@ -3013,9 +3035,9 @@ dependencies = [ [[package]] name = "ic-asset" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "962a5f1992104aa7aec190ff6debbb3cce93a4cf73772c0e682b752ad7eca1ec" +checksum = "87c4d4ae428d41c7f5d9cccb319bf660c610802036e9ee3ac410ef2322999ba7" dependencies = [ "backoff", "brotli", @@ -3083,14 +3105,14 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "ic-certification" -version = "3.0.3" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb40d73f9f8273dc6569a68859003bbd467c9dc6d53c6fd7d174742f857209d" +checksum = "7c11273a40f8d67926ee423b0bd21381ae8419db809b42f33c5cb3319549b40f" dependencies = [ "hex", "serde", @@ -3128,9 +3150,9 @@ dependencies = [ [[package]] name = "ic-identity-hsm" -version = "0.46.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6753cf0b4b1c8abf9c900bd2dc233635a10f800fb12f27ffe39212d8d2458c7" +checksum = "979ad4a529f16a1d2fea2904c0a8dd9500668b02f9fcb8ebcb7694621bd9f369" dependencies = [ "hex", "ic-agent", @@ -3188,9 +3210,9 @@ dependencies = [ [[package]] name = "ic-transport-types" -version = "0.46.2" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26448ff3dd1dc1afbfdd008153b2fde7ab117296e866cd7642c4a0cb9c88e421" +checksum = "3a91a2dc71282291a7c26ec7c23e57335fc4a562a6b0b571ede0044790a68a3f" dependencies = [ "candid", "hex", @@ -3206,9 +3228,9 @@ dependencies = [ [[package]] name = "ic-utils" -version = "0.46.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99534a857fd9999b9adfde82b2623bb9925997dc1813ec315bd3b4085911104a" +checksum = "e4dac90c041fee47ed98d9f2d1bb3455fb79c43bc4730af3c91f29aec3822e52" dependencies = [ "async-trait", "candid", @@ -3277,7 +3299,7 @@ dependencies = [ [[package]] name = "icp" -version = "0.2.1" +version = "0.2.3" dependencies = [ "async-dropper", "async-trait", @@ -3320,7 +3342,7 @@ dependencies = [ "p256", "pem", "pkcs8", - "rand 0.10.0", + "rand 0.10.1", "reqwest", "schemars", "scrypt", @@ -3349,16 +3371,17 @@ dependencies = [ [[package]] name = "icp-canister-interfaces" -version = "0.2.1" +version = "0.2.3" dependencies = [ "bigdecimal", "candid", + "ic-management-canister-types 0.7.1", "serde", ] [[package]] name = "icp-cli" -version = "0.2.1" +version = "0.2.3" dependencies = [ "anstyle", "anyhow", @@ -3405,7 +3428,7 @@ dependencies = [ "phf", "pkcs8", "predicates", - "rand 0.10.0", + "rand 0.10.1", "regex", "reqwest", "sec1", @@ -3445,9 +3468,9 @@ dependencies = [ [[package]] name = "icrc-ledger-types" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aafb78e620b2cc2b000cd745c0504dfb23a828acc3dd6f1baef208cd6c471e32" +checksum = "263acc94fa47008ad00f76b8d3694efe7f8361870c9ec92cddcc588b571518b7" dependencies = [ "base32", "candid", @@ -3455,7 +3478,6 @@ dependencies = [ "hex", "ic-stable-structures", "icrc-cbor", - "itertools 0.12.1", "minicbor", "num-bigint 0.4.6", "num-traits", @@ -3623,11 +3645,11 @@ dependencies = [ [[package]] name = "indicatif" -version = "0.18.3" +version = "0.18.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9375e112e4b463ec1b1c6c011953545c65a30164fbab5b581df32b3abf0dcb88" +checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" dependencies = [ - "console 0.16.2", + "console 0.16.3", "portable-atomic", "unicode-width 0.2.2", "unit-prefix", @@ -3645,11 +3667,11 @@ dependencies = [ [[package]] name = "inotify" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" +checksum = "bd5b3eaf1a28b758ac0faa5a4254e8ab2705605496f1b1f3fbbc3988ad73d199" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "inotify-sys", "libc", ] @@ -3684,9 +3706,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" @@ -3722,15 +3744,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.14.0" @@ -3742,15 +3755,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.18" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -3763,20 +3776,20 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.18" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "jiff-tzdb" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68971ebff725b9e2ca27a601c5eb38a4c5d64422c4cbab0c535f248087eda5c2" +checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076" [[package]] name = "jiff-tzdb-platform" @@ -3796,7 +3809,7 @@ dependencies = [ "cesu8", "cfg-if", "combine", - "jni-sys", + "jni-sys 0.3.1", "log", "thiserror 1.0.69", "walkdir", @@ -3805,9 +3818,31 @@ dependencies = [ [[package]] name = "jni-sys" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] [[package]] name = "jobserver" @@ -3821,9 +3856,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -3903,7 +3938,7 @@ dependencies = [ "log", "secret-service", "security-framework 2.11.1", - "security-framework 3.5.1", + "security-framework 3.7.0", "windows-sys 0.60.2", "zeroize", ] @@ -4044,13 +4079,14 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "libc", - "redox_syscall 0.7.0", + "plain", + "redox_syscall 0.7.3", ] [[package]] @@ -4069,9 +4105,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.23" +version = "1.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" +checksum = "d52f4c29e2a68ac30c9087e1b772dc9f44a2b66ed44edf2266cf2be9b03dafc1" dependencies = [ "cc", "libc", @@ -4081,9 +4117,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "liquid" @@ -4122,7 +4158,7 @@ checksum = "de66c928222984aea59fcaed8ba627f388aaac3c1f57dcb05cc25495ef8faefe" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -4181,7 +4217,7 @@ dependencies = [ "proc-macro2", "quote", "regex-syntax", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -4201,15 +4237,15 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memmap2" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" dependencies = [ "libc", ] @@ -4254,7 +4290,7 @@ checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -4346,7 +4382,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cfg-if", "cfg_aliases", "libc", @@ -4359,7 +4395,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cfg-if", "cfg_aliases", "libc", @@ -4371,7 +4407,7 @@ version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cfg-if", "cfg_aliases", "libc", @@ -4398,7 +4434,7 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "fsevent-sys", "inotify", "kqueue", @@ -4416,14 +4452,14 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] name = "ntapi" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c70f219e21142367c70c0b30c6a9e3a14d55b4d12a204d897fbec83a0363f081" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" dependencies = [ "winapi", ] @@ -4552,9 +4588,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", ] @@ -4565,7 +4601,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -4595,9 +4631,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" dependencies = [ "portable-atomic", ] @@ -4616,11 +4652,11 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.75" +version = "0.10.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cfg-if", "foreign-types", "libc", @@ -4637,7 +4673,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -4663,9 +4699,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.111" +version = "0.9.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" dependencies = [ "cc", "libc", @@ -4834,9 +4870,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.5" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" dependencies = [ "memchr", "ucd-trie", @@ -4844,9 +4880,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.5" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" dependencies = [ "pest", "pest_generator", @@ -4854,22 +4890,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.5" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "pest_meta" -version = "2.8.5" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" dependencies = [ "pest", "sha2 0.10.9", @@ -4916,7 +4952,7 @@ dependencies = [ "phf_shared 0.13.1", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -4945,29 +4981,29 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -4977,9 +5013,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" dependencies = [ "atomic-waker", "fastrand", @@ -5029,6 +5065,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "polling" version = "3.11.0" @@ -5045,15 +5087,15 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" dependencies = [ "portable-atomic", ] @@ -5090,9 +5132,9 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "predicates" -version = "3.1.3" +version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" dependencies = [ "anstyle", "difflib", @@ -5104,15 +5146,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" [[package]] name = "predicates-tree" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" dependencies = [ "predicates-core", "termtree", @@ -5136,7 +5178,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -5150,11 +5192,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.23.10+spec-1.0.0", + "toml_edit 0.25.5+spec-1.1.0", ] [[package]] @@ -5177,9 +5219,9 @@ dependencies = [ [[package]] name = "psm" -version = "0.1.29" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa96cb91275ed31d6da3e983447320c4eb219ac180fa1679a0889ff32861e2d" +checksum = "3852766467df634d74f0b2d7819bf8dc483a0eb2e3b0f50f756f9cfe8b0d18d8" dependencies = [ "ar_archive_writer", "cc", @@ -5269,9 +5311,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -5317,9 +5359,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ "chacha20", "getrandom 0.4.2", @@ -5391,16 +5433,16 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] name = "redox_syscall" -version = "0.7.0" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -5442,7 +5484,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -5462,9 +5504,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -5474,9 +5516,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -5485,9 +5527,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "remove_dir_all" @@ -5573,7 +5615,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4e35aaaa439a5bda2f8d15251bc375e4edfac75f9865734644782c9701b5709" dependencies = [ "ahash 0.8.12", - "bitflags 2.10.0", + "bitflags 2.11.0", "instant", "num-traits", "once_cell", @@ -5591,7 +5633,7 @@ checksum = "d4322a2a4e8cf30771dd9f27f7f37ca9ac8fe812dddd811096a98483080dabe6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -5685,11 +5727,11 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys", @@ -5698,9 +5740,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.36" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "aws-lc-rs", "once_cell", @@ -5719,7 +5761,7 @@ dependencies = [ "openssl-probe 0.2.1", "rustls-pki-types", "schannel", - "security-framework 3.5.1", + "security-framework 3.7.0", ] [[package]] @@ -5747,7 +5789,7 @@ dependencies = [ "rustls-native-certs", "rustls-platform-verifier-android", "rustls-webpki", - "security-framework 3.5.1", + "security-framework 3.7.0", "security-framework-sys", "webpki-root-certs", "windows-sys 0.61.2", @@ -5761,9 +5803,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06" dependencies = [ "aws-lc-rs", "ring", @@ -5779,9 +5821,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "salsa20" @@ -5821,16 +5863,16 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] [[package]] name = "schema-gen" -version = "0.2.1" +version = "0.2.3" dependencies = [ "icp", "schemars", @@ -5840,9 +5882,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ "dyn-clone", "ref-cast", @@ -5854,14 +5896,14 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4908ad288c5035a8eb12cfdf0d49270def0a268ee162b75eeee0f85d155a7c45" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -5960,7 +6002,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -5969,11 +6011,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.5.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -5982,9 +6024,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -6037,7 +6079,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" dependencies = [ - "erased-serde 0.4.9", + "erased-serde 0.4.10", "serde", "serde_core", "typeid", @@ -6090,7 +6132,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -6101,7 +6143,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -6125,7 +6167,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -6173,9 +6215,9 @@ dependencies = [ [[package]] name = "serial_test" -version = "3.3.1" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d0b343e184fc3b7bb44dff0705fffcf4b3756ba6aff420dddd8b24ca145e555" +checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f" dependencies = [ "fslock", "futures-executor", @@ -6189,13 +6231,13 @@ dependencies = [ [[package]] name = "serial_test_derive" -version = "3.3.1" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f50427f258fb77356e4cd4aa0e87e2bd2c66dbcee41dc405282cae2bfc26c83" +checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -6308,9 +6350,9 @@ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "simple_asn1" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" dependencies = [ "num-bigint 0.4.6", "num-traits", @@ -6326,9 +6368,9 @@ checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "slog" @@ -6386,17 +6428,17 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -6417,9 +6459,9 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stacker" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1f8b29fb42aafcea4edeeb6b2f2d7ecd0d969c48b4cf0d2e64aafc471dd6e59" +checksum = "08d74a23609d509411d10e2176dc2a4346e3b4aea2e7b1869f19fdedbc71c013" dependencies = [ "cc", "cfg-if", @@ -6492,7 +6534,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -6504,7 +6546,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -6532,9 +6574,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.114" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -6558,7 +6600,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -6577,11 +6619,11 @@ dependencies = [ [[package]] name = "system-configuration" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -6604,9 +6646,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tar" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" dependencies = [ "filetime", "libc", @@ -6615,12 +6657,12 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.24.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2", @@ -6680,7 +6722,7 @@ checksum = "e5a2455608f6a56fc2ce66c89dd005501c169688f29165468e65f0b47e060333" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -6715,7 +6757,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -6726,7 +6768,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -6807,9 +6849,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -6839,13 +6881,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -6885,9 +6927,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.11+spec-1.1.0" +version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ "indexmap", "serde_core", @@ -6895,7 +6937,7 @@ dependencies = [ "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", - "winnow", + "winnow 0.7.15", ] [[package]] @@ -6916,6 +6958,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_datetime" +version = "1.0.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.22.27" @@ -6926,35 +6977,35 @@ dependencies = [ "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", - "winnow", + "winnow 0.7.15", ] [[package]] name = "toml_edit" -version = "0.23.10+spec-1.0.0" +version = "0.25.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1" dependencies = [ "indexmap", - "toml_datetime 0.7.5+spec-1.1.0", + "toml_datetime 1.0.1+spec-1.1.0", "toml_parser", - "winnow", + "winnow 1.0.0", ] [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.0.10+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" dependencies = [ - "winnow", + "winnow 1.0.0", ] [[package]] name = "toml_writer" -version = "1.0.6+spec-1.1.0" +version = "1.0.7+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d" [[package]] name = "tower" @@ -6977,7 +7028,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "bytes", "futures-util", "http", @@ -7020,7 +7071,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -7046,9 +7097,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "nu-ansi-term", "sharded-slab", @@ -7090,13 +7141,13 @@ checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "uds_windows" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "winapi", + "windows-sys 0.61.2", ] [[package]] @@ -7119,9 +7170,9 @@ checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-normalization" @@ -7207,11 +7258,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.20.0" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.2", "js-sys", "serde_core", "wasm-bindgen", @@ -7305,9 +7356,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -7318,9 +7369,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.58" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", "futures-util", @@ -7332,9 +7383,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7342,22 +7393,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] @@ -7403,7 +7454,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "hashbrown 0.15.5", "indexmap", "semver", @@ -7415,7 +7466,7 @@ version = "0.245.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f08c9adee0428b7bddf3890fc27e015ac4b761cc608c822667102b8bfd6995e" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "hashbrown 0.16.1", "indexmap", "semver", @@ -7424,9 +7475,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.85" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -7587,7 +7638,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -7598,7 +7649,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -7931,21 +7982,30 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" dependencies = [ "memchr", ] [[package]] name = "winreg" -version = "0.55.0" +version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" dependencies = [ "cfg-if", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -7978,7 +8038,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn 2.0.114", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -7994,7 +8054,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -8006,7 +8066,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.10.0", + "bitflags 2.11.0", "indexmap", "log", "serde", @@ -8096,7 +8156,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "synstructure", ] @@ -8141,7 +8201,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "zvariant_utils", ] @@ -8158,22 +8218,22 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.36" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dafd85c832c1b68bbb4ec0c72c7f6f4fc5179627d2bc7c26b30e4c0cc11e76cc" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.36" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cb7e4e8436d9db52fbd6625dbf2f45243ab84994a72882ec8227b99e72b439a" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -8193,7 +8253,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "synstructure", ] @@ -8214,7 +8274,7 @@ checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -8247,20 +8307,20 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "zlib-rs" -version = "0.5.5" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3" +checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" [[package]] name = "zmij" -version = "1.0.17" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "zvariant" @@ -8284,7 +8344,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "zvariant_utils", ] @@ -8296,5 +8356,5 @@ checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] diff --git a/Cargo.toml b/Cargo.toml index fa43d3cb..b2a08a2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ resolver = "3" [workspace.package] authors = ["DFINITY Stiftung "] edition = "2024" -version = "0.2.1" +version = "0.2.3" repository = "https://github.com/dfinity/icp-cli" rust-version = "1.88.0" license = "Apache-2.0" @@ -24,7 +24,7 @@ axoupdater = "0.10.0" backoff = { version = "0.4", features = ["tokio"] } bigdecimal = "0.4.10" bip32 = "0.5.0" -bollard = "0.20.1" +bollard = "0.20.2" byte-unit = "5.1.6" camino = { version = "1.1.9", features = ["serde1"] } cargo-generate = "0.23.7" @@ -46,15 +46,15 @@ glob = "0.3.2" handlebars = "6.3.2" hex = { version = "0.4.3", features = ["serde"] } httptest = "0.16.3" -ic-agent = { version = "0.46.0" } -ic-asset = "0.28.0" +ic-agent = { version = "0.47.0" } +ic-asset = "0.29.0" ic-ed25519 = "0.6.0" ic-ledger-types = "0.16.0" ic-management-canister-types = { version = "0.7.1" } -ic-utils = { version = "0.46.0" } +ic-utils = { version = "0.47.0" } icp = { path = "crates/icp" } icp-canister-interfaces = { path = "crates/icp-canister-interfaces" } -ic-identity-hsm = "0.46.0" +ic-identity-hsm = "0.47.0" icrc-ledger-types = "0.1.10" indicatif = "0.18.0" indoc = "2.0.6" @@ -75,7 +75,7 @@ predicates = "3" pem = "3.0.5" phf = { version = "0.13.1", features = ["macros"] } pkcs8 = { version = "0.10.2", features = ["encryption", "std"] } -rand = "0.10.0" +rand = "0.10.1" regex = "1.12.2" reqwest = { version = "0.13.2", default-features = false, features = ["rustls", "json", "stream"] } schemars = { version = "1.0.4", features = ["derive", "url2"] } @@ -104,7 +104,7 @@ tracing-subscriber = "0.3.20" url = { version = "2.5.4", features = ["serde"] } uuid = { version = "1.16.0", features = ["serde", "v4"] } wasmparser = "0.245.1" -winreg = "0.55" +winreg = "0.56.0" wslpath2 = "0.1" zeroize = "1.8.1" diff --git a/README.md b/README.md index 2cbe70ef..62c85264 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ If you're coming from dfx (the previous Internet Computer SDK), see the **[Migra ## Documentation -📚 **[Full Documentation Site](https://dfinity.github.io/icp-cli/)** — Complete guides, tutorials, and reference +📚 **[Full Documentation Site](https://cli.internetcomputer.org)** — Complete guides, tutorials, and reference Or browse the markdown docs directly: diff --git a/crates/icp-canister-interfaces/Cargo.toml b/crates/icp-canister-interfaces/Cargo.toml index 727fe8ae..fcffc9b5 100644 --- a/crates/icp-canister-interfaces/Cargo.toml +++ b/crates/icp-canister-interfaces/Cargo.toml @@ -8,4 +8,5 @@ publish.workspace = true [dependencies] bigdecimal = { workspace = true } candid = { workspace = true } +ic-management-canister-types = { workspace = true } serde = { workspace = true } diff --git a/crates/icp-canister-interfaces/src/cycles_ledger.rs b/crates/icp-canister-interfaces/src/cycles_ledger.rs index 72604f18..b35fc5a0 100644 --- a/crates/icp-canister-interfaces/src/cycles_ledger.rs +++ b/crates/icp-canister-interfaces/src/cycles_ledger.rs @@ -1,7 +1,7 @@ use candid::{CandidType, Nat, Principal}; use serde::Deserialize; -use crate::management_canister::CanisterSettingsArg; +use ic_management_canister_types::CanisterSettings; /// 100m cycles pub const CYCLES_LEDGER_BLOCK_FEE: u128 = 100_000_000; @@ -20,7 +20,7 @@ pub enum SubnetSelectionArg { #[derive(Clone, Debug, CandidType, Deserialize)] pub struct CreationArgs { pub subnet_selection: Option, - pub settings: Option, + pub settings: Option, } #[derive(Clone, Debug, CandidType, Deserialize)] diff --git a/crates/icp-canister-interfaces/src/internet_identity.rs b/crates/icp-canister-interfaces/src/internet_identity.rs index fdb8b90b..ccc03cd7 100644 --- a/crates/icp-canister-interfaces/src/internet_identity.rs +++ b/crates/icp-canister-interfaces/src/internet_identity.rs @@ -1,5 +1,8 @@ use candid::Principal; +pub const INTERNET_IDENTITY_FRONTEND_CID: &str = "uqzsh-gqaaa-aaaaq-qaada-cai"; +pub const INTERNET_IDENTITY_FRONTEND_PRINCIPAL: Principal = + Principal::from_slice(&[0, 0, 0, 0, 2, 16, 0, 6, 1, 1]); pub const INTERNET_IDENTITY_CID: &str = "rdmx6-jaaaa-aaaaa-aaadq-cai"; pub const INTERNET_IDENTITY_PRINCIPAL: Principal = Principal::from_slice(&[0, 0, 0, 0, 0, 0, 0, 7, 1, 1]); @@ -12,4 +15,12 @@ mod tests { fn internet_identity_cid_and_principal_match() { assert_eq!(INTERNET_IDENTITY_CID, INTERNET_IDENTITY_PRINCIPAL.to_text()); } + + #[test] + fn internet_identity_frontend_cid_is_valid() { + assert_eq!( + INTERNET_IDENTITY_FRONTEND_CID, + INTERNET_IDENTITY_FRONTEND_PRINCIPAL.to_text() + ); + } } diff --git a/crates/icp-canister-interfaces/src/lib.rs b/crates/icp-canister-interfaces/src/lib.rs index b2344326..9a95c416 100644 --- a/crates/icp-canister-interfaces/src/lib.rs +++ b/crates/icp-canister-interfaces/src/lib.rs @@ -4,7 +4,6 @@ pub mod cycles_minting_canister; pub mod governance; pub mod icp_ledger; pub mod internet_identity; -pub mod management_canister; pub mod nns_migration; pub mod nns_root; pub mod proxy; diff --git a/crates/icp-canister-interfaces/src/management_canister.rs b/crates/icp-canister-interfaces/src/management_canister.rs deleted file mode 100644 index 25a5bceb..00000000 --- a/crates/icp-canister-interfaces/src/management_canister.rs +++ /dev/null @@ -1,33 +0,0 @@ -use candid::{CandidType, Nat, Principal}; -use serde::Deserialize; - -#[derive(Clone, Debug, CandidType, Deserialize)] -pub struct MgmtCreateCanisterArgs { - pub settings: Option, - pub sender_canister_version: Option, -} - -#[derive(Clone, Debug, CandidType, Deserialize)] -pub struct MgmtCreateCanisterResponse { - pub canister_id: Principal, -} - -#[derive(Clone, Debug, CandidType, Deserialize)] -pub struct CanisterSettingsArg { - pub freezing_threshold: Option, - pub controllers: Option>, - pub reserved_cycles_limit: Option, - pub log_visibility: Option, - pub memory_allocation: Option, - pub compute_allocation: Option, -} - -/// Log visibility setting for a canister. -/// Matches the cycles ledger's LogVisibility variant type. -#[derive(Clone, Debug, CandidType, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum LogVisibility { - Controllers, - Public, - AllowedViewers(Vec), -} diff --git a/crates/icp-cli/src/commands/args.rs b/crates/icp-cli/src/commands/args.rs index 62836fd9..82f66e8c 100644 --- a/crates/icp-cli/src/commands/args.rs +++ b/crates/icp-cli/src/commands/args.rs @@ -1,11 +1,15 @@ use std::fmt::Display; use std::str::FromStr; +use anyhow::{Context as _, bail}; use candid::Principal; use clap::Args; use ic_ledger_types::AccountIdentifier; use icp::context::{CanisterSelection, EnvironmentSelection, NetworkSelection}; use icp::identity::IdentitySelection; +use icp::manifest::ArgsFormat; +use icp::prelude::PathBuf; +use icp::{InitArgs, fs}; use icrc_ledger_types::icrc1::account::Account; use crate::options::{EnvironmentOpt, IdentityOpt, NetworkOpt}; @@ -204,6 +208,82 @@ impl Display for FlexibleAccountId { } } +/// Grouped flags for specifying canister install arguments, shared by `canister install`, and `deploy`. +#[derive(Args, Clone, Debug, Default)] +pub(crate) struct ArgsOpt { + /// Inline arguments, interpreted per `--args-format` (Candid by default). + #[arg(long, conflicts_with = "args_file")] + pub(crate) args: Option, + + /// Path to a file containing arguments. + #[arg(long, conflicts_with = "args")] + pub(crate) args_file: Option, + + /// Format of the arguments. + #[arg(long, default_value = "candid")] + pub(crate) args_format: ArgsFormat, +} + +impl ArgsOpt { + /// Returns whether any args were provided via CLI flags. + pub(crate) fn is_some(&self) -> bool { + self.args.is_some() || self.args_file.is_some() + } + + /// Resolve CLI args to raw bytes, reading files as needed. + /// Returns `None` if no args were provided. + pub(crate) fn resolve_bytes(&self) -> Result>, anyhow::Error> { + load_args( + self.args.as_deref(), + self.args_file.as_ref(), + &self.args_format, + "--args", + )? + .as_ref() + .map(|ia| ia.to_bytes().context("failed to encode args")) + .transpose() + } +} + +/// Load args from an inline value or a file, returning the intermediate [`InitArgs`] +/// representation. Returns `None` if neither was provided. +/// +/// `inline_arg_name` is used in the error message when `--args-format bin` is given +/// with an inline value (e.g. `"--args"` or `"a positional argument"`). +pub(crate) fn load_args( + inline_value: Option<&str>, + args_file: Option<&PathBuf>, + args_format: &ArgsFormat, + inline_arg_name: &str, +) -> Result, anyhow::Error> { + match (inline_value, args_file) { + (Some(value), None) => { + if *args_format == ArgsFormat::Bin { + bail!("--args-format bin requires --args-file, not {inline_arg_name}"); + } + Ok(Some(InitArgs::Text { + content: value.to_owned(), + format: args_format.clone(), + })) + } + (None, Some(file_path)) => Ok(Some(match args_format { + ArgsFormat::Bin => { + let bytes = fs::read(file_path).context("failed to read args file")?; + InitArgs::Binary(bytes) + } + fmt => { + let content = fs::read_to_string(file_path).context("failed to read args file")?; + InitArgs::Text { + content: content.trim().to_owned(), + format: fmt.clone(), + } + } + })), + (None, None) => Ok(None), + (Some(_), Some(_)) => unreachable!("clap conflicts_with prevents this"), + } +} + #[cfg(test)] mod tests { use candid::Principal; diff --git a/crates/icp-cli/src/commands/canister/call.rs b/crates/icp-cli/src/commands/canister/call.rs index 1b6e8892..c06f58ca 100644 --- a/crates/icp-cli/src/commands/canister/call.rs +++ b/crates/icp-cli/src/commands/canister/call.rs @@ -1,6 +1,6 @@ use anyhow::{Context as _, anyhow, bail}; use candid::types::{Type, TypeInner}; -use candid::{Encode, IDLArgs, Nat, Principal, TypeEnv, types::Function}; +use candid::{IDLArgs, Principal, TypeEnv, types::Function}; use candid_parser::assist; use candid_parser::parse_idl_args; use candid_parser::utils::CandidSource; @@ -8,15 +8,18 @@ use clap::{Args, ValueEnum}; use dialoguer::console::Term; use ic_agent::Agent; use icp::context::Context; -use icp::fs; -use icp::manifest::InitArgsFormat; +use icp::manifest::ArgsFormat; use icp::parsers::CyclesAmount; use icp::prelude::*; -use icp_canister_interfaces::proxy::{ProxyArgs, ProxyResult}; +use serde::Serialize; use std::io::{self, Write}; -use tracing::warn; +use tracing::{error, warn}; -use crate::{commands::args, operations::misc::fetch_canister_metadata}; +use crate::{ + commands::args::{self, load_args}, + operations::misc::fetch_canister_metadata, + operations::proxy::update_or_proxy_raw, +}; /// How to interpret and display the call response blob. #[derive(Debug, Clone, Copy, Default, ValueEnum)] @@ -53,7 +56,7 @@ pub(crate) struct CallArgs { /// Format of the call arguments. #[arg(long, default_value = "candid")] - pub(crate) args_format: InitArgsFormat, + pub(crate) args_format: ArgsFormat, /// Principal of a proxy canister to route the call through. /// @@ -79,6 +82,10 @@ pub(crate) struct CallArgs { /// How to interpret and display the response. #[arg(long, short, default_value = "auto")] pub(crate) output: CallOutputMode, + + /// Output command results as JSON + #[arg(long)] + pub(crate) json: bool, } pub(crate) async fn exec(ctx: &Context, args: &CallArgs) -> Result<(), anyhow::Error> { @@ -127,45 +134,36 @@ pub(crate) async fn exec(ctx: &Context, args: &CallArgs) -> Result<(), anyhow::E Bytes(Vec), } - let resolved_args = match (&args.args, &args.args_file) { - (Some(value), None) => { - if args.args_format == InitArgsFormat::Bin { - bail!("--args-format bin requires --args-file, not a positional argument"); - } - Some(match args.args_format { - InitArgsFormat::Candid => ResolvedArgs::Candid( - parse_idl_args(value).context("failed to parse Candid arguments")?, - ), - InitArgsFormat::Hex => ResolvedArgs::Bytes( - hex::decode(value).context("failed to decode hex arguments")?, - ), - InitArgsFormat::Bin => unreachable!(), - }) + let resolved_args = match load_args( + args.args.as_deref(), + args.args_file.as_ref(), + &args.args_format, + "a positional argument", + )? { + None => None, + Some(icp::InitArgs::Binary(bytes)) => Some(ResolvedArgs::Bytes(bytes)), + Some(icp::InitArgs::Text { + content, + format: ArgsFormat::Candid, + }) => Some(ResolvedArgs::Candid( + parse_idl_args(&content).context("failed to parse Candid arguments")?, + )), + Some(icp::InitArgs::Text { + content, + format: ArgsFormat::Hex, + }) => Some(ResolvedArgs::Bytes( + hex::decode(&content).context("failed to decode hex arguments")?, + )), + Some(icp::InitArgs::Text { + format: ArgsFormat::Bin, + .. + }) => { + unreachable!("load_args rejects bin format for inline values") } - (None, Some(file_path)) => Some(match args.args_format { - InitArgsFormat::Bin => { - let bytes = fs::read(file_path).context("failed to read args file")?; - ResolvedArgs::Bytes(bytes) - } - InitArgsFormat::Hex => { - let content = fs::read_to_string(file_path).context("failed to read args file")?; - ResolvedArgs::Bytes( - hex::decode(content.trim()).context("failed to decode hex from file")?, - ) - } - InitArgsFormat::Candid => { - let content = fs::read_to_string(file_path).context("failed to read args file")?; - ResolvedArgs::Candid( - parse_idl_args(content.trim()).context("failed to parse Candid from file")?, - ) - } - }), - (None, None) => None, - (Some(_), Some(_)) => unreachable!("clap conflicts_with prevents this"), }; let arg_bytes = match (&declared_method, resolved_args) { - (_, None) if args.args_format != InitArgsFormat::Candid => { + (_, None) if args.args_format != ArgsFormat::Candid => { bail!("arguments must be provided when --args-format is not candid"); } (None, None) => bail!( @@ -201,30 +199,7 @@ pub(crate) async fn exec(ctx: &Context, args: &CallArgs) -> Result<(), anyhow::E .context("failed to serialize candid arguments with specific types")?, }; - let res = if let Some(proxy_cid) = args.proxy { - // Route the call through the proxy canister - let proxy_args = ProxyArgs { - canister_id: cid, - method: method.clone(), - args: arg_bytes, - cycles: Nat::from(args.cycles.get()), - }; - let proxy_arg_bytes = - Encode!(&proxy_args).context("failed to encode proxy call arguments")?; - - let proxy_res = agent - .update(&proxy_cid, "proxy") - .with_arg(proxy_arg_bytes) - .await?; - - let proxy_result: (ProxyResult,) = - candid::decode_args(&proxy_res).context("failed to decode proxy canister response")?; - - match proxy_result.0 { - ProxyResult::Ok(ok) => ok.result, - ProxyResult::Err(err) => bail!(err.format_error()), - } - } else if args.query { + let res = if args.query { // Preemptive check: error if Candid shows this is an update method if let Some((_, func)) = &declared_method && !func.is_query() @@ -240,47 +215,100 @@ pub(crate) async fn exec(ctx: &Context, args: &CallArgs) -> Result<(), anyhow::E .call() .await? } else { - // Direct update call to the target canister - agent.update(&cid, &method).with_arg(arg_bytes).await? + update_or_proxy_raw( + &agent, + cid, + &method, + arg_bytes, + args.proxy, + None, + args.cycles.get(), + ) + .await? }; let mut term = Term::buffered_stdout(); let res_hex = || format!("response (hex): {}", hex::encode(&res)); + let mut json_response = JsonCallResponse { + response_bytes: hex::encode(&res), + response_text: None, + response_candid: None, + }; - match args.output { - CallOutputMode::Auto => { - if let Ok(ret) = try_decode_candid(&res, declared_method.as_ref()) { - print_candid_for_term(&mut term, &ret) - .context("failed to print candid return value")?; - } else if let Ok(s) = std::str::from_utf8(&res) { - writeln!(term, "{s}")?; - term.flush()?; - } else { + // catch errors, because the json result should be printed regardless of errors + let res = (|| { + match args.output { + CallOutputMode::Auto => { + if let Ok(ret) = try_decode_candid(&res, declared_method.as_ref()) { + if args.json { + json_response.response_candid = Some(format!("{ret}")); + } else { + print_candid_for_term(&mut term, &ret) + .context("failed to print candid return value")?; + } + } else if let Ok(s) = std::str::from_utf8(&res) { + if args.json { + json_response.response_text = Some(s.to_string()); + } else { + writeln!(term, "{s}")?; + term.flush()?; + } + } else if !args.json { + writeln!(term, "{}", hex::encode(&res))?; + term.flush()?; + } + } + CallOutputMode::Candid => { + let ret = + try_decode_candid(&res, declared_method.as_ref()).with_context(res_hex)?; + if args.json { + json_response.response_candid = Some(format!("{ret}")); + } else { + print_candid_for_term(&mut term, &ret) + .context("failed to print candid return value")?; + } + } + CallOutputMode::Text => { + let s = std::str::from_utf8(&res) + .with_context(res_hex) + .context("response is not valid UTF-8")?; + if args.json { + json_response.response_text = Some(s.to_string()); + } else { + writeln!(term, "{s}")?; + term.flush()?; + } + } + CallOutputMode::Hex => { writeln!(term, "{}", hex::encode(&res))?; term.flush()?; } - } - CallOutputMode::Candid => { - let ret = try_decode_candid(&res, declared_method.as_ref()).with_context(res_hex)?; - print_candid_for_term(&mut term, &ret) - .context("failed to print candid return value")?; - } - CallOutputMode::Text => { - let s = std::str::from_utf8(&res) - .with_context(res_hex) - .context("response is not valid UTF-8")?; - writeln!(term, "{s}")?; - term.flush()?; - } - CallOutputMode::Hex => { - writeln!(term, "{}", hex::encode(&res))?; - term.flush()?; + }; + anyhow::Ok(()) + })(); + if args.json { + let write_result = serde_json::to_writer(term, &json_response); + if let Err(write_err) = write_result { + if let Err(decode_err) = res { + error!("failed to write JSON response: {write_err}"); + return Err(decode_err); + } else { + return Err(write_err).context("failed to write JSON response"); + } } } + res?; Ok(()) } +#[derive(Serialize)] +struct JsonCallResponse { + response_bytes: String, + response_text: Option, + response_candid: Option, +} + /// Tries to decode the response as Candid. Returns `None` if decoding fails. fn try_decode_candid( res: &[u8], diff --git a/crates/icp-cli/src/commands/canister/create.rs b/crates/icp-cli/src/commands/canister/create.rs index 338cea7c..f2d6e126 100644 --- a/crates/icp-cli/src/commands/canister/create.rs +++ b/crates/icp-cli/src/commands/canister/create.rs @@ -1,13 +1,19 @@ +use std::io::stdout; + use anyhow::anyhow; use candid::{Nat, Principal}; use clap::{ArgGroup, Args, Parser}; +use ic_management_canister_types::CanisterSettings as MgmtCanisterSettings; use icp::context::Context; use icp::parsers::{CyclesAmount, DurationAmount, MemoryAmount}; use icp::{Canister, context::CanisterSelection, prelude::*}; -use icp_canister_interfaces::management_canister::CanisterSettingsArg; +use serde::Serialize; use tracing::info; -use crate::{commands::args, operations::create::CreateOperation}; +use crate::{ + commands::args, + operations::create::{CreateOperation, CreateTarget}, +}; pub(crate) const DEFAULT_CANISTER_CYCLES: u128 = 2 * TRILLION; @@ -79,9 +85,17 @@ pub(crate) struct CreateArgs { pub(crate) cycles: CyclesAmount, /// The subnet to create canisters on. - #[arg(long)] + #[arg(long, conflicts_with = "proxy")] pub(crate) subnet: Option, + /// Principal of a proxy canister to route the create_canister call through. + /// + /// When specified, the canister will be created on the same subnet as the + /// proxy canister by forwarding the management canister call through the + /// proxy's `proxy` method. + #[arg(long, conflicts_with = "subnet")] + pub(crate) proxy: Option, + /// Create a canister detached from any project configuration. The canister id will be /// printed out but not recorded in the project configuration. Not valid if `Canister` /// is provided. @@ -91,11 +105,18 @@ pub(crate) struct CreateArgs { required_unless_present = "canister" )] pub detached: bool, + + /// Output command results as JSON + #[arg(long, conflicts_with = "quiet")] + pub(crate) json: bool, } impl CreateArgs { - pub(crate) fn canister_settings_with_default(&self, default: &Canister) -> CanisterSettingsArg { - CanisterSettingsArg { + pub(crate) fn canister_settings_with_default( + &self, + default: &Canister, + ) -> MgmtCanisterSettings { + MgmtCanisterSettings { freezing_threshold: self .settings .freezing_threshold @@ -126,11 +147,20 @@ impl CreateArgs { .compute_allocation .or(default.settings.compute_allocation) .map(Nat::from), + ..Default::default() + } + } + + fn create_target(&self) -> CreateTarget { + match (self.subnet, self.proxy) { + (Some(subnet), _) => CreateTarget::Subnet(subnet), + (_, Some(proxy)) => CreateTarget::Proxy(proxy), + _ => CreateTarget::None, } } - pub(crate) fn canister_settings(&self) -> CanisterSettingsArg { - CanisterSettingsArg { + pub(crate) fn canister_settings(&self) -> MgmtCanisterSettings { + MgmtCanisterSettings { freezing_threshold: self .settings .freezing_threshold @@ -154,6 +184,7 @@ impl CreateArgs { .clone() .map(|m| Nat::from(m.get())), compute_allocation: self.settings.compute_allocation.map(Nat::from), + ..Default::default() } } } @@ -185,7 +216,8 @@ async fn create_canister(ctx: &Context, args: &CreateArgs) -> Result<(), anyhow: ) .await?; - let create_operation = CreateOperation::new(agent, args.subnet, args.cycles.get(), vec![]); + let create_operation = + CreateOperation::new(agent, args.create_target(), args.cycles.get(), vec![]); let canister_settings = args.canister_settings(); @@ -193,6 +225,14 @@ async fn create_canister(ctx: &Context, args: &CreateArgs) -> Result<(), anyhow: if args.quiet { println!("{id}"); + } else if args.json { + serde_json::to_writer( + stdout(), + &JsonCreate { + canister_id: id, + canister_name: None, + }, + )?; } else { println!("Created canister with ID {id}"); } @@ -237,8 +277,12 @@ async fn create_project_canister(ctx: &Context, args: &CreateArgs) -> Result<(), .into_values() .collect(); - let create_operation = - CreateOperation::new(agent, args.subnet, args.cycles.get(), existing_canisters); + let create_operation = CreateOperation::new( + agent, + args.create_target(), + args.cycles.get(), + existing_canisters, + ); let canister_settings = args.canister_settings_with_default(&canister_info); let id = create_operation.create(&canister_settings).await?; @@ -249,9 +293,23 @@ async fn create_project_canister(ctx: &Context, args: &CreateArgs) -> Result<(), if args.quiet { println!("{id}"); + } else if args.json { + serde_json::to_writer( + stdout(), + &JsonCreate { + canister_id: id, + canister_name: Some(canister.clone()), + }, + )?; } else { println!("Created canister {canister} with ID {id}"); } Ok(()) } + +#[derive(Serialize)] +struct JsonCreate { + canister_id: Principal, + canister_name: Option, +} diff --git a/crates/icp-cli/src/commands/canister/delete.rs b/crates/icp-cli/src/commands/canister/delete.rs index a82baa54..de32f003 100644 --- a/crates/icp-cli/src/commands/canister/delete.rs +++ b/crates/icp-cli/src/commands/canister/delete.rs @@ -1,13 +1,19 @@ +use candid::Principal; use clap::Args; +use ic_management_canister_types::CanisterIdRecord; use icp::context::{CanisterSelection, Context}; -use crate::commands::args; +use crate::{commands::args, operations::proxy_management}; /// Delete a canister from a network #[derive(Debug, Args)] pub(crate) struct DeleteArgs { #[command(flatten)] pub(crate) cmd_args: args::CanisterCommandArgs, + + /// Principal of a proxy canister to route the management canister call through. + #[arg(long)] + pub(crate) proxy: Option, } pub(crate) async fn exec(ctx: &Context, args: &DeleteArgs) -> Result<(), anyhow::Error> { @@ -28,11 +34,8 @@ pub(crate) async fn exec(ctx: &Context, args: &DeleteArgs) -> Result<(), anyhow: ) .await?; - // Management Interface - let mgmt = ic_utils::interfaces::ManagementCanister::create(&agent); - - // Instruct management canister to delete canister - mgmt.delete_canister(&cid).await?; + proxy_management::delete_canister(&agent, args.proxy, CanisterIdRecord { canister_id: cid }) + .await?; // Remove canister ID from the id store if it was referenced by name if let CanisterSelection::Named(canister_name) = &selections.canister { diff --git a/crates/icp-cli/src/commands/canister/install.rs b/crates/icp-cli/src/commands/canister/install.rs index 7c80dcc4..69b74ace 100644 --- a/crates/icp-cli/src/commands/canister/install.rs +++ b/crates/icp-cli/src/commands/canister/install.rs @@ -1,17 +1,17 @@ use std::io::IsTerminal; use anyhow::{Context as _, anyhow, bail}; +use candid::Principal; use clap::Args; use dialoguer::Confirm; -use ic_utils::interfaces::management_canister::builders::CanisterInstallMode; +use ic_management_canister_types::CanisterInstallMode; use icp::context::{CanisterSelection, Context}; -use icp::manifest::InitArgsFormat; +use icp::fs; use icp::prelude::*; -use icp::{InitArgs, fs}; use tracing::{info, warn}; use crate::{ - commands::args, + commands::args::{self, ArgsOpt}, operations::{ candid_compat::{CandidCompatibility, check_candid_compatibility}, install::{install_canister, resolve_install_mode_and_status}, @@ -29,17 +29,8 @@ pub(crate) struct InstallArgs { #[arg(long)] pub(crate) wasm: Option, - /// Inline initialization arguments, interpreted per `--args-format` (Candid by default). - #[arg(long, conflicts_with = "args_file")] - pub(crate) args: Option, - - /// Path to a file containing initialization arguments. - #[arg(long, conflicts_with = "args")] - pub(crate) args_file: Option, - - /// Format of the initialization arguments. - #[arg(long, default_value = "candid")] - pub(crate) args_format: InitArgsFormat, + #[command(flatten)] + pub(crate) args_opt: ArgsOpt, /// Skip confirmation prompts, including the Candid interface compatibility check. #[arg(long, short)] @@ -47,6 +38,10 @@ pub(crate) struct InstallArgs { #[command(flatten)] pub(crate) cmd_args: args::CanisterCommandArgs, + + /// Principal of a proxy canister to route the management canister call through. + #[arg(long)] + pub(crate) proxy: Option, } pub(crate) async fn exec(ctx: &Context, args: &InstallArgs) -> Result<(), anyhow::Error> { @@ -87,43 +82,17 @@ pub(crate) async fn exec(ctx: &Context, args: &InstallArgs) -> Result<(), anyhow .await?; // If you add .did support to this code, consider extracting/unifying with the logic from call.rs - let init_args = match (&args.args, &args.args_file) { - (Some(value), None) => { - if args.args_format == InitArgsFormat::Bin { - bail!("--args-format bin requires --args-file, not --args"); - } - Some(InitArgs::Text { - content: value.clone(), - format: args.args_format.clone(), - }) - } - (None, Some(file_path)) => Some(match args.args_format { - InitArgsFormat::Bin => { - let bytes = fs::read(file_path).context("failed to read init args file")?; - InitArgs::Binary(bytes) - } - ref fmt => { - let content = - fs::read_to_string(file_path).context("failed to read init args file")?; - InitArgs::Text { - content: content.trim().to_owned(), - format: fmt.clone(), - } - } - }), - (None, None) => None, - (Some(_), Some(_)) => unreachable!("clap conflicts_with prevents this"), - }; - - let init_args_bytes = init_args - .as_ref() - .map(|ia| ia.to_bytes().context("failed to encode init args")) - .transpose()?; + let init_args_bytes = args.args_opt.resolve_bytes()?; let canister_display = args.cmd_args.canister.to_string(); - let (install_mode, status) = - resolve_install_mode_and_status(&agent, &canister_display, &canister_id, &args.mode) - .await?; + let (install_mode, status) = resolve_install_mode_and_status( + &agent, + args.proxy, + &canister_display, + &canister_id, + &args.mode, + ) + .await?; // Candid interface compatibility check for upgrades if !args.yes && matches!(install_mode, CanisterInstallMode::Upgrade(_)) { @@ -156,6 +125,7 @@ pub(crate) async fn exec(ctx: &Context, args: &InstallArgs) -> Result<(), anyhow install_canister( &agent, + args.proxy, &canister_id, &canister_display, &wasm, diff --git a/crates/icp-cli/src/commands/canister/list.rs b/crates/icp-cli/src/commands/canister/list.rs index cedf298d..f56b3f20 100644 --- a/crates/icp-cli/src/commands/canister/list.rs +++ b/crates/icp-cli/src/commands/canister/list.rs @@ -1,5 +1,8 @@ +use std::io::stdout; + use clap::Args; use icp::context::Context; +use serde::Serialize; use crate::options::EnvironmentOpt; @@ -8,15 +11,24 @@ use crate::options::EnvironmentOpt; pub(crate) struct ListArgs { #[command(flatten)] pub(crate) environment: EnvironmentOpt, + /// Output command results as JSON + #[arg(long)] + pub(crate) json: bool, } pub(crate) async fn exec(ctx: &Context, args: &ListArgs) -> Result<(), anyhow::Error> { let environment_selection = args.environment.clone().into(); let env = ctx.get_environment(&environment_selection).await?; - - for c in env.canisters.keys() { - println!("{c}"); + let canisters = env.canisters.keys().cloned().collect(); + if args.json { + serde_json::to_writer(stdout(), &JsonList { canisters })?; + } else { + println!("{}", canisters.join("\n")); } - Ok(()) } + +#[derive(Serialize)] +struct JsonList { + canisters: Vec, +} diff --git a/crates/icp-cli/src/commands/canister/logs.rs b/crates/icp-cli/src/commands/canister/logs.rs index 2245933d..78c66ac4 100644 --- a/crates/icp-cli/src/commands/canister/logs.rs +++ b/crates/icp-cli/src/commands/canister/logs.rs @@ -1,15 +1,18 @@ +use std::io::stdout; + use anyhow::{Context as _, anyhow}; +use candid::Principal; use clap::Args; -use ic_utils::interfaces::ManagementCanister; -use ic_utils::interfaces::management_canister::{ - CanisterLogFilter, CanisterLogRecord, FetchCanisterLogsArgs, FetchCanisterLogsResult, -}; +use ic_agent::Agent; +use ic_management_canister_types::{CanisterLogFilter, CanisterLogRecord, FetchCanisterLogsArgs}; use icp::context::Context; use icp::signal::stop_signal; +use itertools::Itertools; +use serde::Serialize; use time::{OffsetDateTime, format_description::well_known::Rfc3339}; use tokio::select; -use crate::commands::args; +use crate::{commands::args, operations::proxy_management}; /// Fetch and display canister logs #[derive(Debug, Args)] @@ -42,6 +45,17 @@ pub(crate) struct LogsArgs { /// Show logs before this log index (exclusive). Cannot be used with --follow #[arg(long, value_name = "INDEX", conflicts_with_all = ["follow", "since", "until"])] pub(crate) until_index: Option, + + /// Output command results as JSON + #[arg(long)] + pub(crate) json: bool, + + /// Principal of a proxy canister to route the management canister call through. + /// + /// Hidden until the IC supports fetch_canister_logs in replicated mode. + /// Tracking: https://github.com/dfinity/portal/pull/6106 + #[arg(long, hide = true)] + pub(crate) proxy: Option, } fn parse_timestamp(s: &str) -> Result { @@ -95,17 +109,26 @@ pub(crate) async fn exec(ctx: &Context, args: &LogsArgs) -> Result<(), anyhow::E ) .await?; - let mgmt = ManagementCanister::create(&agent); - if args.follow { // Follow mode: continuously fetch and display new logs - follow_logs(&mgmt, &canister_id, args.interval).await + follow_logs(args, &agent, args.proxy, &canister_id, args.interval).await } else { // Single fetch mode: fetch all logs once - fetch_and_display_logs(&mgmt, &canister_id, build_filter(args)?).await + fetch_and_display_logs(args, &agent, args.proxy, &canister_id, build_filter(args)?).await } } +#[derive(Serialize)] +struct JsonFollowRecord { + timestamp: u64, + index: u64, + content: String, +} +#[derive(Serialize)] +struct JsonListRecord { + log_records: Vec, +} + fn build_filter(args: &LogsArgs) -> Result, anyhow::Error> { if args.since_index.is_some() || args.until_index.is_some() { let start = args.since_index.unwrap_or(0); @@ -141,7 +164,9 @@ fn build_filter(args: &LogsArgs) -> Result, anyhow::Er } async fn fetch_and_display_logs( - mgmt: &ManagementCanister<'_>, + args: &LogsArgs, + agent: &Agent, + proxy: Option, canister_id: &candid::Principal, filter: Option, ) -> Result<(), anyhow::Error> { @@ -149,14 +174,34 @@ async fn fetch_and_display_logs( canister_id: *canister_id, filter, }; - let (result,): (FetchCanisterLogsResult,) = mgmt - .fetch_canister_logs(&fetch_args) + let result = proxy_management::fetch_canister_logs(agent, proxy, fetch_args) .await .context("Failed to fetch canister logs")?; - for log in result.canister_log_records { - let formatted = format_log(&log); - println!("{formatted}"); + if args.json { + println!( + "{}", + result + .canister_log_records + .iter() + .map(format_log) + .format("\n") + ); + } else { + serde_json::to_writer( + stdout(), + &JsonListRecord { + log_records: result + .canister_log_records + .iter() + .map(|log| JsonFollowRecord { + timestamp: log.timestamp_nanos, + index: log.idx, + content: String::from_utf8_lossy(&log.content).into_owned(), + }) + .collect(), + }, + )?; } Ok(()) @@ -165,7 +210,9 @@ async fn fetch_and_display_logs( const FOLLOW_LOOKBACK_NANOS: u64 = 60 * 60 * 1_000_000_000; // 1 hour async fn follow_logs( - mgmt: &ManagementCanister<'_>, + args: &LogsArgs, + agent: &Agent, + proxy: Option, canister_id: &candid::Principal, interval_seconds: u64, ) -> Result<(), anyhow::Error> { @@ -195,8 +242,7 @@ async fn follow_logs( canister_id: *canister_id, filter, }; - let (result,): (FetchCanisterLogsResult,) = mgmt - .fetch_canister_logs(&fetch_args) + let result = proxy_management::fetch_canister_logs(agent, proxy, fetch_args) .await .context("Failed to fetch canister logs")?; @@ -204,8 +250,18 @@ async fn follow_logs( if !new_logs.is_empty() { for log in &new_logs { - let formatted = format_log(log); - println!("{formatted}"); + if args.json { + serde_json::to_writer( + stdout(), + &JsonFollowRecord { + timestamp: log.timestamp_nanos, + index: log.idx, + content: String::from_utf8_lossy(&log.content).into_owned(), + }, + )?; + } else { + println!("{}", format_log(log)); + } } // Update last_idx to the highest idx we've displayed if let Some(last_log) = new_logs.last() { @@ -387,6 +443,8 @@ mod tests { until, since_index, until_index, + json: false, + proxy: None, } } diff --git a/crates/icp-cli/src/commands/canister/metadata.rs b/crates/icp-cli/src/commands/canister/metadata.rs index d117dc33..32486aaa 100644 --- a/crates/icp-cli/src/commands/canister/metadata.rs +++ b/crates/icp-cli/src/commands/canister/metadata.rs @@ -1,6 +1,9 @@ +use std::io::stdout; + use anyhow::bail; use clap::Args; use icp::context::Context; +use serde::Serialize; use crate::{commands::args, operations::misc::fetch_canister_metadata}; @@ -12,6 +15,10 @@ pub(crate) struct MetadataArgs { /// The name of the metadata section to read pub(crate) metadata_name: String, + + /// Output command results as JSON + #[arg(long)] + pub(crate) json: bool, } pub(crate) async fn exec(ctx: &Context, args: &MetadataArgs) -> Result<(), anyhow::Error> { @@ -40,7 +47,11 @@ pub(crate) async fn exec(ctx: &Context, args: &MetadataArgs) -> Result<(), anyho match metadata { Some(value) => { - println!("{value}"); + if args.json { + serde_json::to_writer(stdout(), &JsonMetadata { value })?; + } else { + println!("{value}"); + } Ok(()) } None => bail!( @@ -50,3 +61,8 @@ pub(crate) async fn exec(ctx: &Context, args: &MetadataArgs) -> Result<(), anyho ), } } + +#[derive(Serialize)] +struct JsonMetadata { + value: String, +} diff --git a/crates/icp-cli/src/commands/canister/migrate_id.rs b/crates/icp-cli/src/commands/canister/migrate_id.rs index 777cc5d3..01ab97a7 100644 --- a/crates/icp-cli/src/commands/canister/migrate_id.rs +++ b/crates/icp-cli/src/commands/canister/migrate_id.rs @@ -4,8 +4,9 @@ use std::time::{Duration, Instant}; use anyhow::bail; use clap::Args; use dialoguer::Confirm; -use ic_management_canister_types::CanisterStatusType; -use ic_utils::interfaces::ManagementCanister; +use ic_management_canister_types::{ + CanisterIdRecord, CanisterSettings, CanisterStatusType, UpdateSettingsArgs, +}; use icp::context::Context; use icp_canister_interfaces::nns_migration::{MigrationStatus, NNS_MIGRATION_PRINCIPAL}; use indicatif::{ProgressBar, ProgressStyle}; @@ -17,6 +18,7 @@ use crate::operations::canister_migration::{ get_subnet_for_canister, migrate_canister, migration_status, }; use crate::operations::misc::format_timestamp; +use crate::operations::proxy_management; use icp::context::CanisterSelection; /// Minimum cycles required for migration (10T). @@ -45,6 +47,10 @@ pub(crate) struct MigrateIdArgs { /// Exit as soon as the migrated canister is deleted (don't wait for full completion) #[arg(long)] skip_watch: bool, + + /// Principal of a proxy canister to route the management canister calls through. + #[arg(long)] + proxy: Option, } pub(crate) async fn exec(ctx: &Context, args: &MigrateIdArgs) -> Result<(), anyhow::Error> { @@ -105,11 +111,23 @@ pub(crate) async fn exec(ctx: &Context, args: &MigrateIdArgs) -> Result<(), anyh } } - let mgmt = ManagementCanister::create(&agent); - // Fetch status of both canisters - let (source_status,) = mgmt.canister_status(&source_cid).await?; - let (target_status,) = mgmt.canister_status(&target_cid).await?; + let source_status = proxy_management::canister_status( + &agent, + args.proxy, + CanisterIdRecord { + canister_id: source_cid, + }, + ) + .await?; + let target_status = proxy_management::canister_status( + &agent, + args.proxy, + CanisterIdRecord { + canister_id: target_cid, + }, + ) + .await?; // Check both are stopped ensure_canister_stopped(source_status.status, &source_name)?; @@ -156,7 +174,14 @@ pub(crate) async fn exec(ctx: &Context, args: &MigrateIdArgs) -> Result<(), anyh } // Check target canister has no snapshots - let (snapshots,) = mgmt.list_canister_snapshots(&target_cid).await?; + let snapshots = proxy_management::list_canister_snapshots( + &agent, + args.proxy, + CanisterIdRecord { + canister_id: target_cid, + }, + ) + .await?; if !snapshots.is_empty() { bail!( "The target canister '{target_name}' ({target_cid}) has {} snapshot(s). \ @@ -188,11 +213,19 @@ pub(crate) async fn exec(ctx: &Context, args: &MigrateIdArgs) -> Result<(), anyh info!("Adding NNS migration canister as controller of '{source_name}'..."); let mut new_controllers = source_controllers; new_controllers.push(NNS_MIGRATION_PRINCIPAL); - let mut builder = mgmt.update_settings(&source_cid); - for controller in new_controllers { - builder = builder.with_controller(controller); - } - builder.await?; + proxy_management::update_settings( + &agent, + args.proxy, + UpdateSettingsArgs { + canister_id: source_cid, + settings: CanisterSettings { + controllers: Some(new_controllers), + ..Default::default() + }, + sender_canister_version: None, + }, + ) + .await?; } let target_controllers = target_status.settings.controllers; @@ -200,11 +233,19 @@ pub(crate) async fn exec(ctx: &Context, args: &MigrateIdArgs) -> Result<(), anyh info!("Adding NNS migration canister as controller of '{target_name}'..."); let mut new_controllers = target_controllers; new_controllers.push(NNS_MIGRATION_PRINCIPAL); - let mut builder = mgmt.update_settings(&target_cid); - for controller in new_controllers { - builder = builder.with_controller(controller); - } - builder.await?; + proxy_management::update_settings( + &agent, + args.proxy, + UpdateSettingsArgs { + canister_id: target_cid, + settings: CanisterSettings { + controllers: Some(new_controllers), + ..Default::default() + }, + sender_canister_version: None, + }, + ) + .await?; } // Initiate migration diff --git a/crates/icp-cli/src/commands/canister/settings/sync.rs b/crates/icp-cli/src/commands/canister/settings/sync.rs index 06ba3f06..7de66c89 100644 --- a/crates/icp-cli/src/commands/canister/settings/sync.rs +++ b/crates/icp-cli/src/commands/canister/settings/sync.rs @@ -1,6 +1,6 @@ use anyhow::bail; +use candid::Principal; use clap::Args; -use ic_utils::interfaces::ManagementCanister; use icp::context::{CanisterSelection, Context}; use crate::commands::args::CanisterCommandArgs; @@ -10,6 +10,10 @@ use crate::commands::args::CanisterCommandArgs; pub(crate) struct SyncArgs { #[command(flatten)] cmd_args: CanisterCommandArgs, + + /// Principal of a proxy canister to route the management canister calls through. + #[arg(long)] + proxy: Option, } pub(crate) async fn exec(ctx: &Context, args: &SyncArgs) -> Result<(), anyhow::Error> { @@ -37,8 +41,6 @@ pub(crate) async fn exec(ctx: &Context, args: &SyncArgs) -> Result<(), anyhow::E ) .await?; - let mgmt = ManagementCanister::create(&agent); - - crate::operations::settings::sync_settings(&mgmt, &cid, &canister).await?; + crate::operations::settings::sync_settings(&agent, args.proxy, &cid, &canister).await?; Ok(()) } diff --git a/crates/icp-cli/src/commands/canister/settings/update.rs b/crates/icp-cli/src/commands/canister/settings/update.rs index 4c60f534..46c8a5f5 100644 --- a/crates/icp-cli/src/commands/canister/settings/update.rs +++ b/crates/icp-cli/src/commands/canister/settings/update.rs @@ -1,16 +1,20 @@ use anyhow::bail; +use candid::Nat; use clap::{ArgAction, Args}; use dialoguer::Confirm; use ic_agent::Identity; use ic_agent::export::Principal; -use ic_management_canister_types::{CanisterStatusResult, EnvironmentVariable, LogVisibility}; +use ic_management_canister_types::{ + CanisterIdRecord, CanisterSettings, CanisterStatusResult, EnvironmentVariable, LogVisibility, + UpdateSettingsArgs, +}; use icp::ProjectLoadError; use icp::context::{CanisterSelection, Context}; use icp::parsers::{CyclesAmount, DurationAmount, MemoryAmount}; use std::collections::{HashMap, HashSet}; use tracing::warn; -use crate::commands::args; +use crate::{commands::args, operations::proxy_management}; #[derive(Clone, Debug, Default, Args)] pub(crate) struct ControllerOpt { @@ -130,6 +134,10 @@ pub(crate) struct UpdateArgs { #[command(flatten)] environment_variables: Option, + + /// Principal of a proxy canister to route the management canister calls through. + #[arg(long)] + proxy: Option, } pub(crate) async fn exec(ctx: &Context, args: &UpdateArgs) -> Result<(), anyhow::Error> { @@ -164,12 +172,16 @@ pub(crate) async fn exec(ctx: &Context, args: &UpdateArgs) -> Result<(), anyhow: <_>::default() }; - // Management Interface - let mgmt = ic_utils::interfaces::ManagementCanister::create(&agent); - let mut current_status: Option = None; if require_current_settings(args) { - current_status = Some(mgmt.canister_status(&cid).await?.0); + current_status = Some( + proxy_management::canister_status( + &agent, + args.proxy, + CanisterIdRecord { canister_id: cid }, + ) + .await?, + ); } // TODO(VZ): Ask for consent if the freezing threshold is too long or too short. @@ -179,13 +191,25 @@ pub(crate) async fn exec(ctx: &Context, args: &UpdateArgs) -> Result<(), anyhow: if let Some(controllers_opt) = &args.controllers { controllers = get_controllers(controllers_opt, current_status.as_ref()); - // Check if the caller is being removed from controllers + // Check if the effective controller is being removed from the controller list. + // When --proxy is set, the proxy canister is the one making management calls and + // is the effective controller. Without --proxy, it's the caller's identity. + let effective_controller = args.proxy.unwrap_or(caller_principal); if let Some(new_controllers) = &controllers - && !new_controllers.contains(&caller_principal) + && !new_controllers.contains(&effective_controller) && !args.force { - warn!("You are about to remove yourself from the controllers list."); - warn!("This will cause you to lose control of the canister and cannot be undone."); + if args.proxy.is_some() { + warn!( + "You are about to remove the proxy canister ({effective_controller}) from the controllers list." + ); + warn!( + "This will prevent further management calls through this proxy and cannot be undone." + ); + } else { + warn!("You are about to remove yourself from the controllers list."); + warn!("This will cause you to lose control of the canister and cannot be undone."); + } let confirmed = Confirm::new() .with_prompt("Do you want to proceed?") @@ -212,81 +236,77 @@ pub(crate) async fn exec(ctx: &Context, args: &UpdateArgs) -> Result<(), anyhow: get_environment_variables(environment_variables_opt, current_status.as_ref()); } - // Update settings. - let mut update = mgmt.update_settings(&cid); - if let Some(controllers) = controllers { - for controller in controllers { - update = update.with_controller(controller); - } - } - if let Some(compute_allocation) = args.compute_allocation { - if configured_settings.compute_allocation.is_some() { - warn!( - "Compute allocation is already set in icp.yaml; this new value will be overridden on next settings sync" - ); - } - update = update.with_compute_allocation(compute_allocation); - } - if let Some(memory_allocation) = &args.memory_allocation { - if configured_settings.memory_allocation.is_some() { - warn!( - "Memory allocation is already set in icp.yaml; this new value will be overridden on next settings sync" - ); - } - update = update.with_memory_allocation(memory_allocation.get()); + // Build settings with warnings for configured values + if args.compute_allocation.is_some() && configured_settings.compute_allocation.is_some() { + warn!( + "Compute allocation is already set in icp.yaml; this new value will be overridden on next settings sync" + ); } - if let Some(freezing_threshold) = &args.freezing_threshold { - if configured_settings.freezing_threshold.is_some() { - warn!( - "Freezing threshold is already set in icp.yaml; this new value will be overridden on next settings sync" - ); - } - update = update.with_freezing_threshold(freezing_threshold.get()); + if args.memory_allocation.is_some() && configured_settings.memory_allocation.is_some() { + warn!( + "Memory allocation is already set in icp.yaml; this new value will be overridden on next settings sync" + ); } - if let Some(reserved_cycles_limit) = &args.reserved_cycles_limit { - if configured_settings.reserved_cycles_limit.is_some() { - warn!( - "Reserved cycles limit is already set in icp.yaml; this new value will be overridden on next settings sync" - ); - } - update = update.with_reserved_cycles_limit(reserved_cycles_limit.get()); + if args.freezing_threshold.is_some() && configured_settings.freezing_threshold.is_some() { + warn!( + "Freezing threshold is already set in icp.yaml; this new value will be overridden on next settings sync" + ); } - if let Some(wasm_memory_limit) = &args.wasm_memory_limit { - if configured_settings.wasm_memory_limit.is_some() { - warn!( - "Wasm memory limit is already set in icp.yaml; this new value will be overridden on next settings sync" - ); - } - update = update.with_wasm_memory_limit(wasm_memory_limit.get()); + if args.reserved_cycles_limit.is_some() && configured_settings.reserved_cycles_limit.is_some() { + warn!( + "Reserved cycles limit is already set in icp.yaml; this new value will be overridden on next settings sync" + ); } - if let Some(wasm_memory_threshold) = &args.wasm_memory_threshold { - if configured_settings.wasm_memory_threshold.is_some() { - warn!( - "Wasm memory threshold is already set in icp.yaml; this new value will be overridden on next settings sync" - ); - } - update = update.with_wasm_memory_threshold(wasm_memory_threshold.get()); + if args.wasm_memory_limit.is_some() && configured_settings.wasm_memory_limit.is_some() { + warn!( + "Wasm memory limit is already set in icp.yaml; this new value will be overridden on next settings sync" + ); } - if let Some(log_memory_limit) = &args.log_memory_limit { - if configured_settings.log_memory_limit.is_some() { - warn!( - "Log memory limit is already set in icp.yaml; this new value will be overridden on next settings sync" - ); - } - update = update.with_log_memory_limit(log_memory_limit.get()); + if args.wasm_memory_threshold.is_some() && configured_settings.wasm_memory_threshold.is_some() { + warn!( + "Wasm memory threshold is already set in icp.yaml; this new value will be overridden on next settings sync" + ); } - if let Some(log_visibility) = log_visibility { - if configured_settings.log_visibility.is_some() { - warn!( - "Log visibility is already set in icp.yaml; this new value will be overridden on next settings sync" - ); - } - update = update.with_log_visibility(log_visibility); + if args.log_memory_limit.is_some() && configured_settings.log_memory_limit.is_some() { + warn!( + "Log memory limit is already set in icp.yaml; this new value will be overridden on next settings sync" + ); } - if let Some(environment_variables) = environment_variables { - update = update.with_environment_variables(environment_variables); + if log_visibility.is_some() && configured_settings.log_visibility.is_some() { + warn!( + "Log visibility is already set in icp.yaml; this new value will be overridden on next settings sync" + ); } - update.await?; + + let settings = CanisterSettings { + controllers, + compute_allocation: args.compute_allocation.map(|v| Nat::from(v as u64)), + memory_allocation: args.memory_allocation.as_ref().map(|m| Nat::from(m.get())), + freezing_threshold: args.freezing_threshold.as_ref().map(|d| Nat::from(d.get())), + reserved_cycles_limit: args + .reserved_cycles_limit + .as_ref() + .map(|r| Nat::from(r.get())), + wasm_memory_limit: args.wasm_memory_limit.as_ref().map(|m| Nat::from(m.get())), + wasm_memory_threshold: args + .wasm_memory_threshold + .as_ref() + .map(|m| Nat::from(m.get())), + log_memory_limit: args.log_memory_limit.as_ref().map(|m| Nat::from(m.get())), + log_visibility, + environment_variables, + }; + + proxy_management::update_settings( + &agent, + args.proxy, + UpdateSettingsArgs { + canister_id: cid, + settings, + sender_canister_version: None, + }, + ) + .await?; Ok(()) } diff --git a/crates/icp-cli/src/commands/canister/snapshot/create.rs b/crates/icp-cli/src/commands/canister/snapshot/create.rs index 0da90594..3ea74fa1 100644 --- a/crates/icp-cli/src/commands/canister/snapshot/create.rs +++ b/crates/icp-cli/src/commands/canister/snapshot/create.rs @@ -1,12 +1,17 @@ +use std::io::stdout; + use anyhow::bail; use byte_unit::{Byte, UnitType}; +use candid::Principal; use clap::Args; -use ic_management_canister_types::{CanisterStatusType, TakeCanisterSnapshotArgs}; -use ic_utils::interfaces::ManagementCanister; +use ic_management_canister_types::{ + CanisterIdRecord, CanisterStatusType, TakeCanisterSnapshotArgs, +}; use icp::context::Context; +use serde::Serialize; use super::SnapshotId; -use crate::{commands::args, operations::misc::format_timestamp}; +use crate::{commands::args, operations::misc::format_timestamp, operations::proxy_management}; /// Create a snapshot of a canister's state #[derive(Debug, Args)] @@ -18,6 +23,18 @@ pub(crate) struct CreateArgs { /// The old snapshot will be deleted once the new one is successfully created. #[arg(long)] replace: Option, + + /// Output command results as JSON + #[arg(long, conflicts_with = "quiet")] + json: bool, + + /// Suppress human-readable output; print only snapshot ID + #[arg(long, short)] + quiet: bool, + + /// Principal of a proxy canister to route the management canister calls through. + #[arg(long)] + proxy: Option, } pub(crate) async fn exec(ctx: &Context, args: &CreateArgs) -> Result<(), anyhow::Error> { @@ -38,11 +55,14 @@ pub(crate) async fn exec(ctx: &Context, args: &CreateArgs) -> Result<(), anyhow: ) .await?; - let mgmt = ManagementCanister::create(&agent); - // Check canister status - must be stopped to create a snapshot let name = &args.cmd_args.canister; - let (status,) = mgmt.canister_status(&cid).await?; + let status = proxy_management::canister_status( + &agent, + args.proxy, + CanisterIdRecord { canister_id: cid }, + ) + .await?; match status.status { CanisterStatusType::Running => { bail!( @@ -62,20 +82,39 @@ pub(crate) async fn exec(ctx: &Context, args: &CreateArgs) -> Result<(), anyhow: sender_canister_version: None, }; - let (snapshot,) = mgmt.take_canister_snapshot(&take_args).await?; - - println!( - "Created snapshot {id} for canister {name} ({cid})", - id = hex::encode(&snapshot.id), - ); - println!( - " Timestamp: {}", - format_timestamp(snapshot.taken_at_timestamp) - ); - println!( - " Size: {}", - Byte::from_u64(snapshot.total_size).get_appropriate_unit(UnitType::Binary) - ); + let snapshot = proxy_management::take_canister_snapshot(&agent, args.proxy, take_args).await?; + if args.json { + serde_json::to_writer( + stdout(), + &JsonSnapshotCreate { + snapshot_id: hex::encode(&snapshot.id), + taken_at_timestamp: snapshot.taken_at_timestamp, + total_size_bytes: snapshot.total_size, + }, + )?; + } else if args.quiet { + println!("{}", hex::encode(&snapshot.id)); + } else { + println!( + "Created snapshot {id} for canister {name} ({cid})", + id = hex::encode(&snapshot.id), + ); + println!( + " Timestamp: {}", + format_timestamp(snapshot.taken_at_timestamp) + ); + println!( + " Size: {}", + Byte::from_u64(snapshot.total_size).get_appropriate_unit(UnitType::Binary) + ); + } Ok(()) } + +#[derive(Serialize)] +struct JsonSnapshotCreate { + snapshot_id: String, + taken_at_timestamp: u64, + total_size_bytes: u64, +} diff --git a/crates/icp-cli/src/commands/canister/snapshot/delete.rs b/crates/icp-cli/src/commands/canister/snapshot/delete.rs index b11d4f54..e31ce320 100644 --- a/crates/icp-cli/src/commands/canister/snapshot/delete.rs +++ b/crates/icp-cli/src/commands/canister/snapshot/delete.rs @@ -1,11 +1,11 @@ +use candid::Principal; use clap::Args; use ic_management_canister_types::DeleteCanisterSnapshotArgs; -use ic_utils::interfaces::ManagementCanister; use icp::context::Context; use tracing::info; use super::SnapshotId; -use crate::commands::args; +use crate::{commands::args, operations::proxy_management}; /// Delete a canister snapshot #[derive(Debug, Args)] @@ -15,6 +15,10 @@ pub(crate) struct DeleteArgs { /// The snapshot ID to delete (hex-encoded) snapshot_id: SnapshotId, + + /// Principal of a proxy canister to route the management canister call through. + #[arg(long)] + proxy: Option, } pub(crate) async fn exec(ctx: &Context, args: &DeleteArgs) -> Result<(), anyhow::Error> { @@ -35,14 +39,12 @@ pub(crate) async fn exec(ctx: &Context, args: &DeleteArgs) -> Result<(), anyhow: ) .await?; - let mgmt = ManagementCanister::create(&agent); - let delete_args = DeleteCanisterSnapshotArgs { canister_id: cid, snapshot_id: args.snapshot_id.0.clone(), }; - mgmt.delete_canister_snapshot(&delete_args).await?; + proxy_management::delete_canister_snapshot(&agent, args.proxy, delete_args).await?; let name = &args.cmd_args.canister; info!( diff --git a/crates/icp-cli/src/commands/canister/snapshot/download.rs b/crates/icp-cli/src/commands/canister/snapshot/download.rs index 35b52940..471a3455 100644 --- a/crates/icp-cli/src/commands/canister/snapshot/download.rs +++ b/crates/icp-cli/src/commands/canister/snapshot/download.rs @@ -1,4 +1,5 @@ use byte_unit::{Byte, UnitType}; +use candid::Principal; use clap::Args; use icp::context::Context; use icp::prelude::*; @@ -29,6 +30,10 @@ pub(crate) struct DownloadArgs { /// Resume a previously interrupted download #[arg(long)] resume: bool, + + /// Principal of a proxy canister to route the management canister calls through. + #[arg(long)] + proxy: Option, } pub(crate) async fn exec(ctx: &Context, args: &DownloadArgs) -> Result<(), anyhow::Error> { @@ -84,7 +89,7 @@ pub(crate) async fn exec(ctx: &Context, args: &DownloadArgs) -> Result<(), anyho id = hex::encode(snapshot_id), ); - let metadata = read_snapshot_metadata(&agent, cid, snapshot_id).await?; + let metadata = read_snapshot_metadata(&agent, args.proxy, cid, snapshot_id).await?; info!( " Timestamp: {}", @@ -119,6 +124,7 @@ pub(crate) async fn exec(ctx: &Context, args: &DownloadArgs) -> Result<(), anyho let pb = create_transfer_progress_bar(metadata.wasm_module_size, "WASM module"); download_blob_to_file( &agent, + args.proxy, cid, snapshot_id, BlobType::WasmModule, @@ -140,6 +146,7 @@ pub(crate) async fn exec(ctx: &Context, args: &DownloadArgs) -> Result<(), anyho let pb = create_transfer_progress_bar(metadata.wasm_memory_size, "WASM memory"); download_blob_to_file( &agent, + args.proxy, cid, snapshot_id, BlobType::WasmMemory, @@ -165,6 +172,7 @@ pub(crate) async fn exec(ctx: &Context, args: &DownloadArgs) -> Result<(), anyho create_transfer_progress_bar(metadata.stable_memory_size, "Stable memory"); download_blob_to_file( &agent, + args.proxy, cid, snapshot_id, BlobType::StableMemory, @@ -193,7 +201,15 @@ pub(crate) async fn exec(ctx: &Context, args: &DownloadArgs) -> Result<(), anyho for chunk_hash in &metadata.wasm_chunk_store { let chunk_path = paths.wasm_chunk_path(&chunk_hash.hash); if !chunk_path.exists() { - download_wasm_chunk(&agent, cid, snapshot_id, chunk_hash, paths).await?; + download_wasm_chunk( + &agent, + args.proxy, + cid, + snapshot_id, + chunk_hash, + paths, + ) + .await?; } } info!("WASM chunks: done"); diff --git a/crates/icp-cli/src/commands/canister/snapshot/list.rs b/crates/icp-cli/src/commands/canister/snapshot/list.rs index 4853b13f..01c1bad6 100644 --- a/crates/icp-cli/src/commands/canister/snapshot/list.rs +++ b/crates/icp-cli/src/commands/canister/snapshot/list.rs @@ -1,15 +1,32 @@ +use std::io::stdout; + use byte_unit::{Byte, UnitType}; +use candid::Principal; use clap::Args; -use ic_utils::interfaces::ManagementCanister; +use ic_management_canister_types::CanisterIdRecord; use icp::context::Context; +use itertools::Itertools; +use serde::Serialize; -use crate::{commands::args, operations::misc::format_timestamp}; +use crate::{commands::args, operations::misc::format_timestamp, operations::proxy_management}; /// List all snapshots for a canister #[derive(Debug, Args)] pub(crate) struct ListArgs { #[command(flatten)] pub(crate) cmd_args: args::CanisterCommandArgs, + + /// Output command results as JSON + #[arg(long, conflicts_with = "quiet")] + pub(crate) json: bool, + + /// Suppress human-readable output; print only snapshot IDs + #[arg(long, short)] + pub(crate) quiet: bool, + + /// Principal of a proxy canister to route the management canister call through. + #[arg(long)] + pub(crate) proxy: Option, } pub(crate) async fn exec(ctx: &Context, args: &ListArgs) -> Result<(), anyhow::Error> { @@ -30,11 +47,35 @@ pub(crate) async fn exec(ctx: &Context, args: &ListArgs) -> Result<(), anyhow::E ) .await?; - let mgmt = ManagementCanister::create(&agent); - - let (snapshots,) = mgmt.list_canister_snapshots(&cid).await?; + let snapshots = proxy_management::list_canister_snapshots( + &agent, + args.proxy, + CanisterIdRecord { canister_id: cid }, + ) + .await?; let name = &args.cmd_args.canister; + if args.json { + serde_json::to_writer( + stdout(), + &JsonSnapshotList { + snapshots: snapshots + .into_iter() + .map(|snapshot| JsonSnapshotListEntry { + snapshot_id: hex::encode(snapshot.id), + taken_at_timestamp: snapshot.taken_at_timestamp, + total_size_bytes: snapshot.total_size, + }) + .collect(), + }, + )?; + return Ok(()); + } else if args.quiet { + println!( + "{}", + snapshots.iter().map(|s| hex::encode(&s.id)).format("\n") + ); + } if snapshots.is_empty() { println!("No snapshots found for canister {name} ({cid})"); } else { @@ -51,3 +92,15 @@ pub(crate) async fn exec(ctx: &Context, args: &ListArgs) -> Result<(), anyhow::E Ok(()) } + +#[derive(Serialize)] +struct JsonSnapshotList { + snapshots: Vec, +} + +#[derive(Serialize)] +struct JsonSnapshotListEntry { + snapshot_id: String, + taken_at_timestamp: u64, + total_size_bytes: u64, +} diff --git a/crates/icp-cli/src/commands/canister/snapshot/restore.rs b/crates/icp-cli/src/commands/canister/snapshot/restore.rs index 22fdec44..bf6ebb8c 100644 --- a/crates/icp-cli/src/commands/canister/snapshot/restore.rs +++ b/crates/icp-cli/src/commands/canister/snapshot/restore.rs @@ -1,12 +1,14 @@ use anyhow::bail; +use candid::Principal; use clap::Args; -use ic_management_canister_types::{CanisterStatusType, LoadCanisterSnapshotArgs}; -use ic_utils::interfaces::ManagementCanister; +use ic_management_canister_types::{ + CanisterIdRecord, CanisterStatusType, LoadCanisterSnapshotArgs, +}; use icp::context::Context; use tracing::info; use super::SnapshotId; -use crate::commands::args; +use crate::{commands::args, operations::proxy_management}; /// Restore a canister from a snapshot #[derive(Debug, Args)] @@ -16,6 +18,10 @@ pub(crate) struct RestoreArgs { /// The snapshot ID to restore (hex-encoded) snapshot_id: SnapshotId, + + /// Principal of a proxy canister to route the management canister calls through. + #[arg(long)] + proxy: Option, } pub(crate) async fn exec(ctx: &Context, args: &RestoreArgs) -> Result<(), anyhow::Error> { @@ -36,11 +42,14 @@ pub(crate) async fn exec(ctx: &Context, args: &RestoreArgs) -> Result<(), anyhow ) .await?; - let mgmt = ManagementCanister::create(&agent); - // Check canister status - must be stopped to restore a snapshot let name = &args.cmd_args.canister; - let (status,) = mgmt.canister_status(&cid).await?; + let status = proxy_management::canister_status( + &agent, + args.proxy, + CanisterIdRecord { canister_id: cid }, + ) + .await?; match status.status { CanisterStatusType::Running => { bail!( @@ -59,7 +68,7 @@ pub(crate) async fn exec(ctx: &Context, args: &RestoreArgs) -> Result<(), anyhow sender_canister_version: None, }; - mgmt.load_canister_snapshot(&load_args).await?; + proxy_management::load_canister_snapshot(&agent, args.proxy, load_args).await?; info!( "Restored canister {name} ({cid}) from snapshot {id}", diff --git a/crates/icp-cli/src/commands/canister/snapshot/upload.rs b/crates/icp-cli/src/commands/canister/snapshot/upload.rs index b808faba..81bdda76 100644 --- a/crates/icp-cli/src/commands/canister/snapshot/upload.rs +++ b/crates/icp-cli/src/commands/canister/snapshot/upload.rs @@ -1,7 +1,11 @@ +use std::io::stdout; + use byte_unit::{Byte, UnitType}; +use candid::Principal; use clap::Args; use icp::context::Context; use icp::prelude::*; +use serde::Serialize; use tracing::info; use super::SnapshotId; @@ -30,6 +34,18 @@ pub(crate) struct UploadArgs { /// Resume a previously interrupted upload #[arg(long)] resume: bool, + + /// Output command results as JSON + #[arg(long)] + json: bool, + + /// Suppress human-readable output; print only snapshot ID + #[arg(long, short, conflicts_with = "json")] + quiet: bool, + + /// Principal of a proxy canister to route the management canister calls through. + #[arg(long)] + proxy: Option, } pub(crate) async fn exec(ctx: &Context, args: &UploadArgs) -> Result<(), anyhow::Error> { @@ -92,7 +108,8 @@ pub(crate) async fn exec(ctx: &Context, args: &UploadArgs) -> Result<(), anyhow: // Upload metadata to create a new snapshot let replace_snapshot = args.replace.as_ref().map(|s| s.0.as_slice()); let result = - upload_snapshot_metadata(&agent, cid, &metadata, replace_snapshot).await?; + upload_snapshot_metadata(&agent, args.proxy, cid, &metadata, replace_snapshot) + .await?; let snapshot_id_hex = hex::encode(&result.snapshot_id); info!("Created snapshot {snapshot_id_hex} for upload"); @@ -112,6 +129,7 @@ pub(crate) async fn exec(ctx: &Context, args: &UploadArgs) -> Result<(), anyhow: let pb = create_transfer_progress_bar(metadata.wasm_module_size, "WASM module"); upload_blob_from_file( &agent, + args.proxy, cid, &snapshot_id_bytes, BlobType::WasmModule, @@ -132,6 +150,7 @@ pub(crate) async fn exec(ctx: &Context, args: &UploadArgs) -> Result<(), anyhow: let pb = create_transfer_progress_bar(metadata.wasm_memory_size, "WASM memory"); upload_blob_from_file( &agent, + args.proxy, cid, &snapshot_id_bytes, BlobType::WasmMemory, @@ -153,6 +172,7 @@ pub(crate) async fn exec(ctx: &Context, args: &UploadArgs) -> Result<(), anyhow: create_transfer_progress_bar(metadata.stable_memory_size, "Stable memory"); upload_blob_from_file( &agent, + args.proxy, cid, &snapshot_id_bytes, BlobType::StableMemory, @@ -177,8 +197,15 @@ pub(crate) async fn exec(ctx: &Context, args: &UploadArgs) -> Result<(), anyhow: for chunk_hash in &metadata.wasm_chunk_store { let hash_hex = hex::encode(&chunk_hash.hash); if !progress.wasm_chunks_uploaded.contains(&hash_hex) { - upload_wasm_chunk(&agent, cid, &snapshot_id_bytes, &chunk_hash.hash, paths) - .await?; + upload_wasm_chunk( + &agent, + args.proxy, + cid, + &snapshot_id_bytes, + &chunk_hash.hash, + paths, + ) + .await?; progress.wasm_chunks_uploaded.insert(hash_hex); save_upload_progress(&progress, paths)?; } @@ -189,7 +216,18 @@ pub(crate) async fn exec(ctx: &Context, args: &UploadArgs) -> Result<(), anyhow: // Clean up progress file on success delete_upload_progress(paths)?; - println!("Snapshot {} uploaded successfully", progress.snapshot_id); + if args.json { + serde_json::to_writer( + stdout(), + &JsonSnapshotUpload { + snapshot_id: progress.snapshot_id.clone(), + }, + )?; + } else if args.quiet { + println!("{}", progress.snapshot_id); + } else { + println!("Snapshot {} uploaded successfully", progress.snapshot_id); + } Ok::<_, anyhow::Error>(progress.snapshot_id) }) @@ -199,3 +237,8 @@ pub(crate) async fn exec(ctx: &Context, args: &UploadArgs) -> Result<(), anyhow: Ok(()) } + +#[derive(Serialize)] +struct JsonSnapshotUpload { + snapshot_id: String, +} diff --git a/crates/icp-cli/src/commands/canister/start.rs b/crates/icp-cli/src/commands/canister/start.rs index 72d44070..4278cc7f 100644 --- a/crates/icp-cli/src/commands/canister/start.rs +++ b/crates/icp-cli/src/commands/canister/start.rs @@ -1,13 +1,19 @@ +use candid::Principal; use clap::Args; +use ic_management_canister_types::CanisterIdRecord; use icp::context::Context; -use crate::commands::args; +use crate::{commands::args, operations::proxy_management}; /// Start a canister on a network #[derive(Debug, Args)] pub(crate) struct StartArgs { #[command(flatten)] pub(crate) cmd_args: args::CanisterCommandArgs, + + /// Principal of a proxy canister to route the management canister call through. + #[arg(long)] + pub(crate) proxy: Option, } pub(crate) async fn exec(ctx: &Context, args: &StartArgs) -> Result<(), anyhow::Error> { @@ -27,11 +33,8 @@ pub(crate) async fn exec(ctx: &Context, args: &StartArgs) -> Result<(), anyhow:: ) .await?; - // Management Interface - let mgmt = ic_utils::interfaces::ManagementCanister::create(&agent); - - // Instruct management canister to start canister - mgmt.start_canister(&cid).await?; + proxy_management::start_canister(&agent, args.proxy, CanisterIdRecord { canister_id: cid }) + .await?; Ok(()) } diff --git a/crates/icp-cli/src/commands/canister/status.rs b/crates/icp-cli/src/commands/canister/status.rs index ca46d4c0..18412e1c 100644 --- a/crates/icp-cli/src/commands/canister/status.rs +++ b/crates/icp-cli/src/commands/canister/status.rs @@ -1,7 +1,9 @@ use anyhow::{anyhow, bail}; use clap::Args; use ic_agent::{Agent, AgentError, export::Principal}; -use ic_management_canister_types::{CanisterStatusResult, EnvironmentVariable, LogVisibility}; +use ic_management_canister_types::{ + CanisterIdRecord, CanisterStatusResult, EnvironmentVariable, LogVisibility, +}; use icp::{ context::{CanisterSelection, Context, EnvironmentSelection, NetworkSelection}, identity::IdentitySelection, @@ -10,7 +12,11 @@ use serde::Serialize; use std::fmt::Write; use tracing::debug; -use crate::{commands::args, options}; +use crate::{ + commands::args, + operations::{proxy::UpdateOrProxyError, proxy_management}, + options, +}; /// Error code returned by the replica if the target canister is not found const E_CANISTER_NOT_FOUND: &str = "IC0301"; @@ -56,6 +62,10 @@ pub(crate) struct StatusArgsOptions { /// looks up public information from the state tree. #[arg(short, long)] pub public: bool, + + /// Principal of a proxy canister to route the management canister call through. + #[arg(long)] + pub proxy: Option, } /// Fetch the list of canister ids from the id_store @@ -203,9 +213,6 @@ pub(crate) async fn exec(ctx: &Context, args: &StatusArgs) -> Result<(), anyhow: ) .await?; - // Management Interface - let mgmt = ic_utils::interfaces::ManagementCanister::create(&agent); - for (i, (maybe_name, cid)) in cids.iter().enumerate() { let output = match args.options.public { true => { @@ -222,8 +229,14 @@ pub(crate) async fn exec(ctx: &Context, args: &StatusArgs) -> Result<(), anyhow: } false => { // Retrieve canister status from management canister - match mgmt.canister_status(cid).await { - Ok((result,)) => { + match proxy_management::canister_status( + &agent, + args.options.proxy, + CanisterIdRecord { canister_id: *cid }, + ) + .await + { + Ok(result) => { let status = SerializableCanisterStatusResult::from( cid.to_owned(), maybe_name.clone(), @@ -237,17 +250,18 @@ pub(crate) async fn exec(ctx: &Context, args: &StatusArgs) -> Result<(), anyhow: .expect("Failed to build canister status output"), } } - Err(AgentError::UncertifiedReject { - reject, - operation: _, + Err(UpdateOrProxyError::DirectUpdateCall { + source: + AgentError::UncertifiedReject { + reject, + operation: _, + }, }) => { if reject.error_code.as_deref() == Some(E_CANISTER_NOT_FOUND) { - // The canister does not exist bail!("Canister {cid} was not found."); } if reject.error_code.as_deref() != Some(E_NOT_A_CONTROLLER) { - // We don't know this error code bail!( "Error looking up canister {cid}: {:?} - {}", reject.error_code, diff --git a/crates/icp-cli/src/commands/canister/stop.rs b/crates/icp-cli/src/commands/canister/stop.rs index b8822a99..0a10bb0f 100644 --- a/crates/icp-cli/src/commands/canister/stop.rs +++ b/crates/icp-cli/src/commands/canister/stop.rs @@ -1,13 +1,19 @@ +use candid::Principal; use clap::Args; +use ic_management_canister_types::CanisterIdRecord; use icp::context::Context; -use crate::commands::args; +use crate::{commands::args, operations::proxy_management}; /// Stop a canister on a network #[derive(Debug, Args)] pub(crate) struct StopArgs { #[command(flatten)] pub(crate) cmd_args: args::CanisterCommandArgs, + + /// Principal of a proxy canister to route the management canister call through. + #[arg(long)] + pub(crate) proxy: Option, } pub(crate) async fn exec(ctx: &Context, args: &StopArgs) -> Result<(), anyhow::Error> { @@ -27,11 +33,8 @@ pub(crate) async fn exec(ctx: &Context, args: &StopArgs) -> Result<(), anyhow::E ) .await?; - // Management Interface - let mgmt = ic_utils::interfaces::ManagementCanister::create(&agent); - - // Instruct management canister to stop canister - mgmt.stop_canister(&cid).await?; + proxy_management::stop_canister(&agent, args.proxy, CanisterIdRecord { canister_id: cid }) + .await?; Ok(()) } diff --git a/crates/icp-cli/src/commands/cycles/balance.rs b/crates/icp-cli/src/commands/cycles/balance.rs index ae59edc3..ab7a1abd 100644 --- a/crates/icp-cli/src/commands/cycles/balance.rs +++ b/crates/icp-cli/src/commands/cycles/balance.rs @@ -1,7 +1,10 @@ +use std::io::stdout; + use bigdecimal::BigDecimal; use clap::Args; use icp::context::Context; use icp_canister_interfaces::cycles_ledger::CYCLES_LEDGER_PRINCIPAL; +use serde::Serialize; use crate::commands::args::TokenCommandArgs; use crate::commands::parsers::parse_subaccount; @@ -17,6 +20,14 @@ pub(crate) struct BalanceArgs { /// The subaccount to check the balance for #[arg(long, value_parser = parse_subaccount)] pub(crate) subaccount: Option<[u8; 32]>, + + /// Output command results as JSON + #[arg(long, conflicts_with = "quiet")] + pub(crate) json: bool, + + /// Suppress human-readable output; print only the balance + #[arg(long, short)] + pub(crate) quiet: bool, } pub(crate) async fn exec(ctx: &Context, args: &BalanceArgs) -> Result<(), anyhow::Error> { @@ -38,8 +49,23 @@ pub(crate) async fn exec(ctx: &Context, args: &BalanceArgs) -> Result<(), anyhow symbol: "cycles".to_string(), }; - // Output information - println!("Balance: {cycles_amount}"); + if args.json { + serde_json::to_writer( + stdout(), + &JsonBalance { + balance: cycles_amount.to_string(), + }, + )?; + } else if args.quiet { + println!("{cycles_amount}"); + } else { + println!("Balance: {cycles_amount}"); + } Ok(()) } + +#[derive(Serialize)] +struct JsonBalance { + balance: String, +} diff --git a/crates/icp-cli/src/commands/cycles/mint.rs b/crates/icp-cli/src/commands/cycles/mint.rs index cc2acf1a..901598d0 100644 --- a/crates/icp-cli/src/commands/cycles/mint.rs +++ b/crates/icp-cli/src/commands/cycles/mint.rs @@ -1,8 +1,11 @@ +use std::io::stdout; + use anyhow::bail; use bigdecimal::BigDecimal; use clap::Args; use icp::context::Context; use icp::parsers::{CyclesAmount, parse_token_amount}; +use serde::Serialize; use crate::commands::args::TokenCommandArgs; use crate::commands::parsers::parse_subaccount; @@ -31,6 +34,10 @@ pub(crate) struct MintArgs { #[command(flatten)] pub(crate) token_command_args: TokenCommandArgs, + + /// Output command results as JSON + #[arg(long)] + pub(crate) json: bool, } pub(crate) async fn exec(ctx: &Context, args: &MintArgs) -> Result<(), anyhow::Error> { @@ -60,11 +67,26 @@ pub(crate) async fn exec(ctx: &Context, args: &MintArgs) -> Result<(), anyhow::E ) .await?; - // Display results - println!( - "Minted {} to your account, new balance: {}.", - mint_info.deposited, mint_info.new_balance - ); + if args.json { + serde_json::to_writer( + stdout(), + &JsonMint { + deposited: mint_info.deposited.to_string(), + new_balance: mint_info.new_balance.to_string(), + }, + )?; + } else { + println!( + "Minted {} to your account, new balance: {}.", + mint_info.deposited, mint_info.new_balance + ); + } Ok(()) } + +#[derive(Serialize)] +struct JsonMint { + deposited: String, + new_balance: String, +} diff --git a/crates/icp-cli/src/commands/cycles/transfer.rs b/crates/icp-cli/src/commands/cycles/transfer.rs index af91f9d5..00ae0a8c 100644 --- a/crates/icp-cli/src/commands/cycles/transfer.rs +++ b/crates/icp-cli/src/commands/cycles/transfer.rs @@ -1,9 +1,12 @@ +use std::io::stdout; + use anyhow::ensure; use clap::Args; use icp::context::Context; use icp::parsers::CyclesAmount; use icp_canister_interfaces::cycles_ledger::{CYCLES_LEDGER_BLOCK_FEE, CYCLES_LEDGER_PRINCIPAL}; use icrc_ledger_types::icrc1::account::Account; +use serde::Serialize; use crate::commands::args::TokenCommandArgs; use crate::commands::parsers::parse_subaccount; @@ -29,6 +32,14 @@ pub(crate) struct TransferArgs { #[command(flatten)] pub(crate) token_command_args: TokenCommandArgs, + + /// Output command results as JSON + #[arg(long, conflicts_with = "quiet")] + pub(crate) json: bool, + + /// Suppress human-readable output; print only the block index + #[arg(long, short)] + pub(crate) quiet: bool, } pub(crate) async fn exec(ctx: &Context, args: &TransferArgs) -> Result<(), anyhow::Error> { @@ -64,11 +75,26 @@ pub(crate) async fn exec(ctx: &Context, args: &TransferArgs) -> Result<(), anyho ) .await?; - // Output information - println!( - "Transferred {} to {} in block {}", - transfer_info.transferred, transfer_info.receiver_display, transfer_info.block_index - ); + if args.json { + serde_json::to_writer( + stdout(), + &JsonTransfer { + block_index: transfer_info.block_index.to_string(), + }, + )?; + } else if args.quiet { + println!("{}", transfer_info.block_index); + } else { + println!( + "Transferred {} to {} in block {}", + transfer_info.transferred, transfer_info.receiver_display, transfer_info.block_index + ); + } Ok(()) } + +#[derive(Serialize)] +struct JsonTransfer { + block_index: String, +} diff --git a/crates/icp-cli/src/commands/deploy.rs b/crates/icp-cli/src/commands/deploy.rs index 330e3078..deb36de8 100644 --- a/crates/icp-cli/src/commands/deploy.rs +++ b/crates/icp-cli/src/commands/deploy.rs @@ -14,12 +14,12 @@ use serde::Serialize; use tracing::info; use crate::{ - commands::canister::create, + commands::{args::ArgsOpt, canister::create}, operations::{ binding_env_vars::set_binding_env_vars_many, build::build_many_with_progress_bar, candid_compat::check_candid_compatibility_many, - create::CreateOperation, + create::{CreateOperation, CreateTarget}, install::{install_many, resolve_install_mode_and_status}, settings::sync_settings_many, sync::sync_many, @@ -30,6 +30,19 @@ use crate::{ /// Deploy a project to an environment #[derive(Args, Debug)] +#[command(after_long_help = "\ +When deploying a single canister, you can pass arguments to the install call +using --args or --args-file: + + # Pass inline Candid arguments + icp deploy my_canister --args '(42 : nat)' + + # Pass arguments from a file + icp deploy my_canister --args-file ./args.did + + # Pass raw bytes + icp deploy my_canister --args-file ./args.bin --args-format bin +")] pub(crate) struct DeployArgs { /// Canister names pub(crate) names: Vec, @@ -39,9 +52,13 @@ pub(crate) struct DeployArgs { pub(crate) mode: String, /// The subnet to use for the canisters being deployed. - #[clap(long)] + #[clap(long, conflicts_with = "proxy")] pub(crate) subnet: Option, + /// Principal of a proxy canister to route management canister calls through. + #[arg(long, conflicts_with = "subnet")] + pub(crate) proxy: Option, + /// One or more controllers for the canisters being deployed. Repeat `--controller` to specify multiple. #[arg(long)] pub(crate) controller: Vec, @@ -60,6 +77,15 @@ pub(crate) struct DeployArgs { #[command(flatten)] pub(crate) environment: EnvironmentOpt, + + /// Output command results as JSON + #[arg(long)] + pub(crate) json: bool, + + /// Arguments to pass to the canister on install. + /// Only valid when deploying a single canister. Takes priority over `init_args` in the manifest. + #[command(flatten)] + pub(crate) args_opt: ArgsOpt, } pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), anyhow::Error> { @@ -81,6 +107,10 @@ pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), anyhow: return Ok(()); } + if args.args_opt.is_some() && cnames.len() != 1 { + anyhow::bail!("--args and --args-file can only be used when deploying a single canister"); + } + let canisters_to_build = try_join_all( cnames .iter() @@ -123,9 +153,14 @@ pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), anyhow: if canisters_to_create.is_empty() { info!("All canisters already exist"); } else { + let target = match (args.subnet, args.proxy) { + (Some(subnet), _) => CreateTarget::Subnet(subnet), + (_, Some(proxy)) => CreateTarget::Proxy(proxy), + _ => CreateTarget::None, + }; let create_operation = CreateOperation::new( agent.clone(), - args.subnet, + target, args.cycles.get(), existing_canisters.into_values().collect(), ); @@ -157,7 +192,9 @@ pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), anyhow: let canister_name = canisters_to_create .get(idx) .expect("should have tried to create every canister"); - println!("Created canister {canister_name} with ID {id}"); + if !args.json { + println!("Created canister {canister_name} with ID {id}"); + } ctx.set_canister_id_for_env(canister_name, id, &environment_selection) .await .map_err(|e| anyhow!(e))?; @@ -207,6 +244,7 @@ pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), anyhow: set_binding_env_vars_many( agent.clone(), + args.proxy, &env.name, target_canisters.clone(), canister_list, @@ -215,7 +253,7 @@ pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), anyhow: .await .map_err(|e| anyhow!(e))?; - sync_settings_many(agent.clone(), target_canisters, ctx.debug) + sync_settings_many(agent.clone(), args.proxy, target_canisters, ctx.debug) .await .map_err(|e| anyhow!(e))?; @@ -234,17 +272,22 @@ pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), anyhow: .map_err(|e| anyhow!(e))?; let (mode, status) = - resolve_install_mode_and_status(&agent, name, &cid, &args.mode).await?; + resolve_install_mode_and_status(&agent, args.proxy, name, &cid, &args.mode).await?; let env = ctx.get_environment(&environment_selection).await?; let (_canister_path, canister_info) = env.get_canister_info(name).map_err(|e| anyhow!(e))?; - let init_args_bytes = canister_info - .init_args - .as_ref() - .map(|ia| ia.to_bytes()) - .transpose()?; + // CLI --args/--args-file take priority over manifest init_args + let init_args_bytes = if args.args_opt.is_some() { + args.args_opt.resolve_bytes()? + } else { + canister_info + .init_args + .as_ref() + .map(|ia| ia.to_bytes()) + .transpose()? + }; Ok::<_, anyhow::Error>((name.clone(), cid, mode, status, init_args_bytes)) } @@ -267,7 +310,14 @@ pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), anyhow: info!("Installing canisters:"); - install_many(agent.clone(), canisters, ctx.artifacts.clone(), ctx.debug).await?; + install_many( + agent.clone(), + args.proxy, + canisters, + ctx.artifacts.clone(), + ctx.debug, + ) + .await?; // Sync the selected canisters @@ -305,13 +355,25 @@ pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), anyhow: if sync_canisters.is_empty() { info!("No canisters have sync steps configured"); } else { + // TODO: When `--proxy` is used and the canister was newly created, the proxy + // canister is its only controller. Sync steps (e.g. asset uploads to a frontend + // canister) will fail because the user's identity lacks the required permissions. + // The fix is to make a proxy call to the frontend canister's `grant_permission` + // method to permit the user identity to upload assets directly before syncing. info!("Syncing canisters:"); sync_many(ctx.syncer.clone(), agent.clone(), sync_canisters, ctx.debug).await?; } // Print URLs for deployed canisters - print_canister_urls(ctx, &environment_selection, agent.clone(), &cnames).await?; + print_canister_urls( + ctx, + &environment_selection, + agent.clone(), + &cnames, + args.json, + ) + .await?; Ok(()) } @@ -353,6 +415,7 @@ async fn print_canister_urls( environment_selection: &EnvironmentSelection, agent: Agent, canister_names: &[String], + json: bool, ) -> Result<(), anyhow::Error> { use icp::network::custom_domains::{canister_gateway_url, gateway_domain}; @@ -369,7 +432,11 @@ async fn print_canister_urls( } }; - println!("Deployed canisters:"); + let mut json_canisters = Vec::new(); + + if !json { + println!("Deployed canisters:"); + } for name in canister_names { let canister_id = match ctx @@ -393,10 +460,18 @@ async fn print_canister_urls( if has_http { let canister_url = canister_gateway_url(http_gateway_url, canister_id, friendly); - println!(" {name}: {canister_url}"); + if json { + json_canisters.push(JsonDeployedCanister { + name: name.clone(), + canister_id, + url: Some(canister_url.to_string()), + }); + } else { + println!(" {name}: {canister_url}"); + } } else { // For canisters without http_request, show the Candid UI URL - if let Some(ui_id) = get_candid_ui_id(ctx, environment_selection).await { + let url = if let Some(ui_id) = get_candid_ui_id(ctx, environment_selection).await { let domain = gateway_domain(http_gateway_url); let mut candid_url = canister_gateway_url(http_gateway_url, ui_id, None); if domain.is_some() { @@ -404,19 +479,59 @@ async fn print_canister_urls( } else { candid_url.set_query(Some(&format!("canisterId={ui_id}&id={canister_id}"))); } - println!(" {name} (Candid UI): {candid_url}"); + if !json { + println!(" {name} (Candid UI): {candid_url}"); + } + Some(candid_url.to_string()) } else { - println!(" {name}: {canister_id} (Candid UI not available)"); + if !json { + println!(" {name}: {canister_id} (Candid UI not available)"); + } + None + }; + if json { + json_canisters.push(JsonDeployedCanister { + name: name.clone(), + canister_id, + url, + }); } } + } else if json { + json_canisters.push(JsonDeployedCanister { + name: name.clone(), + canister_id, + url: None, + }); } else { println!(" {name}: {canister_id} (No gateway URL available)"); } } + if json { + serde_json::to_writer( + std::io::stdout(), + &JsonDeploy { + canisters: json_canisters, + }, + )?; + } + Ok(()) } +#[derive(Serialize)] +struct JsonDeploy { + canisters: Vec, +} + +#[derive(Serialize)] +struct JsonDeployedCanister { + name: String, + canister_id: Principal, + url: Option, +} + /// Gets the Candid UI canister ID for the network /// Returns None if the Candid UI ID cannot be determined async fn get_candid_ui_id( diff --git a/crates/icp-cli/src/commands/identity/default.rs b/crates/icp-cli/src/commands/identity/default.rs index d6b617c0..bf81c1e4 100644 --- a/crates/icp-cli/src/commands/identity/default.rs +++ b/crates/icp-cli/src/commands/identity/default.rs @@ -3,7 +3,7 @@ use icp::context::Context; use icp::identity::manifest::{IdentityDefaults, IdentityList, change_default_identity}; use tracing::info; -/// Display the currently selected identity +/// Display or set the currently selected identity #[derive(Debug, Args)] pub(crate) struct DefaultArgs { /// Identity to set as default. If omitted, prints the current default. diff --git a/crates/icp-cli/src/commands/identity/list.rs b/crates/icp-cli/src/commands/identity/list.rs index 545f3908..723b1a1a 100644 --- a/crates/icp-cli/src/commands/identity/list.rs +++ b/crates/icp-cli/src/commands/identity/list.rs @@ -1,14 +1,26 @@ +use std::io::stdout; + +use candid::Principal; use clap::Args; use icp::identity::manifest::{IdentityDefaults, IdentityList}; use itertools::Itertools; +use serde::Serialize; use icp::context::Context; /// List the identities #[derive(Debug, Args)] -pub(crate) struct ListArgs; +pub(crate) struct ListArgs { + /// Output command results as JSON + #[arg(long, conflicts_with = "quiet")] + pub(crate) json: bool, + + /// Suppress human-readable output; print only identity names + #[arg(long, short)] + pub(crate) quiet: bool, +} -pub(crate) async fn exec(ctx: &Context, _: &ListArgs) -> Result<(), anyhow::Error> { +pub(crate) async fn exec(ctx: &Context, args: &ListArgs) -> Result<(), anyhow::Error> { let dirs = ctx.dirs.identity()?.into_read().await?; let list = IdentityList::load_from(dirs.as_ref())?; @@ -22,6 +34,30 @@ pub(crate) async fn exec(ctx: &Context, _: &ListArgs) -> Result<(), anyhow::Erro .rev() .collect::>(); + if args.json { + serde_json::to_writer( + stdout(), + &JsonIdentityList { + default_identity: defaults.default.clone(), + identities: sorted_identities + .iter() + .map(|(name, id)| JsonIdentity { + name: name.to_string(), + principal: id.principal(), + }) + .collect(), + }, + )?; + return Ok(()); + } + + if args.quiet { + for (name, _) in &sorted_identities { + println!("{name}"); + } + return Ok(()); + } + let longest_identity_name_length = sorted_identities .iter() .map(|(name, _)| name.len()) @@ -40,3 +76,15 @@ pub(crate) async fn exec(ctx: &Context, _: &ListArgs) -> Result<(), anyhow::Erro Ok(()) } + +#[derive(Serialize)] +struct JsonIdentityList { + default_identity: String, + identities: Vec, +} + +#[derive(Serialize)] +struct JsonIdentity { + name: String, + principal: Principal, +} diff --git a/crates/icp-cli/src/commands/identity/new.rs b/crates/icp-cli/src/commands/identity/new.rs index 218c48af..7dd3ee90 100644 --- a/crates/icp-cli/src/commands/identity/new.rs +++ b/crates/icp-cli/src/commands/identity/new.rs @@ -1,3 +1,5 @@ +use std::io::stdout; + use anyhow::Context as _; use bip39::{Language, Mnemonic, MnemonicType}; use clap::Args; @@ -13,6 +15,7 @@ use icp::{ }; use icp::context::Context; +use serde::Serialize; use tracing::{info, warn}; use crate::commands::identity::StorageMode; @@ -34,6 +37,14 @@ pub(crate) struct NewArgs { /// Write the seed phrase to a file instead of printing to stdout #[arg(long, value_name = "FILE")] output_seed: Option, + + /// Output command results as JSON + #[arg(long, conflicts_with = "quiet")] + json: bool, + + /// Suppress human-readable output; print only the seed phrase + #[arg(long, short)] + quiet: bool, } pub(crate) async fn exec(ctx: &Context, args: &NewArgs) -> Result<(), anyhow::Error> { @@ -95,9 +106,25 @@ pub(crate) async fn exec(ctx: &Context, args: &NewArgs) -> Result<(), anyhow::Er warn!( "Write the seed phrase down and store it in a secure location. If you lose it, you will lose access to your identity." ); - println!("Your seed phrase: {mnemonic}"); + if args.json { + serde_json::to_writer( + stdout(), + &JsonNew { + seed_phrase: mnemonic.to_string(), + }, + )?; + } else if args.quiet { + println!("{mnemonic}"); + } else { + println!("Your seed phrase: {mnemonic}"); + } } } Ok(()) } + +#[derive(Serialize)] +struct JsonNew { + seed_phrase: String, +} diff --git a/crates/icp-cli/src/commands/network/start.rs b/crates/icp-cli/src/commands/network/start.rs index af1c1804..55c8e6d0 100644 --- a/crates/icp-cli/src/commands/network/start.rs +++ b/crates/icp-cli/src/commands/network/start.rs @@ -15,7 +15,7 @@ use icp::{ }, settings::Settings, }; -use tracing::{debug, info}; +use tracing::{debug, info, warn}; use super::args::NetworkOrEnvironmentArgs; use icp::context::Context; @@ -78,8 +78,23 @@ pub(crate) async fn exec(ctx: &Context, args: &StartArgs) -> Result<(), anyhow:: nd.ensure_exists() .context("failed to create network directory")?; - if nd.load_network_descriptor().await?.is_some() { - bail!("network '{}' is already running", network.name); + if let Some(descriptor) = nd.load_network_descriptor().await? { + debug!( + "Found network descriptor for {} in: {}", + nd.network_name, nd.network_root + ); + if descriptor.child_locator.is_alive().await { + bail!("network '{}' is already running", network.name); + } else { + warn!( + "Found stale network descriptor for '{}' (process is no longer running). \ + Cleaning up and starting fresh.", + network.name + ); + nd.cleanup_port_descriptor(descriptor.gateway_port()) + .await?; + nd.cleanup_project_network_descriptor().await?; + } } // Clean up any existing canister ID mappings of which environment is on this network diff --git a/crates/icp-cli/src/commands/token/balance.rs b/crates/icp-cli/src/commands/token/balance.rs index 0f913e41..5651a3a7 100644 --- a/crates/icp-cli/src/commands/token/balance.rs +++ b/crates/icp-cli/src/commands/token/balance.rs @@ -1,5 +1,8 @@ +use std::io::stdout; + use clap::Args; use icp::context::Context; +use serde::Serialize; use crate::commands::args::TokenCommandArgs; use crate::commands::parsers::parse_subaccount; @@ -15,6 +18,14 @@ pub(crate) struct BalanceArgs { /// The subaccount to check the balance for #[arg(long, value_parser = parse_subaccount)] pub(crate) subaccount: Option<[u8; 32]>, + + /// Output command results as JSON + #[arg(long, conflicts_with = "quiet")] + pub(crate) json: bool, + + /// Suppress human-readable output; print only the balance + #[arg(long, short)] + pub(crate) quiet: bool, } /// Check the token balance of a given identity @@ -41,8 +52,23 @@ pub(crate) async fn exec( // Get the balance from the ledger let balance = get_balance(&agent, args.subaccount, token).await?; - // Output information - println!("Balance: {balance}"); + if args.json { + serde_json::to_writer( + stdout(), + &JsonBalance { + balance: balance.to_string(), + }, + )?; + } else if args.quiet { + println!("{balance}"); + } else { + println!("Balance: {balance}"); + } Ok(()) } + +#[derive(Serialize)] +struct JsonBalance { + balance: String, +} diff --git a/crates/icp-cli/src/commands/token/transfer.rs b/crates/icp-cli/src/commands/token/transfer.rs index 843caf0a..8edaff18 100644 --- a/crates/icp-cli/src/commands/token/transfer.rs +++ b/crates/icp-cli/src/commands/token/transfer.rs @@ -1,7 +1,10 @@ +use std::io::stdout; + use bigdecimal::BigDecimal; use clap::Args; use icp::context::Context; use icp::parsers::parse_token_amount; +use serde::Serialize; use crate::commands::args::{FlexibleAccountId, TokenCommandArgs}; use crate::commands::parsers::parse_subaccount; @@ -30,6 +33,14 @@ pub(crate) struct TransferArgs { #[command(flatten)] pub(crate) token_command_args: TokenCommandArgs, + + /// Output command results as JSON + #[arg(long, conflicts_with = "quiet")] + pub(crate) json: bool, + + /// Suppress human-readable output; print only the block index + #[arg(long, short)] + pub(crate) quiet: bool, } pub(crate) async fn exec( @@ -65,11 +76,26 @@ pub(crate) async fn exec( let transfer_info = transfer(&agent, args.from_subaccount, token, &args.amount, &receiver).await?; - // Output information - println!( - "Transferred {} to {} in block {}", - transfer_info.transferred, transfer_info.receiver_display, transfer_info.block_index - ); + if args.json { + serde_json::to_writer( + stdout(), + &JsonTransfer { + block_index: transfer_info.block_index.to_string(), + }, + )?; + } else if args.quiet { + println!("{}", transfer_info.block_index); + } else { + println!( + "Transferred {} to {} in block {}", + transfer_info.transferred, transfer_info.receiver_display, transfer_info.block_index + ); + } Ok(()) } + +#[derive(Serialize)] +struct JsonTransfer { + block_index: String, +} diff --git a/crates/icp-cli/src/operations/binding_env_vars.rs b/crates/icp-cli/src/operations/binding_env_vars.rs index 668a17ee..88eff87f 100644 --- a/crates/icp-cli/src/operations/binding_env_vars.rs +++ b/crates/icp-cli/src/operations/binding_env_vars.rs @@ -1,16 +1,17 @@ use std::collections::{BTreeMap, HashSet}; use futures::{StreamExt, stream::FuturesOrdered}; -use ic_agent::{Agent, AgentError, export::Principal}; -use ic_utils::interfaces::{ - ManagementCanister, management_canister::builders::EnvironmentVariable, -}; +use ic_agent::{Agent, export::Principal}; +use ic_management_canister_types::{CanisterSettings, EnvironmentVariable, UpdateSettingsArgs}; use icp::Canister; use snafu::Snafu; use tracing::error; use crate::progress::{ProgressManager, ProgressManagerSettings}; +use super::proxy::UpdateOrProxyError; +use super::proxy_management; + #[derive(Debug, Snafu)] pub enum BindingEnvVarsOperationError { #[snafu(display("Could not find canister id(s) for {} in environment '{environment}'. Make sure they are created first", canister_names.join(", ")))] @@ -19,8 +20,8 @@ pub enum BindingEnvVarsOperationError { canister_names: Vec, }, - #[snafu(display("agent error: {source}"))] - Agent { source: AgentError }, + #[snafu(transparent)] + UpdateOrProxy { source: UpdateOrProxyError }, } #[derive(Debug, Snafu)] @@ -37,7 +38,8 @@ struct BindingEnvVarsFailure { } pub(crate) async fn set_env_vars_for_canister( - mgmt: &ManagementCanister<'_>, + agent: &Agent, + proxy: Option, canister_id: &Principal, canister_info: &Canister, binding_vars: &[(String, String)], @@ -57,10 +59,20 @@ pub(crate) async fn set_env_vars_for_canister( .into_iter() .map(|(name, value)| EnvironmentVariable { name, value }) .collect::>(); - mgmt.update_settings(canister_id) - .with_environment_variables(environment_variables) - .await - .map_err(|source| BindingEnvVarsOperationError::Agent { source })?; + + proxy_management::update_settings( + agent, + proxy, + UpdateSettingsArgs { + canister_id: *canister_id, + settings: CanisterSettings { + environment_variables: Some(environment_variables), + ..Default::default() + }, + sender_canister_version: None, + }, + ) + .await?; Ok(()) } @@ -68,13 +80,12 @@ pub(crate) async fn set_env_vars_for_canister( /// Orchestrates setting environment variables for multiple canisters with progress tracking pub(crate) async fn set_binding_env_vars_many( agent: Agent, + proxy: Option, environment_name: &str, target_canisters: Vec<(Principal, Canister)>, canister_list: BTreeMap, debug: bool, ) -> Result<(), SetBindingEnvVarsManyError> { - let mgmt = ManagementCanister::create(&agent); - // Check that all the canisters in this environment have an id // We need to have all the ids to generate environment variables // for the bindings @@ -118,13 +129,13 @@ pub(crate) async fn set_binding_env_vars_many( let canister_name = info.name.clone(); let settings_fn = { - let mgmt = mgmt.clone(); + let agent = agent.clone(); let pb = pb.clone(); let binding_vars = binding_vars.clone(); async move { pb.set_message("Updating environment variables..."); - set_env_vars_for_canister(&mgmt, &cid, &info, &binding_vars).await + set_env_vars_for_canister(&agent, proxy, &cid, &info, &binding_vars).await } }; diff --git a/crates/icp-cli/src/operations/create.rs b/crates/icp-cli/src/operations/create.rs index c9aa9bf3..e77be871 100644 --- a/crates/icp-cli/src/operations/create.rs +++ b/crates/icp-cli/src/operations/create.rs @@ -1,23 +1,27 @@ +use std::sync::Arc; + use candid::{Decode, Encode, Nat, Principal}; use ic_agent::{ Agent, AgentError, agent::{Subnet, SubnetType}, }; +use ic_management_canister_types::{ + CanisterIdRecord, CanisterSettings, CreateCanisterArgs as MgmtCreateCanisterArgs, +}; use icp_canister_interfaces::{ cycles_ledger::{ CYCLES_LEDGER_PRINCIPAL, CreateCanisterArgs, CreateCanisterResponse, CreationArgs, SubnetSelectionArg, }, cycles_minting_canister::CYCLES_MINTING_CANISTER_PRINCIPAL, - management_canister::{ - CanisterSettingsArg, MgmtCreateCanisterArgs, MgmtCreateCanisterResponse, - }, }; use rand::seq::IndexedRandom; use snafu::{OptionExt, ResultExt, Snafu}; -use std::sync::Arc; use tokio::sync::OnceCell; +use super::proxy::UpdateOrProxyError; +use super::proxy_management; + #[derive(Debug, Snafu)] pub enum CreateOperationError { #[snafu(display("failed to encode candid: {source}"))] @@ -49,11 +53,27 @@ pub enum CreateOperationError { #[snafu(display("failed to resolve subnet: {message}"))] SubnetResolution { message: String }, + + #[snafu(transparent)] + UpdateOrProxyCall { source: UpdateOrProxyError }, +} + +/// Determines how a new canister is created. +pub enum CreateTarget { + /// Create the canister on a specific subnet, chosen by the caller. + Subnet(Principal), + /// Create the canister via a proxy canister. The `create_canister` call is + /// forwarded through the proxy's `proxy` method to the management canister, + /// so the new canister will be placed on the same subnet as the proxy. + Proxy(Principal), + /// No explicit target. The subnet is resolved automatically: either from an + /// existing canister in the project or by picking a random available subnet. + None, } struct CreateOperationInner { agent: Agent, - subnet: Option, + target: CreateTarget, cycles: u128, existing_canisters: Vec, resolved_subnet: OnceCell>, @@ -74,14 +94,14 @@ impl Clone for CreateOperation { impl CreateOperation { pub fn new( agent: Agent, - subnet: Option, + target: CreateTarget, cycles: u128, existing_canisters: Vec, ) -> Self { Self { inner: Arc::new(CreateOperationInner { agent, - subnet, + target, cycles, existing_canisters, resolved_subnet: OnceCell::new(), @@ -95,8 +115,12 @@ impl CreateOperation { /// - `Err(CreateOperationError)` if an error occurred. pub async fn create( &self, - settings: &CanisterSettingsArg, + settings: &CanisterSettings, ) -> Result { + if let CreateTarget::Proxy(proxy) = self.inner.target { + return self.create_proxy(settings, proxy).await; + } + let selected_subnet = self .get_subnet() .await @@ -107,9 +131,7 @@ impl CreateOperation { .get_subnet_by_id(&selected_subnet) .await .context(GetSubnetSnafu)?; - let cid = if let Some(SubnetType::Unknown(kind)) = subnet_info.subnet_type() - && kind == "cloud_engine" - { + let cid = if let Some(SubnetType::CloudEngine) = subnet_info.subnet_type() { self.create_mgmt(settings, &subnet_info).await? } else { self.create_ledger(settings, selected_subnet).await? @@ -119,7 +141,7 @@ impl CreateOperation { async fn create_ledger( &self, - settings: &CanisterSettingsArg, + settings: &CanisterSettings, selected_subnet: Principal, ) -> Result { let creation_args = CreationArgs { @@ -160,7 +182,7 @@ impl CreateOperation { async fn create_mgmt( &self, - settings: &CanisterSettingsArg, + settings: &CanisterSettings, selected_subnet: &Subnet, ) -> Result { let arg = MgmtCreateCanisterArgs { @@ -185,10 +207,31 @@ impl CreateOperation { ) .await .context(AgentSnafu)?; - let resp = Decode!(&resp, MgmtCreateCanisterResponse).context(CandidDecodeSnafu)?; + let resp: CanisterIdRecord = Decode!(&resp, CanisterIdRecord).context(CandidDecodeSnafu)?; Ok(resp.canister_id) } + async fn create_proxy( + &self, + settings: &CanisterSettings, + proxy: Principal, + ) -> Result { + let args = MgmtCreateCanisterArgs { + settings: Some(settings.clone()), + sender_canister_version: None, + }; + + let result = proxy_management::create_canister( + &self.inner.agent, + Some(proxy), + self.inner.cycles, + args, + ) + .await?; + + Ok(result.canister_id) + } + /// 1. If a subnet is explicitly provided, use it /// 2. If no canisters exist yet, pick a random available subnet /// 3. If canisters exist, use the same subnet as the first existing canister @@ -200,7 +243,7 @@ impl CreateOperation { .resolved_subnet .get_or_init(|| async { // If subnet is explicitly provided, use it - if let Some(subnet) = self.inner.subnet { + if let CreateTarget::Subnet(subnet) = self.inner.target { return Ok(subnet); } diff --git a/crates/icp-cli/src/operations/install.rs b/crates/icp-cli/src/operations/install.rs index f66d80ee..0564f3ed 100644 --- a/crates/icp-cli/src/operations/install.rs +++ b/crates/icp-cli/src/operations/install.rs @@ -1,10 +1,10 @@ +use candid::Encode; use futures::{StreamExt, stream::FuturesOrdered}; -use ic_agent::{Agent, AgentError, export::Principal}; +use ic_agent::{Agent, export::Principal}; use ic_management_canister_types::{ - CanisterId, CanisterStatusType, ChunkHash, UpgradeFlags, UploadChunkArgs, WasmMemoryPersistence, -}; -use ic_utils::interfaces::{ - ManagementCanister, management_canister::builders::CanisterInstallMode, + CanisterId, CanisterIdRecord, CanisterInstallMode, CanisterStatusType, ChunkHash, + ClearChunkStoreArgs, InstallChunkedCodeArgs, InstallCodeArgs, UpgradeFlags, UploadChunkArgs, + WasmMemoryPersistence, }; use sha2::{Digest, Sha256}; use snafu::{ResultExt, Snafu}; @@ -14,26 +14,28 @@ use tracing::{debug, error, warn}; use crate::progress::{ProgressManager, ProgressManagerSettings}; use super::misc::fetch_canister_metadata; +use super::proxy::UpdateOrProxyError; +use super::proxy_management; #[derive(Debug, Snafu)] pub enum InstallOperationError { #[snafu(display("Could not find build artifact for canister '{canister_name}'"))] ArtifactNotFound { canister_name: String }, - #[snafu(display("agent error: {source}"))] - Agent { source: AgentError }, - #[snafu(display("Failed to stop canister '{canister_name}' before upgrade"))] StopCanister { canister_name: String, - source: AgentError, + source: UpdateOrProxyError, }, #[snafu(display("Failed to start canister '{canister_name}' after upgrade"))] StartCanister { canister_name: String, - source: AgentError, + source: UpdateOrProxyError, }, + + #[snafu(transparent)] + UpdateOrProxy { source: UpdateOrProxyError }, } #[derive(Debug, Snafu)] @@ -54,24 +56,30 @@ struct InstallFailure { /// determine whether the canister already has code installed. pub(crate) async fn resolve_install_mode_and_status( agent: &Agent, + proxy: Option, canister_name: &str, canister_id: &Principal, mode: &str, ) -> Result<(CanisterInstallMode, CanisterStatusType), ResolveInstallModeError> { - let mgmt = ManagementCanister::create(agent); - let (status,) = mgmt - .canister_status(canister_id) - .await - .context(ResolveInstallModeSnafu { canister_name })?; + let status = proxy_management::canister_status( + agent, + proxy, + CanisterIdRecord { + canister_id: CanisterId::from(*canister_id), + }, + ) + .await + .context(ResolveInstallModeSnafu { canister_name })?; + let canister_status = status.status; match mode { "auto" => Ok(if status.module_hash.is_some() { - (CanisterInstallMode::Upgrade(None), status.status) + (CanisterInstallMode::Upgrade(None), canister_status) } else { - (CanisterInstallMode::Install, status.status) + (CanisterInstallMode::Install, canister_status) }), - "install" => Ok((CanisterInstallMode::Install, status.status)), - "reinstall" => Ok((CanisterInstallMode::Reinstall, status.status)), - "upgrade" => Ok((CanisterInstallMode::Upgrade(None), status.status)), + "install" => Ok((CanisterInstallMode::Install, canister_status)), + "reinstall" => Ok((CanisterInstallMode::Reinstall, canister_status)), + "upgrade" => Ok((CanisterInstallMode::Upgrade(None), canister_status)), _ => panic!("invalid install mode: {mode}"), } } @@ -80,11 +88,12 @@ pub(crate) async fn resolve_install_mode_and_status( #[snafu(display("Failed to resolve install mode for canister {canister_name}"))] pub(crate) struct ResolveInstallModeError { canister_name: String, - source: AgentError, + source: UpdateOrProxyError, } pub(crate) async fn install_canister( agent: &Agent, + proxy: Option, canister_id: &Principal, canister_name: &str, wasm: &[u8], @@ -118,6 +127,7 @@ pub(crate) async fn install_canister( do_install_operation( agent, + proxy, canister_id, canister_name, wasm, @@ -130,6 +140,7 @@ pub(crate) async fn install_canister( async fn do_install_operation( agent: &Agent, + proxy: Option, canister_id: &Principal, canister_name: &str, wasm: &[u8], @@ -137,8 +148,6 @@ async fn do_install_operation( status: CanisterStatusType, init_args: Option<&[u8]>, ) -> Result<(), InstallOperationError> { - let mgmt = ManagementCanister::create(agent); - // Threshold for chunked installation: 2 MB // Raw install_code messages are limited to 2 MiB const CHUNK_THRESHOLD: usize = 2 * 1024 * 1024; @@ -149,34 +158,46 @@ async fn do_install_operation( // Generous overhead for encoding, target canister ID, install mode, etc. const ENCODING_OVERHEAD: usize = 500; + let cid = CanisterId::from(*canister_id); + let arg = init_args + .map(|a| a.to_vec()) + .unwrap_or_else(|| Encode!().unwrap()); + // Calculate total install message size - let init_args_len = init_args.map_or(0, |args| args.len()); - let total_install_size = wasm.len() + init_args_len + ENCODING_OVERHEAD; + let total_install_size = wasm.len() + arg.len() + ENCODING_OVERHEAD; if total_install_size <= CHUNK_THRESHOLD { // Small wasm: use regular install_code debug!("Installing wasm for {canister_name} using install_code"); - let mut builder = mgmt.install_code(canister_id, wasm).with_mode(mode); - - if let Some(args) = init_args { - builder = builder.with_raw_arg(args.into()); - } + let install_args = InstallCodeArgs { + mode, + canister_id: cid, + wasm_module: wasm.to_vec(), + arg, + sender_canister_version: None, + }; - stop_and_start_if_upgrade(&mgmt, canister_id, canister_name, mode, status, async { - builder - .await - .map_err(|source| InstallOperationError::Agent { source }) - }) + stop_and_start_if_upgrade( + agent, + proxy, + canister_id, + canister_name, + mode, + status, + async { + proxy_management::install_code(agent, proxy, install_args).await?; + Ok(()) + }, + ) .await?; } else { // Large wasm: use chunked installation debug!("Installing wasm for {canister_name} using chunked installation"); // Clear any existing chunks to ensure a clean state - mgmt.clear_chunk_store(canister_id) - .await - .map_err(|source| InstallOperationError::Agent { source })?; + proxy_management::clear_chunk_store(agent, proxy, ClearChunkStoreArgs { canister_id: cid }) + .await?; // Split wasm into chunks and upload them let chunks: Vec<&[u8]> = wasm.chunks(CHUNK_SIZE).collect(); @@ -191,14 +212,11 @@ async fn do_install_operation( ); let upload_args = UploadChunkArgs { - canister_id: CanisterId::from(*canister_id), + canister_id: cid, chunk: chunk.to_vec(), }; - let (chunk_hash,) = mgmt - .upload_chunk(canister_id, &upload_args) - .await - .map_err(|source| InstallOperationError::Agent { source })?; + let chunk_hash = proxy_management::upload_chunk(agent, proxy, upload_args).await?; chunk_hashes.push(chunk_hash); } @@ -210,29 +228,38 @@ async fn do_install_operation( debug!("Installing chunked code with {} chunks", chunk_hashes.len()); - // Build and execute install_chunked_code - let mut builder = mgmt - .install_chunked_code(canister_id, &wasm_module_hash) - .with_chunk_hashes(chunk_hashes) - .with_install_mode(mode); - - if let Some(args) = init_args { - builder = builder.with_raw_arg(args.to_vec()); - } + let chunked_args = InstallChunkedCodeArgs { + mode, + target_canister: cid, + store_canister: None, + chunk_hashes_list: chunk_hashes, + wasm_module_hash, + arg, + sender_canister_version: None, + }; - let install_res = - stop_and_start_if_upgrade(&mgmt, canister_id, canister_name, mode, status, async { - builder - .await - .map_err(|source| InstallOperationError::Agent { source }) - }) - .await; + let install_res = stop_and_start_if_upgrade( + agent, + proxy, + canister_id, + canister_name, + mode, + status, + async { + proxy_management::install_chunked_code(agent, proxy, chunked_args).await?; + Ok(()) + }, + ) + .await; // Clear chunk store after successful installation to free up storage - let clear_res = mgmt - .clear_chunk_store(canister_id) - .await - .map_err(|source| InstallOperationError::Agent { source }); + let clear_res = proxy_management::clear_chunk_store( + agent, + proxy, + ClearChunkStoreArgs { canister_id: cid }, + ) + .await + .map_err(InstallOperationError::from); if let Err(clear_error) = clear_res { if let Err(install_error) = install_res { @@ -249,7 +276,8 @@ async fn do_install_operation( } async fn stop_and_start_if_upgrade( - mgmt: &ManagementCanister<'_>, + agent: &Agent, + proxy: Option, canister_id: &Principal, canister_name: &str, mode: CanisterInstallMode, @@ -260,9 +288,12 @@ async fn stop_and_start_if_upgrade( mode, CanisterInstallMode::Upgrade(_) | CanisterInstallMode::Reinstall ) && matches!(status, CanisterStatusType::Running); + let cid_record = CanisterIdRecord { + canister_id: CanisterId::from(*canister_id), + }; // Stop the canister before proceeding if should_guard { - mgmt.stop_canister(canister_id) + proxy_management::stop_canister(agent, proxy, cid_record.clone()) .await .context(StopCanisterSnafu { canister_name })?; } @@ -270,7 +301,7 @@ async fn stop_and_start_if_upgrade( let install_result = f.await; // Restart the canister whether or not the installation succeeded if should_guard { - let start_result = mgmt.start_canister(canister_id).await; + let start_result = proxy_management::start_canister(agent, proxy, cid_record).await; if let Err(start_error) = start_result { // If both install and start failed, report the install error since it's more likely to be the root cause if let Err(install_error) = install_result { @@ -288,6 +319,7 @@ async fn stop_and_start_if_upgrade( /// Installs code to multiple canisters and displays progress bars. pub(crate) async fn install_many( agent: Agent, + proxy: Option, canisters: impl IntoIterator< Item = ( String, @@ -322,6 +354,7 @@ pub(crate) async fn install_many( install_canister( &agent, + proxy, &cid, &name, &wasm, diff --git a/crates/icp-cli/src/operations/mod.rs b/crates/icp-cli/src/operations/mod.rs index cc98ce15..f1d8ba2b 100644 --- a/crates/icp-cli/src/operations/mod.rs +++ b/crates/icp-cli/src/operations/mod.rs @@ -4,6 +4,8 @@ pub(crate) mod candid_compat; pub(crate) mod canister_migration; pub(crate) mod create; pub(crate) mod install; +pub(crate) mod proxy; +pub(crate) mod proxy_management; pub(crate) mod settings; pub(crate) mod snapshot_transfer; pub(crate) mod sync; diff --git a/crates/icp-cli/src/operations/proxy.rs b/crates/icp-cli/src/operations/proxy.rs new file mode 100644 index 00000000..a50f4947 --- /dev/null +++ b/crates/icp-cli/src/operations/proxy.rs @@ -0,0 +1,114 @@ +use candid::utils::{ArgumentDecoder, ArgumentEncoder}; +use candid::{Encode, Nat, Principal}; +use ic_agent::Agent; +use icp_canister_interfaces::proxy::{ProxyArgs, ProxyResult}; +use snafu::{ResultExt, Snafu}; + +#[derive(Debug, Snafu)] +pub enum UpdateOrProxyError { + #[snafu(display("failed to encode proxy call arguments: {source}"))] + ProxyEncode { source: candid::Error }, + + #[snafu(display("direct update call failed: {source}"))] + DirectUpdateCall { source: ic_agent::AgentError }, + + #[snafu(display("proxy update call failed: {source}"))] + ProxyUpdateCall { source: ic_agent::AgentError }, + + #[snafu(display("failed to decode proxy canister response: {source}"))] + ProxyDecode { source: candid::Error }, + + #[snafu(display("proxy call failed: {message}"))] + ProxyCall { message: String }, + + #[snafu(display("failed to encode call arguments: {source}"))] + CandidEncode { source: candid::Error }, + + #[snafu(display("failed to decode call response: {source}"))] + CandidDecode { source: candid::Error }, +} + +/// Dispatches a canister update call, optionally routing through a proxy canister. +/// +/// If `proxy` is `None`, makes a direct update call to the target canister. +/// If `proxy` is `Some`, wraps the call in [`ProxyArgs`] and sends it to the +/// proxy canister's `proxy` method, which forwards it to the target. +/// +/// `effective_canister_id` overrides the effective canister ID used for HTTP +/// routing in direct calls. This is required when calling the management +/// canister, where the effective canister ID must be the target canister +/// rather than `aaaaa-aa`. When `None`, defaults to `canister_id`. +/// +/// The `cycles` parameter is only used for proxied calls. +pub async fn update_or_proxy_raw( + agent: &Agent, + canister_id: Principal, + method: &str, + arg: Vec, + proxy: Option, + effective_canister_id: Option, + cycles: u128, +) -> Result, UpdateOrProxyError> { + if let Some(proxy_cid) = proxy { + let proxy_args = ProxyArgs { + canister_id, + method: method.to_string(), + args: arg, + cycles: Nat::from(cycles), + }; + let proxy_arg_bytes = Encode!(&proxy_args).context(ProxyEncodeSnafu)?; + + let proxy_res = agent + .update(&proxy_cid, "proxy") + .with_arg(proxy_arg_bytes) + .await + .context(ProxyUpdateCallSnafu)?; + + let proxy_result: (ProxyResult,) = + candid::decode_args(&proxy_res).context(ProxyDecodeSnafu)?; + + match proxy_result.0 { + ProxyResult::Ok(ok) => Ok(ok.result), + ProxyResult::Err(err) => ProxyCallSnafu { + message: err.format_error(), + } + .fail(), + } + } else { + let mut builder = agent.update(&canister_id, method).with_arg(arg); + if let Some(eid) = effective_canister_id { + builder = builder.with_effective_canister_id(eid); + } + let res = builder.await.context(DirectUpdateCallSnafu)?; + Ok(res) + } +} + +/// Like [`update_or_proxy_raw`], but accepts typed Candid arguments and decodes the response. +pub async fn update_or_proxy( + agent: &Agent, + canister_id: Principal, + method: &str, + args: A, + proxy: Option, + effective_canister_id: Option, + cycles: u128, +) -> Result +where + A: ArgumentEncoder, + R: for<'a> ArgumentDecoder<'a>, +{ + let arg = candid::encode_args(args).context(CandidEncodeSnafu)?; + let res = update_or_proxy_raw( + agent, + canister_id, + method, + arg, + proxy, + effective_canister_id, + cycles, + ) + .await?; + let decoded: R = candid::decode_args(&res).context(CandidDecodeSnafu)?; + Ok(decoded) +} diff --git a/crates/icp-cli/src/operations/proxy_management.rs b/crates/icp-cli/src/operations/proxy_management.rs new file mode 100644 index 00000000..ffc7cac7 --- /dev/null +++ b/crates/icp-cli/src/operations/proxy_management.rs @@ -0,0 +1,402 @@ +use candid::Principal; +use ic_agent::Agent; +use ic_management_canister_types::{ + CanisterIdRecord, CanisterStatusResult, ClearChunkStoreArgs, CreateCanisterArgs, + DeleteCanisterArgs, DeleteCanisterSnapshotArgs, FetchCanisterLogsArgs, FetchCanisterLogsResult, + InstallChunkedCodeArgs, InstallCodeArgs, ListCanisterSnapshotsArgs, + ListCanisterSnapshotsResult, LoadCanisterSnapshotArgs, ReadCanisterSnapshotDataArgs, + ReadCanisterSnapshotDataResult, ReadCanisterSnapshotMetadataArgs, + ReadCanisterSnapshotMetadataResult, StartCanisterArgs, StopCanisterArgs, + TakeCanisterSnapshotArgs, TakeCanisterSnapshotResult, UpdateSettingsArgs, + UploadCanisterSnapshotDataArgs, UploadCanisterSnapshotMetadataArgs, + UploadCanisterSnapshotMetadataResult, UploadChunkArgs, UploadChunkResult, +}; + +use snafu::{ResultExt, Snafu}; + +use super::proxy::{UpdateOrProxyError, update_or_proxy}; + +pub async fn create_canister( + agent: &Agent, + proxy: Option, + cycles: u128, + args: CreateCanisterArgs, +) -> Result { + let (result,): (CanisterIdRecord,) = update_or_proxy( + agent, + Principal::management_canister(), + "create_canister", + (args,), + proxy, + None, + cycles, + ) + .await?; + Ok(result) +} + +pub async fn canister_status( + agent: &Agent, + proxy: Option, + args: CanisterIdRecord, +) -> Result { + let effective = args.canister_id; + let (result,): (CanisterStatusResult,) = update_or_proxy( + agent, + Principal::management_canister(), + "canister_status", + (args,), + proxy, + Some(effective), + 0, + ) + .await?; + Ok(result) +} + +pub async fn stop_canister( + agent: &Agent, + proxy: Option, + args: StopCanisterArgs, +) -> Result<(), UpdateOrProxyError> { + let effective = args.canister_id; + update_or_proxy::<_, ()>( + agent, + Principal::management_canister(), + "stop_canister", + (args,), + proxy, + Some(effective), + 0, + ) + .await +} + +pub async fn start_canister( + agent: &Agent, + proxy: Option, + args: StartCanisterArgs, +) -> Result<(), UpdateOrProxyError> { + let effective = args.canister_id; + update_or_proxy::<_, ()>( + agent, + Principal::management_canister(), + "start_canister", + (args,), + proxy, + Some(effective), + 0, + ) + .await +} + +pub async fn delete_canister( + agent: &Agent, + proxy: Option, + args: DeleteCanisterArgs, +) -> Result<(), UpdateOrProxyError> { + let effective = args.canister_id; + update_or_proxy::<_, ()>( + agent, + Principal::management_canister(), + "delete_canister", + (args,), + proxy, + Some(effective), + 0, + ) + .await +} + +pub async fn update_settings( + agent: &Agent, + proxy: Option, + args: UpdateSettingsArgs, +) -> Result<(), UpdateOrProxyError> { + let effective = args.canister_id; + update_or_proxy::<_, ()>( + agent, + Principal::management_canister(), + "update_settings", + (args,), + proxy, + Some(effective), + 0, + ) + .await +} + +pub async fn install_code( + agent: &Agent, + proxy: Option, + args: InstallCodeArgs, +) -> Result<(), UpdateOrProxyError> { + let effective = args.canister_id; + update_or_proxy::<_, ()>( + agent, + Principal::management_canister(), + "install_code", + (args,), + proxy, + Some(effective), + 0, + ) + .await +} + +pub async fn install_chunked_code( + agent: &Agent, + proxy: Option, + args: InstallChunkedCodeArgs, +) -> Result<(), UpdateOrProxyError> { + let effective = args.target_canister; + update_or_proxy::<_, ()>( + agent, + Principal::management_canister(), + "install_chunked_code", + (args,), + proxy, + Some(effective), + 0, + ) + .await +} + +pub async fn upload_chunk( + agent: &Agent, + proxy: Option, + args: UploadChunkArgs, +) -> Result { + let effective = args.canister_id; + let (result,): (UploadChunkResult,) = update_or_proxy( + agent, + Principal::management_canister(), + "upload_chunk", + (args,), + proxy, + Some(effective), + 0, + ) + .await?; + Ok(result) +} + +pub async fn clear_chunk_store( + agent: &Agent, + proxy: Option, + args: ClearChunkStoreArgs, +) -> Result<(), UpdateOrProxyError> { + let effective = args.canister_id; + update_or_proxy::<_, ()>( + agent, + Principal::management_canister(), + "clear_chunk_store", + (args,), + proxy, + Some(effective), + 0, + ) + .await +} + +#[derive(Debug, Snafu)] +pub enum FetchCanisterLogsError { + #[snafu(display("failed to encode call arguments: {source}"))] + CandidEncode { source: candid::Error }, + + #[snafu(display("failed to decode call response: {source}"))] + CandidDecode { source: candid::Error }, + + #[snafu(display("direct query call failed: {source}"))] + DirectQueryCall { source: ic_agent::AgentError }, + + #[snafu(display("proxied call failed: {source}"))] + ProxiedCall { source: UpdateOrProxyError }, +} + +/// Fetches canister logs from the management canister. +/// +/// Unlike other management canister methods, `fetch_canister_logs` is a +/// **query** call when made directly. When a proxy is provided, the call is +/// routed through the proxy canister as an update call instead. +pub async fn fetch_canister_logs( + agent: &Agent, + proxy: Option, + args: FetchCanisterLogsArgs, +) -> Result { + let effective = args.canister_id; + if proxy.is_some() { + let (result,): (FetchCanisterLogsResult,) = update_or_proxy( + agent, + Principal::management_canister(), + "fetch_canister_logs", + (args,), + proxy, + Some(effective), + 0, + ) + .await + .context(ProxiedCallSnafu)?; + Ok(result) + } else { + let arg = candid::encode_args((args,)).context(CandidEncodeSnafu)?; + let res = agent + .query(&Principal::management_canister(), "fetch_canister_logs") + .with_arg(arg) + .with_effective_canister_id(effective) + .await + .context(DirectQueryCallSnafu)?; + let (result,): (FetchCanisterLogsResult,) = + candid::decode_args(&res).context(CandidDecodeSnafu)?; + Ok(result) + } +} + +pub async fn take_canister_snapshot( + agent: &Agent, + proxy: Option, + args: TakeCanisterSnapshotArgs, +) -> Result { + let effective = args.canister_id; + let (result,): (TakeCanisterSnapshotResult,) = update_or_proxy( + agent, + Principal::management_canister(), + "take_canister_snapshot", + (args,), + proxy, + Some(effective), + 0, + ) + .await?; + Ok(result) +} + +pub async fn load_canister_snapshot( + agent: &Agent, + proxy: Option, + args: LoadCanisterSnapshotArgs, +) -> Result<(), UpdateOrProxyError> { + let effective = args.canister_id; + update_or_proxy::<_, ()>( + agent, + Principal::management_canister(), + "load_canister_snapshot", + (args,), + proxy, + Some(effective), + 0, + ) + .await +} + +pub async fn list_canister_snapshots( + agent: &Agent, + proxy: Option, + args: ListCanisterSnapshotsArgs, +) -> Result { + let effective = args.canister_id; + let (result,): (ListCanisterSnapshotsResult,) = update_or_proxy( + agent, + Principal::management_canister(), + "list_canister_snapshots", + (args,), + proxy, + Some(effective), + 0, + ) + .await?; + Ok(result) +} + +pub async fn delete_canister_snapshot( + agent: &Agent, + proxy: Option, + args: DeleteCanisterSnapshotArgs, +) -> Result<(), UpdateOrProxyError> { + let effective = args.canister_id; + update_or_proxy::<_, ()>( + agent, + Principal::management_canister(), + "delete_canister_snapshot", + (args,), + proxy, + Some(effective), + 0, + ) + .await +} + +pub async fn read_canister_snapshot_metadata( + agent: &Agent, + proxy: Option, + args: ReadCanisterSnapshotMetadataArgs, +) -> Result { + let effective = args.canister_id; + let (result,): (ReadCanisterSnapshotMetadataResult,) = update_or_proxy( + agent, + Principal::management_canister(), + "read_canister_snapshot_metadata", + (args,), + proxy, + Some(effective), + 0, + ) + .await?; + Ok(result) +} + +pub async fn upload_canister_snapshot_metadata( + agent: &Agent, + proxy: Option, + args: UploadCanisterSnapshotMetadataArgs, +) -> Result { + let effective = args.canister_id; + let (result,): (UploadCanisterSnapshotMetadataResult,) = update_or_proxy( + agent, + Principal::management_canister(), + "upload_canister_snapshot_metadata", + (args,), + proxy, + Some(effective), + 0, + ) + .await?; + Ok(result) +} + +pub async fn read_canister_snapshot_data( + agent: &Agent, + proxy: Option, + args: ReadCanisterSnapshotDataArgs, +) -> Result { + let effective = args.canister_id; + let (result,): (ReadCanisterSnapshotDataResult,) = update_or_proxy( + agent, + Principal::management_canister(), + "read_canister_snapshot_data", + (args,), + proxy, + Some(effective), + 0, + ) + .await?; + Ok(result) +} + +pub async fn upload_canister_snapshot_data( + agent: &Agent, + proxy: Option, + args: UploadCanisterSnapshotDataArgs, +) -> Result<(), UpdateOrProxyError> { + let effective = args.canister_id; + update_or_proxy::<_, ()>( + agent, + Principal::management_canister(), + "upload_canister_snapshot_data", + (args,), + proxy, + Some(effective), + 0, + ) + .await +} diff --git a/crates/icp-cli/src/operations/settings.rs b/crates/icp-cli/src/operations/settings.rs index 88323821..7de4999c 100644 --- a/crates/icp-cli/src/operations/settings.rs +++ b/crates/icp-cli/src/operations/settings.rs @@ -1,10 +1,11 @@ use std::collections::{HashMap, HashSet}; -use candid::Principal; +use candid::{Nat, Principal}; use futures::{StreamExt, stream::FuturesOrdered}; -use ic_agent::{Agent, AgentError}; -use ic_management_canister_types::{EnvironmentVariable, LogVisibility as IcLogVisibility}; -use ic_utils::interfaces::ManagementCanister; +use ic_agent::Agent; +use ic_management_canister_types::{ + CanisterIdRecord, CanisterSettings, EnvironmentVariable, LogVisibility, UpdateSettingsArgs, +}; use icp::{Canister, canister::Settings}; use itertools::Itertools; use num_traits::ToPrimitive; @@ -13,19 +14,20 @@ use tracing::error; use crate::progress::{ProgressManager, ProgressManagerSettings}; +use super::proxy::UpdateOrProxyError; +use super::proxy_management; + #[derive(Debug, Snafu)] #[allow(clippy::enum_variant_names)] pub(crate) enum SyncSettingsOperationError { #[snafu(display("failed to fetch current canister settings for canister {canister}"))] FetchCurrentSettings { - source: AgentError, + source: UpdateOrProxyError, canister: Principal, }, - #[snafu(display("invalid canister settings in manifest for canister {name}"))] - ValidateSettings { source: AgentError, name: String }, #[snafu(display("failed to update canister settings for canister {canister}"))] UpdateSettings { - source: AgentError, + source: UpdateOrProxyError, canister: Principal, }, } @@ -45,11 +47,11 @@ struct SettingsFailure { /// Compare two LogVisibility values in an order-insensitive manner. /// For AllowedViewers, the principal lists are compared as sets. -fn log_visibility_eq(a: &IcLogVisibility, b: &IcLogVisibility) -> bool { +fn log_visibility_eq(a: &LogVisibility, b: &LogVisibility) -> bool { match (a, b) { - (IcLogVisibility::Controllers, IcLogVisibility::Controllers) => true, - (IcLogVisibility::Public, IcLogVisibility::Public) => true, - (IcLogVisibility::AllowedViewers(va), IcLogVisibility::AllowedViewers(vb)) => { + (LogVisibility::Controllers, LogVisibility::Controllers) => true, + (LogVisibility::Public, LogVisibility::Public) => true, + (LogVisibility::AllowedViewers(va), LogVisibility::AllowedViewers(vb)) => { let set_a: HashSet<_> = va.iter().collect(); let set_b: HashSet<_> = vb.iter().collect(); set_a == set_b @@ -67,14 +69,15 @@ fn environment_variables_eq(a: &[EnvironmentVariable], b: &[EnvironmentVariable] } pub(crate) async fn sync_settings( - mgmt: &ManagementCanister<'_>, + agent: &Agent, + proxy: Option, cid: &Principal, canister: &Canister, ) -> Result<(), SyncSettingsOperationError> { - let (status,) = mgmt - .canister_status(cid) - .await - .context(FetchCurrentSettingsSnafu { canister: *cid })?; + let status = + proxy_management::canister_status(agent, proxy, CanisterIdRecord { canister_id: *cid }) + .await + .context(FetchCurrentSettingsSnafu { canister: *cid })?; let &Settings { ref log_visibility, compute_allocation, @@ -89,8 +92,8 @@ pub(crate) async fn sync_settings( let current_settings = status.settings; // Convert our log_visibility to IC type for comparison and update - let log_visibility_setting: Option = - log_visibility.clone().map(IcLogVisibility::from); + let log_visibility_setting: Option = + log_visibility.clone().map(LogVisibility::from); let environment_variable_setting = if let Some(configured_environment_variables) = &environment_variables { @@ -144,52 +147,41 @@ pub(crate) async fn sync_settings( // No changes needed return Ok(()); } - let mut builder = mgmt.update_settings(cid); - if let Some(v) = log_visibility_setting { - builder = builder.with_log_visibility(v); - } - if let Some(v) = compute_allocation { - builder = builder.with_compute_allocation(v); - } - if let Some(v) = memory_allocation.as_ref().map(|m| m.get()) { - builder = builder.with_memory_allocation(v); - } - if let Some(v) = freezing_threshold.as_ref().map(|d| d.get()) { - builder = builder.with_freezing_threshold(v); - } - if let Some(v) = reserved_cycles_limit.as_ref().map(|r| r.get()) { - builder = builder.with_reserved_cycles_limit(v); - } - if let Some(v) = wasm_memory_limit.as_ref().map(|m| m.get()) { - builder = builder.with_wasm_memory_limit(v); - } - if let Some(v) = wasm_memory_threshold.as_ref().map(|m| m.get()) { - builder = builder.with_wasm_memory_threshold(v); - } - if let Some(v) = log_memory_limit.as_ref().map(|m| m.get()) { - builder = builder.with_log_memory_limit(v); - } - if let Some(v) = environment_variable_setting { - builder = builder.with_environment_variables(v); - } - builder - .build() - .context(ValidateSettingsSnafu { - name: &canister.name, - })? - .await - .context(UpdateSettingsSnafu { canister: *cid })?; + + let settings = CanisterSettings { + log_visibility: log_visibility_setting, + compute_allocation: compute_allocation.map(Nat::from), + memory_allocation: memory_allocation.as_ref().map(|m| Nat::from(m.get())), + freezing_threshold: freezing_threshold.as_ref().map(|d| Nat::from(d.get())), + reserved_cycles_limit: reserved_cycles_limit.as_ref().map(|r| Nat::from(r.get())), + wasm_memory_limit: wasm_memory_limit.as_ref().map(|m| Nat::from(m.get())), + wasm_memory_threshold: wasm_memory_threshold.as_ref().map(|m| Nat::from(m.get())), + log_memory_limit: log_memory_limit.as_ref().map(|m| Nat::from(m.get())), + environment_variables: environment_variable_setting, + ..Default::default() + }; + + proxy_management::update_settings( + agent, + proxy, + UpdateSettingsArgs { + canister_id: *cid, + settings, + sender_canister_version: None, + }, + ) + .await + .context(UpdateSettingsSnafu { canister: *cid })?; Ok(()) } pub(crate) async fn sync_settings_many( agent: Agent, + proxy: Option, target_canisters: Vec<(Principal, Canister)>, debug: bool, ) -> Result<(), SyncSettingsManyError> { - let mgmt = ManagementCanister::create(&agent); - let mut futs = FuturesOrdered::new(); let progress_manager = ProgressManager::new(ProgressManagerSettings { hidden: debug }); @@ -198,12 +190,12 @@ pub(crate) async fn sync_settings_many( let canister_name = info.name.clone(); let settings_fn = { - let mgmt = mgmt.clone(); + let agent = agent.clone(); let pb = pb.clone(); async move { pb.set_message("Updating canister settings..."); - sync_settings(&mgmt, &cid, &info).await + sync_settings(&agent, proxy, &cid, &info).await } }; @@ -262,28 +254,28 @@ mod tests { #[test] fn log_visibility_eq_controllers() { assert!(log_visibility_eq( - &IcLogVisibility::Controllers, - &IcLogVisibility::Controllers + &LogVisibility::Controllers, + &LogVisibility::Controllers )); } #[test] fn log_visibility_eq_public() { assert!(log_visibility_eq( - &IcLogVisibility::Public, - &IcLogVisibility::Public + &LogVisibility::Public, + &LogVisibility::Public )); } #[test] fn log_visibility_eq_different_variants() { assert!(!log_visibility_eq( - &IcLogVisibility::Controllers, - &IcLogVisibility::Public + &LogVisibility::Controllers, + &LogVisibility::Public )); assert!(!log_visibility_eq( - &IcLogVisibility::Public, - &IcLogVisibility::Controllers + &LogVisibility::Public, + &LogVisibility::Controllers )); } @@ -293,8 +285,8 @@ mod tests { let p2 = Principal::from_text("2vxsx-fae").unwrap(); assert!(log_visibility_eq( - &IcLogVisibility::AllowedViewers(vec![p1, p2]), - &IcLogVisibility::AllowedViewers(vec![p1, p2]) + &LogVisibility::AllowedViewers(vec![p1, p2]), + &LogVisibility::AllowedViewers(vec![p1, p2]) )); } @@ -305,8 +297,8 @@ mod tests { // Order should not matter assert!(log_visibility_eq( - &IcLogVisibility::AllowedViewers(vec![p1, p2]), - &IcLogVisibility::AllowedViewers(vec![p2, p1]) + &LogVisibility::AllowedViewers(vec![p1, p2]), + &LogVisibility::AllowedViewers(vec![p2, p1]) )); } @@ -317,8 +309,8 @@ mod tests { let p3 = Principal::from_text("ryjl3-tyaaa-aaaaa-aaaba-cai").unwrap(); assert!(!log_visibility_eq( - &IcLogVisibility::AllowedViewers(vec![p1, p2]), - &IcLogVisibility::AllowedViewers(vec![p1, p3]) + &LogVisibility::AllowedViewers(vec![p1, p2]), + &LogVisibility::AllowedViewers(vec![p1, p3]) )); } @@ -328,8 +320,8 @@ mod tests { let p2 = Principal::from_text("2vxsx-fae").unwrap(); assert!(!log_visibility_eq( - &IcLogVisibility::AllowedViewers(vec![p1]), - &IcLogVisibility::AllowedViewers(vec![p1, p2]) + &LogVisibility::AllowedViewers(vec![p1]), + &LogVisibility::AllowedViewers(vec![p1, p2]) )); } @@ -338,12 +330,12 @@ mod tests { let p1 = Principal::from_text("aaaaa-aa").unwrap(); assert!(!log_visibility_eq( - &IcLogVisibility::AllowedViewers(vec![p1]), - &IcLogVisibility::Controllers + &LogVisibility::AllowedViewers(vec![p1]), + &LogVisibility::Controllers )); assert!(!log_visibility_eq( - &IcLogVisibility::AllowedViewers(vec![p1]), - &IcLogVisibility::Public + &LogVisibility::AllowedViewers(vec![p1]), + &LogVisibility::Public )); } diff --git a/crates/icp-cli/src/operations/snapshot_transfer.rs b/crates/icp-cli/src/operations/snapshot_transfer.rs index ddaf9c4a..24a6388d 100644 --- a/crates/icp-cli/src/operations/snapshot_transfer.rs +++ b/crates/icp-cli/src/operations/snapshot_transfer.rs @@ -12,7 +12,9 @@ use ic_management_canister_types::{ UploadCanisterSnapshotDataArgs, UploadCanisterSnapshotMetadataArgs, UploadCanisterSnapshotMetadataResult, }; -use ic_utils::interfaces::ManagementCanister; + +use super::proxy::UpdateOrProxyError; +use super::proxy_management; use icp::{ fs::lock::{DirectoryStructureLock, LWrite, LockError, PathsAccess}, prelude::*, @@ -105,43 +107,43 @@ pub enum SnapshotTransferError { #[snafu(display("Failed to read snapshot metadata for canister {canister_id}"))] ReadMetadata { canister_id: Principal, - #[snafu(source(from(AgentError, Box::new)))] - source: Box, + #[snafu(source(from(UpdateOrProxyError, Box::new)))] + source: Box, }, #[snafu(display("Failed to read snapshot data chunk at offset {offset}"))] ReadDataChunk { offset: u64, - #[snafu(source(from(AgentError, Box::new)))] - source: Box, + #[snafu(source(from(UpdateOrProxyError, Box::new)))] + source: Box, }, #[snafu(display("Failed to read WASM chunk with hash {hash}"))] ReadWasmChunk { hash: String, - #[snafu(source(from(AgentError, Box::new)))] - source: Box, + #[snafu(source(from(UpdateOrProxyError, Box::new)))] + source: Box, }, #[snafu(display("Failed to upload snapshot metadata for canister {canister_id}"))] UploadMetadata { canister_id: Principal, - #[snafu(source(from(AgentError, Box::new)))] - source: Box, + #[snafu(source(from(UpdateOrProxyError, Box::new)))] + source: Box, }, #[snafu(display("Failed to upload snapshot data chunk at offset {offset}"))] UploadDataChunk { offset: u64, - #[snafu(source(from(AgentError, Box::new)))] - source: Box, + #[snafu(source(from(UpdateOrProxyError, Box::new)))] + source: Box, }, #[snafu(display("Failed to upload WASM chunk with hash {hash}"))] UploadWasmChunk { hash: String, - #[snafu(source(from(AgentError, Box::new)))] - source: Box, + #[snafu(source(from(UpdateOrProxyError, Box::new)))] + source: Box, }, #[snafu(transparent)] @@ -379,18 +381,27 @@ impl BlobType { } /// Check if an agent error is retryable. -fn is_retryable(error: &AgentError) -> bool { +fn is_retryable_agent_error(error: &AgentError) -> bool { matches!( error, AgentError::TimeoutWaitingForResponse() | AgentError::TransportError(_) ) } +/// Check if an `UpdateOrProxyError` is retryable (by inspecting the inner agent error). +fn is_retryable(error: &UpdateOrProxyError) -> bool { + match error { + UpdateOrProxyError::DirectUpdateCall { source } + | UpdateOrProxyError::ProxyUpdateCall { source } => is_retryable_agent_error(source), + _ => false, + } +} + /// Execute an async operation with exponential backoff retry. -async fn with_retry(operation: F) -> Result +async fn with_retry(operation: F) -> Result where F: Fn() -> Fut, - Fut: std::future::Future>, + Fut: std::future::Future>, { let mut backoff = ExponentialBackoff { max_elapsed_time: Some(std::time::Duration::from_secs(60)), @@ -429,19 +440,21 @@ pub fn create_transfer_progress_bar(total_bytes: u64, label: &str) -> ProgressBa /// Read snapshot metadata from a canister. pub async fn read_snapshot_metadata( agent: &Agent, + proxy: Option, canister_id: Principal, snapshot_id: &[u8], ) -> Result { - let mgmt = ManagementCanister::create(agent); - let args = ReadCanisterSnapshotMetadataArgs { canister_id, snapshot_id: snapshot_id.to_vec(), }; - let (metadata,) = with_retry(|| async { mgmt.read_canister_snapshot_metadata(&args).await }) - .await - .context(ReadMetadataSnafu { canister_id })?; + let metadata = with_retry(|| { + let args = args.clone(); + async move { proxy_management::read_canister_snapshot_metadata(agent, proxy, args).await } + }) + .await + .context(ReadMetadataSnafu { canister_id })?; Ok(metadata) } @@ -449,12 +462,11 @@ pub async fn read_snapshot_metadata( /// Upload snapshot metadata to create a new snapshot. pub async fn upload_snapshot_metadata( agent: &Agent, + proxy: Option, canister_id: Principal, metadata: &ReadCanisterSnapshotMetadataResult, replace_snapshot: Option<&[u8]>, ) -> Result { - let mgmt = ManagementCanister::create(agent); - // Convert Option to SnapshotMetadataGlobal, failing on None let globals = metadata .globals @@ -477,9 +489,12 @@ pub async fn upload_snapshot_metadata( on_low_wasm_memory_hook_status: metadata.on_low_wasm_memory_hook_status.clone(), }; - let (result,) = with_retry(|| async { mgmt.upload_canister_snapshot_metadata(&args).await }) - .await - .context(UploadMetadataSnafu { canister_id })?; + let result = with_retry(|| { + let args = args.clone(); + async move { proxy_management::upload_canister_snapshot_metadata(agent, proxy, args).await } + }) + .await + .context(UploadMetadataSnafu { canister_id })?; Ok(result) } @@ -491,6 +506,7 @@ pub async fn upload_snapshot_metadata( /// The agent handles rate limiting and semaphoring internally. pub async fn download_blob_to_file( agent: &Agent, + proxy: Option, canister_id: Principal, snapshot_id: &[u8], blob_type: BlobType, @@ -511,8 +527,6 @@ pub async fn download_blob_to_file( return Ok(()); } - let mgmt = ManagementCanister::create(agent); - // Create or open file for random-access writing let file = if output_path.exists() { File::options() @@ -551,14 +565,18 @@ pub async fn download_blob_to_file( kind: blob_type.make_read_kind(chunk_offset, chunk_size), }; - let mgmt = mgmt.clone(); in_progress.push(async move { - let result = with_retry(|| async { mgmt.read_canister_snapshot_data(&args).await }) - .await - .context(ReadDataChunkSnafu { - offset: chunk_offset, - })?; - Ok::<_, SnapshotTransferError>((chunk_offset, result.0.chunk)) + let result = with_retry(|| { + let args = args.clone(); + async move { + proxy_management::read_canister_snapshot_data(agent, proxy, args).await + } + }) + .await + .context(ReadDataChunkSnafu { + offset: chunk_offset, + })?; + Ok::<_, SnapshotTransferError>((chunk_offset, result.chunk)) }); } @@ -603,13 +621,12 @@ pub async fn download_blob_to_file( /// Download a single WASM chunk by hash. pub async fn download_wasm_chunk( agent: &Agent, + proxy: Option, canister_id: Principal, snapshot_id: &[u8], chunk_hash: &ChunkHash, paths: LWrite<&SnapshotPaths>, ) -> Result<(), SnapshotTransferError> { - let mgmt = ManagementCanister::create(agent); - let args = ReadCanisterSnapshotDataArgs { canister_id, snapshot_id: snapshot_id.to_vec(), @@ -621,9 +638,12 @@ pub async fn download_wasm_chunk( let hash_hex = hex::encode(&chunk_hash.hash); let output_path = paths.wasm_chunk_path(&chunk_hash.hash); - let (result,) = with_retry(|| async { mgmt.read_canister_snapshot_data(&args).await }) - .await - .context(ReadWasmChunkSnafu { hash: &hash_hex })?; + let result = with_retry(|| { + let args = args.clone(); + async move { proxy_management::read_canister_snapshot_data(agent, proxy, args).await } + }) + .await + .context(ReadWasmChunkSnafu { hash: &hash_hex })?; icp::fs::write(&output_path, &result.chunk)?; @@ -637,6 +657,7 @@ pub async fn download_wasm_chunk( /// Returns the final byte offset after all uploads complete. pub async fn upload_blob_from_file( agent: &Agent, + proxy: Option, canister_id: Principal, snapshot_id: &[u8], blob_type: BlobType, @@ -659,8 +680,6 @@ pub async fn upload_blob_from_file( BlobType::StableMemory => progress.stable_memory_offset, }; - let mgmt = ManagementCanister::create(agent); - let mut file = File::open(&input_path) .await .context(OpenBlobForUploadSnafu { path: &input_path })?; @@ -695,12 +714,17 @@ pub async fn upload_blob_from_file( chunk, }; - let mgmt = mgmt.clone(); + let chunk_len = args.chunk.len() as u64; in_progress.push(async move { - with_retry(|| async { mgmt.upload_canister_snapshot_data(&args).await }) - .await - .context(UploadDataChunkSnafu { offset })?; - Ok::<_, SnapshotTransferError>((offset, args.chunk.len() as u64)) + with_retry(|| { + let args = args.clone(); + async move { + proxy_management::upload_canister_snapshot_data(agent, proxy, args).await + } + }) + .await + .context(UploadDataChunkSnafu { offset })?; + Ok::<_, SnapshotTransferError>((offset, chunk_len)) }); } @@ -751,13 +775,12 @@ pub async fn upload_blob_from_file( /// Upload a single WASM chunk. pub async fn upload_wasm_chunk( agent: &Agent, + proxy: Option, canister_id: Principal, snapshot_id: &[u8], chunk_hash: &[u8], paths: LWrite<&SnapshotPaths>, ) -> Result<(), SnapshotTransferError> { - let mgmt = ManagementCanister::create(agent); - let chunk_path = paths.wasm_chunk_path(chunk_hash); let chunk = icp::fs::read(&chunk_path)?; @@ -770,9 +793,12 @@ pub async fn upload_wasm_chunk( let hash_hex = hex::encode(chunk_hash); - with_retry(|| async { mgmt.upload_canister_snapshot_data(&args).await }) - .await - .context(UploadWasmChunkSnafu { hash: hash_hex })?; + with_retry(|| { + let args = args.clone(); + async move { proxy_management::upload_canister_snapshot_data(agent, proxy, args).await } + }) + .await + .context(UploadWasmChunkSnafu { hash: hash_hex })?; Ok(()) } diff --git a/crates/icp-cli/tests/canister_create_tests.rs b/crates/icp-cli/tests/canister_create_tests.rs index 77373a0a..44bf3eff 100644 --- a/crates/icp-cli/tests/canister_create_tests.rs +++ b/crates/icp-cli/tests/canister_create_tests.rs @@ -4,8 +4,12 @@ use predicates::{ prelude::PredicateBooleanExt, str::{contains, starts_with}, }; +use test_tag::tag; -use crate::common::{ENVIRONMENT_RANDOM_PORT, NETWORK_RANDOM_PORT, TestContext, clients}; +use crate::common::{ + ENVIRONMENT_DOCKER_ENGINE, ENVIRONMENT_RANDOM_PORT, NETWORK_DOCKER_ENGINE, NETWORK_RANDOM_PORT, + TestContext, clients, +}; use icp::{fs::write_string, prelude::*}; mod common; @@ -537,3 +541,142 @@ async fn canister_create_detached() { .assert() .failure(); } + +#[tokio::test] +async fn canister_create_through_proxy() { + let ctx = TestContext::new(); + + let project_dir = ctx.create_project_dir("icp"); + + let pm = formatdoc! {r#" + canisters: + - name: my-canister + build: + steps: + - type: script + command: echo hi + + {NETWORK_RANDOM_PORT} + {ENVIRONMENT_RANDOM_PORT} + "#}; + + write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest"); + + let _g = ctx.start_network_in(&project_dir, "random-network").await; + ctx.ping_until_healthy(&project_dir, "random-network"); + + // Get the proxy canister ID from network status + let output = ctx + .icp() + .current_dir(&project_dir) + .args(["network", "status", "random-network", "--json"]) + .output() + .expect("failed to get network status"); + let status_json: serde_json::Value = + serde_json::from_slice(&output.stdout).expect("failed to parse network status JSON"); + let proxy_cid = status_json + .get("proxy_canister_principal") + .and_then(|v| v.as_str()) + .expect("proxy canister principal not found in network status") + .to_string(); + + // Create canister through the proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "create", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .stdout(contains("Created canister my-canister with ID")); + + let id_mapping_path = project_dir + .join(".icp") + .join("cache") + .join("mappings") + .join("random-environment.ids.json"); + assert!( + id_mapping_path.exists(), + "ID mapping file should exist at {id_mapping_path}" + ); +} + +#[tag(docker)] +#[tokio::test] +async fn canister_create_cloud_engine() { + let ctx = TestContext::new(); + + let project_dir = ctx.create_project_dir("icp"); + + let pm = formatdoc! {r#" + canisters: + - name: my-canister + build: + steps: + - type: script + command: echo hi + + {NETWORK_DOCKER_ENGINE} + {ENVIRONMENT_DOCKER_ENGINE} + "#}; + + write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest"); + + ctx.docker_pull_engine_network(); + let _guard = ctx + .start_network_in(&project_dir, "docker-engine-network") + .await; + ctx.ping_until_healthy(&project_dir, "docker-engine-network"); + + // Find the CloudEngine subnet by querying the topology endpoint + // TODO replace with a subnet selection parameter once we have one + let topology_url = ctx.gateway_url().join("/_/topology").unwrap(); + let topology: serde_json::Value = reqwest::get(topology_url) + .await + .expect("failed to fetch topology") + .json() + .await + .expect("failed to parse topology"); + + let subnet_configs = topology["subnet_configs"] + .as_object() + .expect("subnet_configs should be an object"); + let cloud_engine_subnet_id = subnet_configs + .iter() + .find_map(|(id, config)| { + (config["subnet_kind"].as_str()? == "CloudEngine").then_some(id.clone()) + }) + .expect("no CloudEngine subnet found in topology"); + + // Create the canister on the CloudEngine subnet + // Only the admin can do this. In local envs, the admin is the anonymous principal + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "create", + "my-canister", + "--subnet", + &cloud_engine_subnet_id, + "--environment", + "docker-engine-environment", + ]) + .assert() + .success(); + + let id_mapping_path = project_dir + .join(".icp") + .join("cache") + .join("mappings") + .join("docker-engine-environment.ids.json"); + assert!( + id_mapping_path.exists(), + "ID mapping file should exist at {id_mapping_path}" + ); +} diff --git a/crates/icp-cli/tests/canister_delete_tests.rs b/crates/icp-cli/tests/canister_delete_tests.rs index c6c79133..6a6f9da2 100644 --- a/crates/icp-cli/tests/canister_delete_tests.rs +++ b/crates/icp-cli/tests/canister_delete_tests.rs @@ -116,3 +116,95 @@ async fn canister_delete() { .failure() .stderr(contains("could not find ID for canister")); } + +#[tokio::test] +async fn canister_delete_through_proxy() { + let ctx = TestContext::new(); + + let project_dir = ctx.create_project_dir("icp"); + + let wasm = ctx.make_asset("example_icp_mo.wasm"); + + let pm = formatdoc! {r#" + canisters: + - name: my-canister + build: + steps: + - type: script + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" + + {NETWORK_RANDOM_PORT} + {ENVIRONMENT_RANDOM_PORT} + "#}; + + write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest"); + + let _g = ctx.start_network_in(&project_dir, "random-network").await; + ctx.ping_until_healthy(&project_dir, "random-network"); + + let proxy_cid = ctx.get_proxy_cid(&project_dir, "random-network"); + + // Deploy through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "deploy", + "--proxy", + &proxy_cid, + "--environment", + "random-environment", + ]) + .assert() + .success(); + + // Verify canister ID exists in id store + let id_mapping_path = project_dir + .join(".icp") + .join("cache") + .join("mappings") + .join("random-environment.ids.json"); + let id_mapping_before = + std::fs::read_to_string(&id_mapping_path).expect("ID mapping file should exist"); + assert!( + id_mapping_before.contains("my-canister"), + "ID mapping should contain my-canister before deletion" + ); + + // Stop canister through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "stop", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success(); + + // Delete canister through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "delete", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success(); + + // Verify canister ID is removed from the id store + let id_mapping_after = + std::fs::read_to_string(&id_mapping_path).expect("ID mapping file should still exist"); + assert!( + !id_mapping_after.contains("my-canister"), + "ID mapping should NOT contain my-canister after deletion" + ); +} diff --git a/crates/icp-cli/tests/canister_install_tests.rs b/crates/icp-cli/tests/canister_install_tests.rs index f275c904..9bed9d1c 100644 --- a/crates/icp-cli/tests/canister_install_tests.rs +++ b/crates/icp-cli/tests/canister_install_tests.rs @@ -479,7 +479,7 @@ async fn canister_install_with_environment_settings_override() { - type: script command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" settings: - memory_allocation: 1073741824 + memory_allocation: 10485760 {NETWORK_RANDOM_PORT} @@ -488,7 +488,7 @@ async fn canister_install_with_environment_settings_override() { network: random-network settings: my-canister: - memory_allocation: 2147483648 + memory_allocation: 20971520 "#}; write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest"); @@ -497,7 +497,7 @@ async fn canister_install_with_environment_settings_override() { let _g = ctx.start_network_in(&project_dir, "random-network").await; ctx.ping_until_healthy(&project_dir, "random-network"); - // Deploy should use the environment override (memory_allocation: 2GB) + // Deploy should use the environment override (memory_allocation: 20MiB) clients::icp(&ctx, &project_dir, Some("random-environment".to_string())) .mint_cycles(10 * TRILLION); @@ -532,8 +532,8 @@ async fn canister_install_with_environment_settings_override() { let output_str = String::from_utf8_lossy(&output); assert!( - output_str.contains("Memory allocation: 2_147_483_648"), - "Expected memory_allocation to be 2_147_483_648 (2GB) from environment override, got: {}", + output_str.contains("Memory allocation: 20_971_520"), + "Expected memory_allocation to be 20_971_520 (20MiB) from environment override, got: {}", output_str ); } @@ -1116,3 +1116,86 @@ async fn canister_install_upgrade_rejects_incompatible_candid() { .success() .stdout(eq("(\"Hello, 42!\")").trim()); } + +#[tokio::test] +async fn canister_install_through_proxy() { + let ctx = TestContext::new(); + + let project_dir = ctx.create_project_dir("icp"); + + let wasm = ctx.make_asset("example_icp_mo.wasm"); + + let pm = formatdoc! {r#" + canisters: + - name: my-canister + build: + steps: + - type: script + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" + + {NETWORK_RANDOM_PORT} + {ENVIRONMENT_RANDOM_PORT} + "#}; + + write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest"); + + let _g = ctx.start_network_in(&project_dir, "random-network").await; + ctx.ping_until_healthy(&project_dir, "random-network"); + + let proxy_cid = ctx.get_proxy_cid(&project_dir, "random-network"); + + // Build canister + ctx.icp() + .current_dir(&project_dir) + .args(["build", "my-canister"]) + .assert() + .success(); + + // Create canister through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "create", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success(); + + // Install canister through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "install", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success(); + + // Verify canister works + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "call", + "--environment", + "random-environment", + "my-canister", + "greet", + "(\"test\")", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .stdout(eq("(\"Hello, test!\")").trim()); +} diff --git a/crates/icp-cli/tests/canister_logs_tests.rs b/crates/icp-cli/tests/canister_logs_tests.rs index b84484b7..49484f91 100644 --- a/crates/icp-cli/tests/canister_logs_tests.rs +++ b/crates/icp-cli/tests/canister_logs_tests.rs @@ -365,3 +365,82 @@ async fn canister_logs_filter_by_timestamp() { .success() .stdout(contains("Timestamped message")); } + +// Ignored: fetch_canister_logs is not yet available in replicated mode. +// Tracking: https://github.com/dfinity/portal/pull/6106 +#[ignore] +#[cfg(unix)] // moc +#[tokio::test] +async fn canister_logs_through_proxy() { + let ctx = TestContext::new(); + let project_dir = ctx.create_project_dir("canister_logs"); + + ctx.copy_asset_dir("canister_logs", &project_dir); + + let pm = formatdoc! {r#" + canisters: + - name: logger + recipe: + type: "@dfinity/motoko@v4.0.0" + configuration: + main: main.mo + args: "" + + {NETWORK_RANDOM_PORT} + {ENVIRONMENT_RANDOM_PORT} + "#}; + + write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest"); + + let _g = ctx.start_network_in(&project_dir, "random-network").await; + ctx.ping_until_healthy(&project_dir, "random-network"); + + let proxy_cid = ctx.get_proxy_cid(&project_dir, "random-network"); + + // Deploy logger through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "deploy", + "logger", + "--proxy", + &proxy_cid, + "--environment", + "random-environment", + ]) + .assert() + .success(); + + // Create some logs by calling through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "call", + "--environment", + "random-environment", + "logger", + "log", + "(\"Proxy log message\")", + "--proxy", + &proxy_cid, + ]) + .assert() + .success(); + + // Fetch logs through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "logs", + "logger", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .stdout(contains("Proxy log message")); +} diff --git a/crates/icp-cli/tests/canister_settings_tests.rs b/crates/icp-cli/tests/canister_settings_tests.rs index 16300351..d01d9fe1 100644 --- a/crates/icp-cli/tests/canister_settings_tests.rs +++ b/crates/icp-cli/tests/canister_settings_tests.rs @@ -318,6 +318,89 @@ fn get_principal(client: &icp_cli::Client<'_>, identity: &str) -> String { client.get_principal(identity).to_string() } +#[tokio::test] +async fn canister_settings_update_through_proxy() { + let ctx = TestContext::new(); + + let project_dir = ctx.create_project_dir("icp"); + + let client = clients::icp(&ctx, &project_dir, None); + let principal_alice = get_principal(&client, "alice"); + + let wasm = ctx.make_asset("example_icp_mo.wasm"); + + let pm = formatdoc! {r#" + canisters: + - name: my-canister + build: + steps: + - type: script + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" + + {NETWORK_RANDOM_PORT} + {ENVIRONMENT_RANDOM_PORT} + "#}; + + write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest"); + + let _g = ctx.start_network_in(&project_dir, "random-network").await; + ctx.ping_until_healthy(&project_dir, "random-network"); + + let proxy_cid = ctx.get_proxy_cid(&project_dir, "random-network"); + + // Deploy through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "deploy", + "--proxy", + &proxy_cid, + "--environment", + "random-environment", + ]) + .assert() + .success(); + + // Add controller through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "settings", + "update", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + "--add-controller", + principal_alice.as_str(), + ]) + .assert() + .success(); + + // Verify the controller was added + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "settings", + "show", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .stdout( + starts_with("Canister Id:") + .and(contains(&proxy_cid)) + .and(contains(principal_alice.as_str())), + ); +} + #[tokio::test] async fn canister_settings_update_log_visibility() { let ctx = TestContext::new(); @@ -1293,3 +1376,95 @@ async fn canister_settings_sync_log_visibility() { sync(&ctx, &project_dir); confirm_log_visibility(&ctx, &project_dir, "Allowed viewers: 2vxsx-fae, aaaaa-aa"); } + +#[tokio::test] +async fn canister_settings_sync_through_proxy() { + let ctx = TestContext::new(); + + let project_dir = ctx.create_project_dir("icp"); + + let wasm = ctx.make_asset("example_icp_mo.wasm"); + + let pm = formatdoc! {r#" + canisters: + - name: my-canister + build: + steps: + - type: script + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" + + {NETWORK_RANDOM_PORT} + {ENVIRONMENT_RANDOM_PORT} + "#}; + + write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest"); + + let _g = ctx.start_network_in(&project_dir, "random-network").await; + ctx.ping_until_healthy(&project_dir, "random-network"); + + let proxy_cid = ctx.get_proxy_cid(&project_dir, "random-network"); + + // Deploy through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "deploy", + "--proxy", + &proxy_cid, + "--environment", + "random-environment", + ]) + .assert() + .success(); + + // Update manifest with memory_allocation setting + let pm_with_settings = formatdoc! {r#" + canisters: + - name: my-canister + build: + steps: + - type: script + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" + settings: + memory_allocation: 10485760 + + {NETWORK_RANDOM_PORT} + {ENVIRONMENT_RANDOM_PORT} + "#}; + + write_string(&project_dir.join("icp.yaml"), &pm_with_settings) + .expect("failed to write project manifest"); + + // Sync settings through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "settings", + "sync", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success(); + + // Verify the setting was applied + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "settings", + "show", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .stdout(contains("Memory allocation: 10_485_760")); +} diff --git a/crates/icp-cli/tests/canister_snapshot_tests.rs b/crates/icp-cli/tests/canister_snapshot_tests.rs index 0cb69e2d..49c18cb6 100644 --- a/crates/icp-cli/tests/canister_snapshot_tests.rs +++ b/crates/icp-cli/tests/canister_snapshot_tests.rs @@ -1495,3 +1495,353 @@ async fn canister_migrate_id() { .assert() .failure(); } + +/// Tests the full snapshot workflow through a proxy: create -> list -> download -> upload -> restore -> delete +#[cfg(unix)] // moc +#[tokio::test] +async fn canister_snapshot_workflow_through_proxy() { + let ctx = TestContext::new(); + let project_dir = ctx.create_project_dir("icp"); + let snapshot_dir = ctx.create_project_dir("snapshot"); + + ctx.copy_asset_dir("echo_init_arg_canister", &project_dir); + + let pm = formatdoc! {r#" + canisters: + - name: my-canister + recipe: + type: "@dfinity/motoko@v4.0.0" + configuration: + main: main.mo + args: "" + init_args: "(opt 1 : opt nat8)" + + {NETWORK_RANDOM_PORT} + {ENVIRONMENT_RANDOM_PORT} + "#}; + + write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest"); + + let _g = ctx.start_network_in(&project_dir, "random-network").await; + ctx.ping_until_healthy(&project_dir, "random-network"); + + let proxy_cid = ctx.get_proxy_cid(&project_dir, "random-network"); + + // Deploy through proxy with initial value 1 + ctx.icp() + .current_dir(&project_dir) + .args([ + "deploy", + "my-canister", + "--proxy", + &proxy_cid, + "--environment", + "random-environment", + ]) + .assert() + .success(); + + // Verify initial value is 1 + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "call", + "--environment", + "random-environment", + "my-canister", + "get", + "()", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .stdout(contains("\"1\"")); + + // Stop canister before creating snapshot + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "stop", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success(); + + // Create a snapshot through proxy + let create_output = ctx + .icp() + .current_dir(&project_dir) + .args([ + "canister", + "snapshot", + "create", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .get_output() + .stdout + .clone(); + + let create_output_str = String::from_utf8_lossy(&create_output); + assert!( + create_output_str.contains("Created snapshot"), + "Expected 'Created snapshot' in output, got: {}", + create_output_str + ); + + let snapshot_id = create_output_str + .lines() + .find(|line| line.contains("Created snapshot")) + .and_then(|line| line.split_whitespace().nth(2)) + .expect("Could not extract snapshot ID from output"); + + // List snapshots through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "snapshot", + "list", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .stdout(contains(snapshot_id)); + + // Download the snapshot through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "snapshot", + "download", + "my-canister", + snapshot_id, + "--output", + snapshot_dir.as_str(), + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .stderr(contains("Snapshot downloaded")); + + assert!( + snapshot_dir.join("metadata.json").exists(), + "metadata.json should exist after download" + ); + + // Upload the snapshot back through proxy + let upload_output = ctx + .icp() + .current_dir(&project_dir) + .args([ + "canister", + "snapshot", + "upload", + "my-canister", + "--input", + snapshot_dir.as_str(), + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .get_output() + .stdout + .clone(); + + let uploaded_snapshot_id = String::from_utf8_lossy(&upload_output) + .lines() + .find(|line| line.contains("uploaded successfully")) + .and_then(|line| line.split_whitespace().nth(1)) + .expect("Could not extract uploaded snapshot ID") + .to_string(); + + // Delete the uploaded snapshot (cleanup) + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "snapshot", + "delete", + "my-canister", + &uploaded_snapshot_id, + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success(); + + // Start, reinstall with new value, then restore from snapshot + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "start", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success(); + + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "install", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + "--mode", + "reinstall", + "--args", + "(opt 99 : opt nat8)", + ]) + .assert() + .success(); + + // Verify value is now 99 + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "call", + "--environment", + "random-environment", + "my-canister", + "get", + "()", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .stdout(contains("\"99\"")); + + // Stop and restore from snapshot through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "stop", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success(); + + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "snapshot", + "restore", + "my-canister", + snapshot_id, + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .stderr(contains("Restored canister")); + + // Start and verify value is back to 1 + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "start", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success(); + + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "call", + "--environment", + "random-environment", + "my-canister", + "get", + "()", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .stdout(contains("\"1\"")); + + // Delete the snapshot through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "snapshot", + "delete", + "my-canister", + snapshot_id, + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .stderr(contains("Deleted snapshot")); + + // List snapshots - should be empty + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "snapshot", + "list", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .stdout(contains("No snapshots found")); +} diff --git a/crates/icp-cli/tests/canister_start_tests.rs b/crates/icp-cli/tests/canister_start_tests.rs index 48861772..e73a8ce5 100644 --- a/crates/icp-cli/tests/canister_start_tests.rs +++ b/crates/icp-cli/tests/canister_start_tests.rs @@ -120,3 +120,106 @@ async fn canister_start() { .and(contains("Controllers: 2vxsx-fae")), ); } + +#[tokio::test] +async fn canister_start_through_proxy() { + let ctx = TestContext::new(); + + let project_dir = ctx.create_project_dir("icp"); + + let wasm = ctx.make_asset("example_icp_mo.wasm"); + + let pm = formatdoc! {r#" + canisters: + - name: my-canister + build: + steps: + - type: script + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" + + {NETWORK_RANDOM_PORT} + {ENVIRONMENT_RANDOM_PORT} + "#}; + + write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest"); + + let _g = ctx.start_network_in(&project_dir, "random-network").await; + ctx.ping_until_healthy(&project_dir, "random-network"); + + let proxy_cid = ctx.get_proxy_cid(&project_dir, "random-network"); + + // Deploy through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "deploy", + "--proxy", + &proxy_cid, + "--environment", + "random-environment", + ]) + .assert() + .success(); + + // Stop canister through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "stop", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success(); + + // Verify stopped + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "status", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .stdout(contains("Status: Stopped")); + + // Start canister through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "start", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success(); + + // Verify running + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "status", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .stdout(contains("Status: Running")); +} diff --git a/crates/icp-cli/tests/canister_status_tests.rs b/crates/icp-cli/tests/canister_status_tests.rs index 71437a35..c6dabfb2 100644 --- a/crates/icp-cli/tests/canister_status_tests.rs +++ b/crates/icp-cli/tests/canister_status_tests.rs @@ -76,3 +76,64 @@ async fn canister_status() { .and(contains("Controllers: 2vxsx-fae")), ); } + +#[tokio::test] +async fn canister_status_through_proxy() { + let ctx = TestContext::new(); + + let project_dir = ctx.create_project_dir("icp"); + + let wasm = ctx.make_asset("example_icp_mo.wasm"); + + let pm = formatdoc! {r#" + canisters: + - name: my-canister + build: + steps: + - type: script + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" + + {NETWORK_RANDOM_PORT} + {ENVIRONMENT_RANDOM_PORT} + "#}; + + write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest"); + + let _g = ctx.start_network_in(&project_dir, "random-network").await; + ctx.ping_until_healthy(&project_dir, "random-network"); + + let proxy_cid = ctx.get_proxy_cid(&project_dir, "random-network"); + + // Deploy through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "deploy", + "--proxy", + &proxy_cid, + "--environment", + "random-environment", + ]) + .assert() + .success(); + + // Query status through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "status", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .stdout( + starts_with("Canister Id:") + .and(contains("Status: Running")) + .and(contains(&proxy_cid)), + ); +} diff --git a/crates/icp-cli/tests/canister_stop_tests.rs b/crates/icp-cli/tests/canister_stop_tests.rs index 70d30074..3f4a0330 100644 --- a/crates/icp-cli/tests/canister_stop_tests.rs +++ b/crates/icp-cli/tests/canister_stop_tests.rs @@ -88,3 +88,79 @@ async fn canister_stop() { .and(contains("Controllers: 2vxsx-fae")), ); } + +#[tokio::test] +async fn canister_stop_through_proxy() { + let ctx = TestContext::new(); + + let project_dir = ctx.create_project_dir("icp"); + + let wasm = ctx.make_asset("example_icp_mo.wasm"); + + let pm = formatdoc! {r#" + canisters: + - name: my-canister + build: + steps: + - type: script + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" + + {NETWORK_RANDOM_PORT} + {ENVIRONMENT_RANDOM_PORT} + "#}; + + write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest"); + + let _g = ctx.start_network_in(&project_dir, "random-network").await; + ctx.ping_until_healthy(&project_dir, "random-network"); + + let proxy_cid = ctx.get_proxy_cid(&project_dir, "random-network"); + + // Deploy through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "deploy", + "--proxy", + &proxy_cid, + "--environment", + "random-environment", + ]) + .assert() + .success(); + + // Stop canister through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "stop", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success(); + + // Verify canister is stopped via status through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "status", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .stdout( + starts_with("Canister Id:") + .and(contains("Status: Stopped")) + .and(contains(&proxy_cid)), + ); +} diff --git a/crates/icp-cli/tests/common/context.rs b/crates/icp-cli/tests/common/context.rs index 3f0ff6ae..1b641f8c 100644 --- a/crates/icp-cli/tests/common/context.rs +++ b/crates/icp-cli/tests/common/context.rs @@ -395,6 +395,10 @@ impl TestContext { .expect("Failed to write network descriptor file"); } + pub(crate) fn gateway_url(&self) -> &Url { + self.http_gateway_url.get().unwrap() + } + pub(crate) fn agent(&self) -> Agent { let agent = Agent::builder() .with_url(self.http_gateway_url.get().unwrap().as_str()) @@ -440,19 +444,39 @@ impl TestContext { .as_ref() } + /// Get the proxy canister principal from network status JSON output. + pub(crate) fn get_proxy_cid(&self, project_dir: &Path, network: &str) -> String { + let output = self + .icp() + .current_dir(project_dir) + .args(["network", "status", network, "--json"]) + .output() + .expect("failed to get network status"); + let status_json: serde_json::Value = + serde_json::from_slice(&output.stdout).expect("failed to parse network status JSON"); + status_json + .get("proxy_canister_principal") + .and_then(|v| v.as_str()) + .expect("proxy canister principal not found in network status") + .to_string() + } + pub(crate) fn docker_pull_network(&self) { + self.docker_pull_image("ghcr.io/dfinity/icp-cli-network-launcher:v11.0.0"); + } + + pub(crate) fn docker_pull_engine_network(&self) { + self.docker_pull_image("ghcr.io/dfinity/icp-cli-network-launcher:engine-beta"); + } + + fn docker_pull_image(&self, image: &str) { let platform = if cfg!(target_arch = "aarch64") { "linux/arm64" } else { "linux/amd64" }; Command::new("docker") - .args([ - "pull", - "--platform", - platform, - "ghcr.io/dfinity/icp-cli-network-launcher:v11.0.0", - ]) + .args(["pull", "--platform", platform, image]) .assert() .success(); } diff --git a/crates/icp-cli/tests/common/mod.rs b/crates/icp-cli/tests/common/mod.rs index 7ec02d52..30694f63 100644 --- a/crates/icp-cli/tests/common/mod.rs +++ b/crates/icp-cli/tests/common/mod.rs @@ -49,6 +49,22 @@ environments: network: docker-network "#; +pub(crate) const NETWORK_DOCKER_ENGINE: &str = r#" +networks: + - name: docker-engine-network + mode: managed + image: ghcr.io/dfinity/icp-cli-network-launcher:engine-beta + port-mapping: + - 0:4943 + - 0:4942 +"#; + +pub(crate) const ENVIRONMENT_DOCKER_ENGINE: &str = r#" +environments: + - name: docker-engine-environment + network: docker-engine-network +"#; + /// This ID is dependent on the toplogy being served by pocket-ic /// NOTE: If the topology is changed (another subnet is added, etc) the ID may change. /// References: diff --git a/crates/icp-cli/tests/deploy_tests.rs b/crates/icp-cli/tests/deploy_tests.rs index b18d7b40..7f8affd0 100644 --- a/crates/icp-cli/tests/deploy_tests.rs +++ b/crates/icp-cli/tests/deploy_tests.rs @@ -1,10 +1,15 @@ use indoc::{formatdoc, indoc}; use predicates::{ ord::eq, + prelude::PredicateBooleanExt, str::{PredicateStrExt, contains}, }; +use test_tag::tag; -use crate::common::{ENVIRONMENT_RANDOM_PORT, NETWORK_RANDOM_PORT, TestContext, clients}; +use crate::common::{ + ENVIRONMENT_DOCKER_ENGINE, ENVIRONMENT_RANDOM_PORT, NETWORK_DOCKER_ENGINE, NETWORK_RANDOM_PORT, + TestContext, clients, +}; use icp::{ fs::{create_dir_all, write_string}, prelude::*, @@ -599,3 +604,437 @@ async fn deploy_upgrade_rejects_incompatible_candid() { .success() .stdout(eq("(\"Hello, 42!\")").trim()); } + +#[tag(docker)] +#[tokio::test] +async fn deploy_cloud_engine() { + let ctx = TestContext::new(); + + let project_dir = ctx.create_project_dir("icp"); + + let wasm = ctx.make_asset("example_icp_mo.wasm"); + + let pm = formatdoc! {r#" + canisters: + - name: my-canister + build: + steps: + - type: script + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" + + {NETWORK_DOCKER_ENGINE} + {ENVIRONMENT_DOCKER_ENGINE} + "#}; + + write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest"); + + ctx.docker_pull_engine_network(); + let _guard = ctx + .start_network_in(&project_dir, "docker-engine-network") + .await; + ctx.ping_until_healthy(&project_dir, "docker-engine-network"); + + // Find the CloudEngine subnet by querying the topology endpoint + // TODO replace with a subnet selection parameter once we have one + let topology_url = ctx.gateway_url().join("/_/topology").unwrap(); + let topology: serde_json::Value = reqwest::get(topology_url) + .await + .expect("failed to fetch topology") + .json() + .await + .expect("failed to parse topology"); + + let subnet_configs = topology["subnet_configs"] + .as_object() + .expect("subnet_configs should be an object"); + let cloud_engine_subnet_id = subnet_configs + .iter() + .find_map(|(id, config)| { + (config["subnet_kind"].as_str()? == "CloudEngine").then_some(id.clone()) + }) + .expect("no CloudEngine subnet found in topology"); + + // Deploy to the CloudEngine subnet + // Only the admin can do this. In local envs, the admin is the anonymous principal + ctx.icp() + .current_dir(&project_dir) + .args([ + "deploy", + "--subnet", + &cloud_engine_subnet_id, + "--environment", + "docker-engine-environment", + ]) + .assert() + .success(); + + // Query canister to verify it works + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "call", + "--environment", + "docker-engine-environment", + "my-canister", + "greet", + "(\"test\")", + ]) + .assert() + .success() + .stdout(eq("(\"Hello, test!\")").trim()); +} + +#[cfg(unix)] // moc +#[tokio::test] +async fn deploy_with_inline_args_candid() { + let ctx = TestContext::new(); + let project_dir = ctx.create_project_dir("icp"); + + ctx.copy_asset_dir("echo_init_arg_canister", &project_dir); + + let pm = formatdoc! {r#" + canisters: + - name: my-canister + recipe: + type: "@dfinity/motoko@v4.0.0" + configuration: + main: main.mo + args: "" + + {NETWORK_RANDOM_PORT} + {ENVIRONMENT_RANDOM_PORT} + "#}; + + write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest"); + + let _g = ctx.start_network_in(&project_dir, "random-network").await; + ctx.ping_until_healthy(&project_dir, "random-network"); + + clients::icp(&ctx, &project_dir, Some("random-environment".to_string())) + .mint_cycles(10 * TRILLION); + + ctx.icp() + .current_dir(&project_dir) + .args([ + "deploy", + "my-canister", + "--environment", + "random-environment", + "--args", + "(opt (42 : nat8))", + ]) + .assert() + .success(); + + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "call", + "--environment", + "random-environment", + "my-canister", + "get", + "()", + ]) + .assert() + .success() + .stdout(eq("(\"42\")").trim()); +} + +#[cfg(unix)] // moc +#[tokio::test] +async fn deploy_with_args_overrides_manifest_init_args() { + let ctx = TestContext::new(); + let project_dir = ctx.create_project_dir("icp"); + + ctx.copy_asset_dir("echo_init_arg_canister", &project_dir); + + // Manifest sets init_args to 7; CLI --args should override it with 42 + let pm = formatdoc! {r#" + canisters: + - name: my-canister + init_args: "(opt (7 : nat8))" + recipe: + type: "@dfinity/motoko@v4.0.0" + configuration: + main: main.mo + args: "" + + {NETWORK_RANDOM_PORT} + {ENVIRONMENT_RANDOM_PORT} + "#}; + + write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest"); + + let _g = ctx.start_network_in(&project_dir, "random-network").await; + ctx.ping_until_healthy(&project_dir, "random-network"); + + clients::icp(&ctx, &project_dir, Some("random-environment".to_string())) + .mint_cycles(10 * TRILLION); + + ctx.icp() + .current_dir(&project_dir) + .args([ + "deploy", + "my-canister", + "--environment", + "random-environment", + "--args", + "(opt (42 : nat8))", + ]) + .assert() + .success(); + + // CLI --args (42) should take priority over manifest init_args (7) + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "call", + "--environment", + "random-environment", + "my-canister", + "get", + "()", + ]) + .assert() + .success() + .stdout(eq("(\"42\")").trim()); +} + +#[cfg(unix)] // moc +#[tokio::test] +async fn deploy_with_args_file() { + let ctx = TestContext::new(); + let project_dir = ctx.create_project_dir("icp"); + + ctx.copy_asset_dir("echo_init_arg_canister", &project_dir); + + let pm = formatdoc! {r#" + canisters: + - name: my-canister + recipe: + type: "@dfinity/motoko@v4.0.0" + configuration: + main: main.mo + args: "" + + {NETWORK_RANDOM_PORT} + {ENVIRONMENT_RANDOM_PORT} + "#}; + + write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest"); + write_string(&project_dir.join("args.txt"), "(opt (42 : nat8))") + .expect("failed to write args file"); + + let _g = ctx.start_network_in(&project_dir, "random-network").await; + ctx.ping_until_healthy(&project_dir, "random-network"); + + clients::icp(&ctx, &project_dir, Some("random-environment".to_string())) + .mint_cycles(10 * TRILLION); + + ctx.icp() + .current_dir(&project_dir) + .args([ + "deploy", + "my-canister", + "--environment", + "random-environment", + "--args-file", + "args.txt", + ]) + .assert() + .success(); + + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "call", + "--environment", + "random-environment", + "my-canister", + "get", + "()", + ]) + .assert() + .success() + .stdout(eq("(\"42\")").trim()); +} + +#[cfg(unix)] // moc +#[tokio::test] +async fn deploy_with_args_hex_format() { + let ctx = TestContext::new(); + let project_dir = ctx.create_project_dir("icp"); + + ctx.copy_asset_dir("echo_init_arg_canister", &project_dir); + + let pm = formatdoc! {r#" + canisters: + - name: my-canister + recipe: + type: "@dfinity/motoko@v4.0.0" + configuration: + main: main.mo + args: "" + + {NETWORK_RANDOM_PORT} + {ENVIRONMENT_RANDOM_PORT} + "#}; + + write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest"); + + let _g = ctx.start_network_in(&project_dir, "random-network").await; + ctx.ping_until_healthy(&project_dir, "random-network"); + + clients::icp(&ctx, &project_dir, Some("random-environment".to_string())) + .mint_cycles(10 * TRILLION); + + // Hex encoding of "(opt 100 : opt nat8)" — didc encode '(opt 100 : opt nat8)' + ctx.icp() + .current_dir(&project_dir) + .args([ + "deploy", + "my-canister", + "--environment", + "random-environment", + "--args", + "4449444c016e7b01000164", + "--args-format", + "hex", + ]) + .assert() + .success(); + + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "call", + "--environment", + "random-environment", + "my-canister", + "get", + "()", + ]) + .assert() + .success() + .stdout(eq("(\"100\")").trim()); +} + +#[test] +fn deploy_with_args_multiple_canisters_fails() { + let ctx = TestContext::new(); + let project_dir = ctx.create_project_dir("icp"); + + let pm = indoc! {r#" + canisters: + - name: canister-a + build: + steps: + - type: script + command: echo hi + - name: canister-b + build: + steps: + - type: script + command: echo hi + "#}; + + write_string(&project_dir.join("icp.yaml"), pm).expect("failed to write project manifest"); + + ctx.icp() + .current_dir(&project_dir) + .args([ + "deploy", + "canister-a", + "canister-b", + "--subnet", + common::SUBNET_ID, + "--args", + "()", + ]) + .assert() + .failure() + .stderr(contains( + "--args and --args-file can only be used when deploying a single canister", + )); +} + +#[tokio::test] +async fn deploy_through_proxy() { + let ctx = TestContext::new(); + + let project_dir = ctx.create_project_dir("icp"); + + let wasm = ctx.make_asset("example_icp_mo.wasm"); + + let pm = formatdoc! {r#" + canisters: + - name: my-canister + build: + steps: + - type: script + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" + + {NETWORK_RANDOM_PORT} + {ENVIRONMENT_RANDOM_PORT} + "#}; + + write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest"); + + let _g = ctx.start_network_in(&project_dir, "random-network").await; + ctx.ping_until_healthy(&project_dir, "random-network"); + + let proxy_cid = ctx.get_proxy_cid(&project_dir, "random-network"); + + // Deploy through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "deploy", + "--proxy", + &proxy_cid, + "--environment", + "random-environment", + ]) + .assert() + .success(); + + // Verify canister works by calling it through proxy (proxy is the controller) + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "call", + "--environment", + "random-environment", + "my-canister", + "greet", + "(\"proxy\")", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .stdout(eq("(\"Hello, proxy!\")").trim()); + + // Verify canister status through proxy shows the proxy as controller + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "status", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .stdout(contains("Status: Running").and(contains(&proxy_cid))); +} diff --git a/crates/icp-cli/tests/network_tests.rs b/crates/icp-cli/tests/network_tests.rs index 29102830..ebf0d38e 100644 --- a/crates/icp-cli/tests/network_tests.rs +++ b/crates/icp-cli/tests/network_tests.rs @@ -4,6 +4,7 @@ use candid::Principal; use icp_canister_interfaces::{ cycles_ledger::CYCLES_LEDGER_PRINCIPAL, cycles_minting_canister::CYCLES_MINTING_CANISTER_PRINCIPAL, icp_ledger::ICP_LEDGER_PRINCIPAL, + internet_identity::INTERNET_IDENTITY_FRONTEND_PRINCIPAL, internet_identity::INTERNET_IDENTITY_PRINCIPAL, registry::REGISTRY_PRINCIPAL, }; use indoc::{formatdoc, indoc}; @@ -498,6 +499,10 @@ async fn network_starts_with_canisters_preset() { .read_state_canister_module_hash(INTERNET_IDENTITY_PRINCIPAL) .await .unwrap(); + agent + .read_state_canister_module_hash(INTERNET_IDENTITY_FRONTEND_PRINCIPAL) + .await + .unwrap(); } #[tag(docker)] @@ -874,3 +879,74 @@ async fn network_gateway_binds_to_configured_interface() { resp.status() ); } + +#[tokio::test] +async fn network_recovers_from_stale_descriptor() { + let ctx = TestContext::new(); + let project_dir = ctx.create_project_dir("stale-descriptor"); + + // Project manifest + write_string(&project_dir.join("icp.yaml"), NETWORK_RANDOM_PORT) + .expect("failed to write project manifest"); + + // Ensure the network descriptor directory exists + let network_dir = project_dir.join(".icp/cache/networks/random-network"); + std::fs::create_dir_all(&network_dir).expect("failed to create network directory"); + + // Create a stale descriptor with a PID that cannot exist + let stale_descriptor = serde_json::json!({ + "v": "1", + "id": "11111111-1111-1111-1111-111111111111", + "project-dir": project_dir.to_string(), + "network": "random-network", + "network-dir": network_dir.to_string(), + "gateway": { + "fixed": false, + "port": 9999, + "host": "localhost", + "ip": "127.0.0.1" + }, + "child-locator": { + "type": "pid", + "pid": u32::MAX, // Non-existent PID + "start-time": 0 + }, + "root-key": "308182301c300d06092a864886f70d0101010500030b008081007f", + "pocketic-config-port": null, + "pocketic-instance-id": null, + "candid-ui-canister-id": null, + "proxy-canister-id": null, + "status-dir": null, + "use-friendly-domains": false + }); + + // Write the stale descriptor + let descriptor_bytes = + serde_json::to_vec(&stale_descriptor).expect("failed to serialize descriptor"); + ctx.write_network_descriptor(&project_dir, "random-network", &descriptor_bytes); + + // Start network - should succeed and clean up the stale descriptor + ctx.icp() + .current_dir(&project_dir) + .args(["network", "start", "random-network", "--background"]) + .assert() + .success() + .stderr(contains("Found stale network descriptor")); + + // Verify the network actually started (descriptor should be updated with real process) + let network = ctx.wait_for_network_descriptor(&project_dir, "random-network"); + + ctx.ping_until_healthy(&project_dir, "random-network"); + + // Verify we can query the network + let agent = ic_agent::Agent::builder() + .with_url(format!("http://127.0.0.1:{}", network.gateway_port)) + .build() + .expect("Failed to build agent"); + + let status = agent.status().await.expect("Failed to get network status"); + assert!( + matches!(&status.replica_health_status, Some(health) if health == "healthy"), + "Network should be healthy" + ); +} diff --git a/crates/icp/src/canister/mod.rs b/crates/icp/src/canister/mod.rs index bf90e53e..791ca788 100644 --- a/crates/icp/src/canister/mod.rs +++ b/crates/icp/src/canister/mod.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use candid::{Nat, Principal}; -use icp_canister_interfaces::management_canister::CanisterSettingsArg; +use ic_management_canister_types::{CanisterSettings, LogVisibility}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -14,73 +14,24 @@ pub mod sync; mod script; /// Controls who can read canister logs. -#[derive(Clone, Debug, Default, PartialEq)] -pub enum LogVisibility { - /// Only controllers can view logs. - #[default] - Controllers, - /// Anyone can view logs. - Public, - /// Specific principals can view logs. - AllowedViewers(Vec), -} - -/// Serialization/deserialization representation for LogVisibility. /// Supports both string format ("controllers", "public") and object format ({ allowed_viewers: [...] }). -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(untagged, rename_all = "snake_case")] -enum LogVisibilityDef { +#[derive(Clone, Debug, PartialEq, Serialize)] +#[serde(untagged)] +pub enum LogVisibilityDef { /// Simple string variants for controllers or public Simple(LogVisibilitySimple), /// Object format with allowed_viewers list AllowedViewers { allowed_viewers: Vec }, } -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] -enum LogVisibilitySimple { +pub enum LogVisibilitySimple { Controllers, Public, } -impl From for LogVisibilityDef { - fn from(value: LogVisibility) -> Self { - match value { - LogVisibility::Controllers => { - LogVisibilityDef::Simple(LogVisibilitySimple::Controllers) - } - LogVisibility::Public => LogVisibilityDef::Simple(LogVisibilitySimple::Public), - LogVisibility::AllowedViewers(viewers) => LogVisibilityDef::AllowedViewers { - allowed_viewers: viewers, - }, - } - } -} - -impl From for LogVisibility { - fn from(value: LogVisibilityDef) -> Self { - match value { - LogVisibilityDef::Simple(LogVisibilitySimple::Controllers) => { - LogVisibility::Controllers - } - LogVisibilityDef::Simple(LogVisibilitySimple::Public) => LogVisibility::Public, - LogVisibilityDef::AllowedViewers { allowed_viewers } => { - LogVisibility::AllowedViewers(allowed_viewers) - } - } - } -} - -impl Serialize for LogVisibility { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - LogVisibilityDef::from(self.clone()).serialize(serializer) - } -} - -impl<'de> Deserialize<'de> for LogVisibility { +impl<'de> Deserialize<'de> for LogVisibilityDef { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, @@ -91,17 +42,17 @@ impl<'de> Deserialize<'de> for LogVisibility { struct LogVisibilityVisitor; impl<'de> Visitor<'de> for LogVisibilityVisitor { - type Value = LogVisibility; + type Value = LogVisibilityDef; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("'controllers', 'public', or object with 'allowed_viewers'") } fn visit_str(self, value: &str) -> Result { - LogVisibilityDef::deserialize( + LogVisibilitySimple::deserialize( serde::de::value::StrDeserializer::::new(value), ) - .map(Into::into) + .map(LogVisibilityDef::Simple) .map_err(|_| { E::custom(format!( "unknown log_visibility value: '{}', expected 'controllers' or 'public'", @@ -131,7 +82,7 @@ impl<'de> Deserialize<'de> for LogVisibility { } allowed_viewers - .map(LogVisibility::AllowedViewers) + .map(|v| LogVisibilityDef::AllowedViewers { allowed_viewers: v }) .ok_or_else(|| Error::missing_field("allowed_viewers")) } } @@ -140,7 +91,7 @@ impl<'de> Deserialize<'de> for LogVisibility { } } -impl JsonSchema for LogVisibility { +impl JsonSchema for LogVisibilityDef { fn schema_name() -> std::borrow::Cow<'static, str> { std::borrow::Cow::Borrowed("LogVisibility") } @@ -175,26 +126,15 @@ impl JsonSchema for LogVisibility { } } -impl From for ic_management_canister_types::LogVisibility { - fn from(value: LogVisibility) -> Self { +impl From for LogVisibility { + fn from(value: LogVisibilityDef) -> Self { match value { - LogVisibility::Controllers => ic_management_canister_types::LogVisibility::Controllers, - LogVisibility::Public => ic_management_canister_types::LogVisibility::Public, - LogVisibility::AllowedViewers(viewers) => { - ic_management_canister_types::LogVisibility::AllowedViewers(viewers) + LogVisibilityDef::Simple(LogVisibilitySimple::Controllers) => { + LogVisibility::Controllers } - } - } -} - -impl From for icp_canister_interfaces::management_canister::LogVisibility { - fn from(value: LogVisibility) -> Self { - use icp_canister_interfaces::management_canister::LogVisibility as CyclesLedgerLogVisibility; - match value { - LogVisibility::Controllers => CyclesLedgerLogVisibility::Controllers, - LogVisibility::Public => CyclesLedgerLogVisibility::Public, - LogVisibility::AllowedViewers(viewers) => { - CyclesLedgerLogVisibility::AllowedViewers(viewers) + LogVisibilityDef::Simple(LogVisibilitySimple::Public) => LogVisibility::Public, + LogVisibilityDef::AllowedViewers { allowed_viewers } => { + LogVisibility::AllowedViewers(allowed_viewers) } } } @@ -204,7 +144,7 @@ impl From for icp_canister_interfaces::management_canister::LogVi #[derive(Clone, Debug, Default, Deserialize, PartialEq, JsonSchema, Serialize)] pub struct Settings { /// Controls who can read canister logs. - pub log_visibility: Option, + pub log_visibility: Option, /// Compute allocation (0 to 100). Represents guaranteed compute capacity. pub compute_allocation: Option, @@ -241,15 +181,16 @@ pub struct Settings { pub environment_variables: Option>, } -impl From for CanisterSettingsArg { +impl From for CanisterSettings { fn from(settings: Settings) -> Self { - CanisterSettingsArg { + CanisterSettings { freezing_threshold: settings.freezing_threshold.map(|d| Nat::from(d.get())), controllers: None, reserved_cycles_limit: settings.reserved_cycles_limit.map(|c| Nat::from(c.get())), log_visibility: settings.log_visibility.map(Into::into), memory_allocation: settings.memory_allocation.map(|m| Nat::from(m.get())), compute_allocation: settings.compute_allocation.map(Nat::from), + ..Default::default() } } } @@ -261,15 +202,21 @@ mod tests { #[test] fn log_visibility_deserialize_controllers() { let yaml = "controllers"; - let result: LogVisibility = serde_yaml::from_str(yaml).unwrap(); - assert_eq!(result, LogVisibility::Controllers); + let result: LogVisibilityDef = serde_yaml::from_str(yaml).unwrap(); + assert_eq!( + result, + LogVisibilityDef::Simple(LogVisibilitySimple::Controllers) + ); } #[test] fn log_visibility_deserialize_public() { let yaml = "public"; - let result: LogVisibility = serde_yaml::from_str(yaml).unwrap(); - assert_eq!(result, LogVisibility::Public); + let result: LogVisibilityDef = serde_yaml::from_str(yaml).unwrap(); + assert_eq!( + result, + LogVisibilityDef::Simple(LogVisibilitySimple::Public) + ); } #[test] @@ -279,12 +226,18 @@ allowed_viewers: - "aaaaa-aa" - "2vxsx-fae" "#; - let result: LogVisibility = serde_yaml::from_str(yaml).unwrap(); + let result: LogVisibilityDef = serde_yaml::from_str(yaml).unwrap(); match result { - LogVisibility::AllowedViewers(viewers) => { - assert_eq!(viewers.len(), 2); - assert_eq!(viewers[0], Principal::from_text("aaaaa-aa").unwrap()); - assert_eq!(viewers[1], Principal::from_text("2vxsx-fae").unwrap()); + LogVisibilityDef::AllowedViewers { allowed_viewers } => { + assert_eq!(allowed_viewers.len(), 2); + assert_eq!( + allowed_viewers[0], + Principal::from_text("aaaaa-aa").unwrap() + ); + assert_eq!( + allowed_viewers[1], + Principal::from_text("2vxsx-fae").unwrap() + ); } _ => panic!("Expected AllowedViewers variant"), } @@ -293,10 +246,10 @@ allowed_viewers: #[test] fn log_visibility_deserialize_allowed_viewers_empty() { let yaml = "allowed_viewers: []"; - let result: LogVisibility = serde_yaml::from_str(yaml).unwrap(); + let result: LogVisibilityDef = serde_yaml::from_str(yaml).unwrap(); match result { - LogVisibility::AllowedViewers(viewers) => { - assert!(viewers.is_empty()); + LogVisibilityDef::AllowedViewers { allowed_viewers } => { + assert!(allowed_viewers.is_empty()); } _ => panic!("Expected AllowedViewers variant"), } @@ -305,7 +258,7 @@ allowed_viewers: #[test] fn log_visibility_deserialize_invalid_string() { let yaml = "invalid"; - let result: Result = serde_yaml::from_str(yaml); + let result: Result = serde_yaml::from_str(yaml); assert!(result.is_err()); let err = result.unwrap_err().to_string(); assert!(err.contains("unknown log_visibility value")); @@ -314,7 +267,7 @@ allowed_viewers: #[test] fn log_visibility_deserialize_invalid_field() { let yaml = "unknown_field: []"; - let result: Result = serde_yaml::from_str(yaml); + let result: Result = serde_yaml::from_str(yaml); assert!(result.is_err()); let err = result.unwrap_err().to_string(); assert!(err.contains("unknown field")); @@ -322,24 +275,26 @@ allowed_viewers: #[test] fn log_visibility_serialize_controllers() { - let log_vis = LogVisibility::Controllers; + let log_vis = LogVisibilityDef::Simple(LogVisibilitySimple::Controllers); let yaml = serde_yaml::to_string(&log_vis).unwrap(); assert_eq!(yaml.trim(), "controllers"); } #[test] fn log_visibility_serialize_public() { - let log_vis = LogVisibility::Public; + let log_vis = LogVisibilityDef::Simple(LogVisibilitySimple::Public); let yaml = serde_yaml::to_string(&log_vis).unwrap(); assert_eq!(yaml.trim(), "public"); } #[test] fn log_visibility_serialize_allowed_viewers() { - let log_vis = LogVisibility::AllowedViewers(vec![ - Principal::from_text("aaaaa-aa").unwrap(), - Principal::from_text("2vxsx-fae").unwrap(), - ]); + let log_vis = LogVisibilityDef::AllowedViewers { + allowed_viewers: vec![ + Principal::from_text("aaaaa-aa").unwrap(), + Principal::from_text("2vxsx-fae").unwrap(), + ], + }; let yaml = serde_yaml::to_string(&log_vis).unwrap(); assert!(yaml.contains("allowed_viewers")); assert!(yaml.contains("aaaaa-aa")); @@ -418,25 +373,20 @@ allowed_viewers: #[test] fn log_visibility_conversion_to_ic_type() { - let controllers = LogVisibility::Controllers; - let ic_controllers: ic_management_canister_types::LogVisibility = controllers.into(); - assert!(matches!( - ic_controllers, - ic_management_canister_types::LogVisibility::Controllers - )); - - let public = LogVisibility::Public; - let ic_public: ic_management_canister_types::LogVisibility = public.into(); - assert!(matches!( - ic_public, - ic_management_canister_types::LogVisibility::Public - )); - - let viewers = - LogVisibility::AllowedViewers(vec![Principal::from_text("aaaaa-aa").unwrap()]); - let ic_viewers: ic_management_canister_types::LogVisibility = viewers.into(); + let controllers = LogVisibilityDef::Simple(LogVisibilitySimple::Controllers); + let ic_controllers: LogVisibility = controllers.into(); + assert!(matches!(ic_controllers, LogVisibility::Controllers)); + + let public = LogVisibilityDef::Simple(LogVisibilitySimple::Public); + let ic_public: LogVisibility = public.into(); + assert!(matches!(ic_public, LogVisibility::Public)); + + let viewers = LogVisibilityDef::AllowedViewers { + allowed_viewers: vec![Principal::from_text("aaaaa-aa").unwrap()], + }; + let ic_viewers: LogVisibility = viewers.into(); match ic_viewers { - ic_management_canister_types::LogVisibility::AllowedViewers(v) => { + LogVisibility::AllowedViewers(v) => { assert_eq!(v.len(), 1); } _ => panic!("Expected AllowedViewers"), diff --git a/crates/icp/src/context/init.rs b/crates/icp/src/context/init.rs index 16e34864..636fb233 100644 --- a/crates/icp/src/context/init.rs +++ b/crates/icp/src/context/init.rs @@ -87,11 +87,11 @@ pub fn initialize( let telemetry_data = Arc::new(crate::telemetry_data::TelemetryData::default()); // Identity loader - let idload = Arc::new(identity::Loader { - dir: dirs.identity().context(IdentityDirectorySnafu)?, + let idload = Arc::new(identity::Loader::new( + dirs.identity().context(IdentityDirectorySnafu)?, password_func, - telemetry_data: telemetry_data.clone(), - }); + telemetry_data.clone(), + )); if let Ok(mockdir) = std::env::var("ICP_CLI_KEYRING_MOCK_DIR") { keyring::set_default_credential_builder(Box::new( crate::identity::keyring_mock::MockKeyring { diff --git a/crates/icp/src/context/mod.rs b/crates/icp/src/context/mod.rs index 94be911e..270d08a5 100644 --- a/crates/icp/src/context/mod.rs +++ b/crates/icp/src/context/mod.rs @@ -548,9 +548,15 @@ impl Context { env_mappings.insert(env_name.clone(), mapping); } } - if let Err(e) = - crate::network::custom_domains::write_custom_domains(status_dir, domain, &env_mappings) - { + let extra: Vec<_> = crate::network::custom_domains::ii_custom_domain_entry(desc.ii, domain) + .into_iter() + .collect(); + if let Err(e) = crate::network::custom_domains::write_custom_domains( + status_dir, + domain, + &env_mappings, + &extra, + ) { tracing::warn!("Failed to update custom domains: {e}"); } } diff --git a/crates/icp/src/identity/mod.rs b/crates/icp/src/identity/mod.rs index 852210e3..e277efe3 100644 --- a/crates/icp/src/identity/mod.rs +++ b/crates/icp/src/identity/mod.rs @@ -1,9 +1,11 @@ -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use async_trait::async_trait; use ic_agent::Identity; use snafu::prelude::*; +use std::collections::HashMap; + use crate::{ fs::lock::{DirectoryStructureLock, LockError, PathsAccess}, identity::{ @@ -70,7 +72,7 @@ impl PathsAccess for IdentityPaths { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum IdentitySelection { /// Current default Default, @@ -106,64 +108,92 @@ pub trait Load: Sync + Send { pub type PasswordFunc = Box Result + Send + Sync>; pub struct Loader { - pub dir: IdentityDirectories, - pub password_func: PasswordFunc, - pub telemetry_data: Arc, + dir: IdentityDirectories, + password_func: PasswordFunc, + telemetry_data: Arc, + #[allow(clippy::type_complexity)] + cache: Mutex, Option)>>, +} + +impl Loader { + pub fn new( + dir: IdentityDirectories, + password_func: PasswordFunc, + telemetry_data: Arc, + ) -> Self { + Self { + dir, + password_func, + telemetry_data, + cache: Mutex::new(HashMap::new()), + } + } } #[async_trait] impl Load for Loader { async fn load(&self, id: IdentitySelection) -> Result, LoadError> { + if let Some((cached, storage_type)) = self.cache.lock().unwrap().get(&id) { + if let Some(t) = storage_type { + self.telemetry_data.set_identity_type(*t); + } + return Ok(Arc::clone(cached)); + } + let password_func = &self.password_func; - let telemetry_data = &self.telemetry_data; - match id { + let (identity, storage_type) = match &id { IdentitySelection::Default => { self.dir - .with_read(async |dirs| { + .with_read(async |dirs| -> Result<_, LoadIdentityInContextError> { let list = IdentityList::load_from(dirs)?; let default_name = manifest::IdentityDefaults::load_from(dirs)?.default; let identity = load_identity(dirs, &list, &default_name, password_func)?; - if let Some(spec) = list.identities.get(&default_name) { - telemetry_data.set_identity_type(spec.into()); - } - Ok(identity) + let storage_type = + list.identities.get(&default_name).map(|spec| spec.into()); + Ok((identity, storage_type)) }) - .await? + .await?? } IdentitySelection::Anonymous => { - telemetry_data.set_identity_type(IdentityStorageType::Anonymous); self.dir - .with_read(async |dirs| { - Ok(load_identity( - dirs, - &IdentityList::load_from(dirs)?, - "anonymous", - || unreachable!(), - )?) + .with_read(async |dirs| -> Result<_, LoadIdentityInContextError> { + Ok(( + load_identity( + dirs, + &IdentityList::load_from(dirs)?, + "anonymous", + || unreachable!(), + )?, + Some(IdentityStorageType::Anonymous), + )) }) - .await? + .await?? } IdentitySelection::Named(name) => { self.dir - .with_read(async |dirs| { + .with_read(async |dirs| -> Result<_, LoadIdentityInContextError> { let list = IdentityList::load_from(dirs)?; - let identity = load_identity(dirs, &list, &name, password_func)?; - if let Some(spec) = list.identities.get(&name) { - telemetry_data.set_identity_type(spec.into()); - } - Ok(identity) + let identity = load_identity(dirs, &list, name, password_func)?; + let storage_type = list.identities.get(name).map(|spec| spec.into()); + Ok((identity, storage_type)) }) - .await? + .await?? } + }; + + if let Some(t) = storage_type { + self.telemetry_data.set_identity_type(t); } + self.cache + .lock() + .unwrap() + .insert(id, (Arc::clone(&identity), storage_type)); + Ok(identity) } } -#[cfg(test)] -use std::collections::HashMap; - #[cfg(test)] pub struct MockIdentityLoader { /// The default identity to return when IdentitySelection::Default is used @@ -221,3 +251,45 @@ impl Load for MockIdentityLoader { }) } } + +#[cfg(test)] +mod tests { + use k256::SecretKey; + use rand::{Rng, rng}; + + use crate::identity::key::{CreateFormat, IdentityKey}; + + use super::*; + #[tokio::test] + async fn cached_identities() { + let tmp = camino_tempfile::tempdir().unwrap(); + let dirs = IdentityPaths::new(tmp.path().to_path_buf()).unwrap(); + let mut k = [0; 32]; + rng().fill_bytes(&mut k); + dirs.with_write(async |dirs| { + crate::identity::key::create_identity( + dirs, + "test", + IdentityKey::Secp256k1(SecretKey::from_bytes(&k.into()).unwrap()), + CreateFormat::Plaintext, + ) + .unwrap(); + }) + .await + .unwrap(); + let loader = Loader::new( + dirs, + Box::new(|| unimplemented!()), + Arc::new(TelemetryData::default()), + ); + let i1 = loader + .load(IdentitySelection::Named("test".to_string())) + .await + .unwrap(); + let i2 = loader + .load(IdentitySelection::Named("test".to_string())) + .await + .unwrap(); + assert!(Arc::ptr_eq(&i1, &i2)); + } +} diff --git a/crates/icp/src/lib.rs b/crates/icp/src/lib.rs index 97adf3e8..b023cc83 100644 --- a/crates/icp/src/lib.rs +++ b/crates/icp/src/lib.rs @@ -11,7 +11,7 @@ use candid_parser::parse_idl_args; use crate::{ canister::{Settings, recipe::Resolve}, manifest::{ - InitArgsFormat, LoadManifestFromPathError, PROJECT_MANIFEST, ProjectRootLocate, + ArgsFormat, LoadManifestFromPathError, PROJECT_MANIFEST, ProjectRootLocate, ProjectRootLocateError, canister::{BuildSteps, SyncSteps}, load_manifest_from_path, @@ -46,10 +46,7 @@ const DATA_DIR: &str = "data"; #[derive(Clone, Debug, PartialEq, Serialize)] pub enum InitArgs { /// Text content (inline or loaded from file). Format is always known. - Text { - content: String, - format: InitArgsFormat, - }, + Text { content: String, format: ArgsFormat }, /// Raw binary bytes (from a file with `format: bin`). Used directly. Binary(Vec), } @@ -72,12 +69,12 @@ impl InitArgs { match self { InitArgs::Binary(bytes) => Ok(bytes.clone()), InitArgs::Text { content, format } => match format { - InitArgsFormat::Hex => hex::decode(content.trim()).context(HexDecodeSnafu), - InitArgsFormat::Candid => { + ArgsFormat::Hex => hex::decode(content.trim()).context(HexDecodeSnafu), + ArgsFormat::Candid => { let args = parse_idl_args(content.trim()).context(CandidParseSnafu)?; args.to_bytes().context(CandidEncodeSnafu) } - InitArgsFormat::Bin => { + ArgsFormat::Bin => { unreachable!("binary format cannot appear in InitArgs::Text") } }, diff --git a/crates/icp/src/manifest/canister.rs b/crates/icp/src/manifest/canister.rs index 9e0fdcbb..6fafaf72 100644 --- a/crates/icp/src/manifest/canister.rs +++ b/crates/icp/src/manifest/canister.rs @@ -7,11 +7,11 @@ use crate::canister::Settings; use super::{adapter, recipe::Recipe, serde_helpers::non_empty_vec}; -/// Format specifier for init args content. +/// Format specifier for canister call/install args content. #[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize, JsonSchema)] #[cfg_attr(feature = "clap", derive(clap::ValueEnum))] #[serde(rename_all = "lowercase")] -pub enum InitArgsFormat { +pub enum ArgsFormat { /// Hex-encoded bytes Hex, /// Candid text format @@ -48,13 +48,13 @@ pub enum ManifestInitArgs { Path { path: String, #[serde(default)] - format: InitArgsFormat, + format: ArgsFormat, }, /// Inline value with explicit format. Value { value: String, #[serde(default)] - format: InitArgsFormat, + format: ArgsFormat, }, } @@ -767,7 +767,7 @@ mod tests { ia, ManifestInitArgs::Path { path: "./args.bin".to_string(), - format: InitArgsFormat::Bin, + format: ArgsFormat::Bin, } ); } @@ -783,7 +783,7 @@ mod tests { ia, ManifestInitArgs::Value { value: "(42)".to_string(), - format: InitArgsFormat::Candid, + format: ArgsFormat::Candid, } ); } @@ -798,7 +798,7 @@ mod tests { ia, ManifestInitArgs::Value { value: "(42)".to_string(), - format: InitArgsFormat::Candid, + format: ArgsFormat::Candid, } ); } diff --git a/crates/icp/src/manifest/mod.rs b/crates/icp/src/manifest/mod.rs index 83bed7cc..d21a867d 100644 --- a/crates/icp/src/manifest/mod.rs +++ b/crates/icp/src/manifest/mod.rs @@ -16,7 +16,7 @@ pub(crate) mod recipe; pub(crate) mod serde_helpers; pub use { - canister::{CanisterManifest, InitArgsFormat, ManifestInitArgs}, + canister::{ArgsFormat, CanisterManifest, ManifestInitArgs}, environment::EnvironmentManifest, network::NetworkManifest, project::ProjectManifest, diff --git a/crates/icp/src/manifest/network.rs b/crates/icp/src/manifest/network.rs index cacd7782..16ddc05d 100644 --- a/crates/icp/src/manifest/network.rs +++ b/crates/icp/src/manifest/network.rs @@ -64,7 +64,8 @@ pub enum ManagedMode { gateway: Option, /// Artificial delay to add to every update call artificial_delay_ms: Option, - /// Set up the Internet Identity canister + /// Set up the Internet Identity canister. Makes internet identity available at + /// id.ai.localhost: ii: Option, /// Set up the NNS nns: Option, diff --git a/crates/icp/src/manifest/project.rs b/crates/icp/src/manifest/project.rs index 7559f28e..b158cbbd 100644 --- a/crates/icp/src/manifest/project.rs +++ b/crates/icp/src/manifest/project.rs @@ -30,7 +30,7 @@ mod tests { use crate::{ canister::Settings, manifest::{ - InitArgsFormat, ManifestInitArgs, + ArgsFormat, ManifestInitArgs, adapter::script, canister::{BuildStep, BuildSteps, Instructions}, environment::CanisterSelection, @@ -459,7 +459,7 @@ mod tests { "canister-2".to_string(), ManifestInitArgs::Value { value: "4449444c0000".to_string(), - format: InitArgsFormat::Hex, + format: ArgsFormat::Hex, }, ), ])), diff --git a/crates/icp/src/network/config.rs b/crates/icp/src/network/config.rs index cc663ff4..c9bb5c08 100644 --- a/crates/icp/src/network/config.rs +++ b/crates/icp/src/network/config.rs @@ -75,6 +75,9 @@ pub struct NetworkDescriptorModel { pub candid_ui_canister_id: Option, /// Canister ID of the deployed proxy canister, if any. pub proxy_canister_id: Option, + /// Whether Internet Identity is deployed on this network. + #[serde(default)] + pub ii: bool, /// Path to the status directory shared with the network launcher. /// Used to write `custom-domains.txt` for friendly domain routing. #[serde(default)] diff --git a/crates/icp/src/network/custom_domains.rs b/crates/icp/src/network/custom_domains.rs index ce0f615e..9938a070 100644 --- a/crates/icp/src/network/custom_domains.rs +++ b/crates/icp/src/network/custom_domains.rs @@ -11,13 +11,17 @@ use crate::{prelude::*, store_id::IdMapping}; /// Each line has the format `..:`. /// The file is written fresh each time from the full set of current mappings /// across all environments sharing this network. +/// +/// `extra_entries` are raw `(full_domain, canister_id)` pairs appended after the +/// environment-based entries (e.g. system canisters like Internet Identity). pub fn write_custom_domains( status_dir: &Path, domain: &str, env_mappings: &BTreeMap, + extra_entries: &[(String, String)], ) -> Result<(), WriteCustomDomainsError> { let file_path = status_dir.join("custom-domains.txt"); - let content: String = env_mappings + let mut content: String = env_mappings .iter() .flat_map(|(env_name, mappings)| { mappings @@ -25,10 +29,25 @@ pub fn write_custom_domains( .map(move |(name, principal)| format!("{name}.{env_name}.{domain}:{principal}\n")) }) .collect(); + for (full_domain, canister_id) in extra_entries { + content.push_str(&format!("{full_domain}:{canister_id}\n")); + } crate::fs::write(&file_path, content.as_bytes())?; Ok(()) } +/// Returns the custom domain entry for the II frontend canister, if II is enabled. +pub fn ii_custom_domain_entry(ii: bool, domain: &str) -> Option<(String, String)> { + if ii { + Some(( + format!("id.ai.{domain}"), + icp_canister_interfaces::internet_identity::INTERNET_IDENTITY_FRONTEND_CID.to_string(), + )) + } else { + None + } +} + /// Extracts the domain authority from a gateway URL for use in subdomain-based /// canister routing. /// @@ -110,7 +129,7 @@ mod tests { ); env_mappings.insert("staging".to_string(), staging_mappings); - write_custom_domains(dir.path(), "localhost", &env_mappings).unwrap(); + write_custom_domains(dir.path(), "localhost", &env_mappings, &[]).unwrap(); let content = std::fs::read_to_string(dir.path().join("custom-domains.txt")).unwrap(); // BTreeMap is ordered, so local comes before staging @@ -122,6 +141,38 @@ mod tests { ); } + #[test] + fn write_custom_domains_with_extra_entries() { + let dir = camino_tempfile::Utf8TempDir::new().unwrap(); + let env_mappings = BTreeMap::new(); + let extra = vec![( + "id.ai.localhost".to_string(), + "uqzsh-gqaaa-aaaaq-qaada-cai".to_string(), + )]; + + write_custom_domains(dir.path(), "localhost", &env_mappings, &extra).unwrap(); + + let content = std::fs::read_to_string(dir.path().join("custom-domains.txt")).unwrap(); + assert_eq!(content, "id.ai.localhost:uqzsh-gqaaa-aaaaq-qaada-cai\n"); + } + + #[test] + fn ii_custom_domain_entry_returns_entry_when_enabled() { + let entry = ii_custom_domain_entry(true, "localhost"); + assert_eq!( + entry, + Some(( + "id.ai.localhost".to_string(), + "uqzsh-gqaaa-aaaaq-qaada-cai".to_string() + )) + ); + } + + #[test] + fn ii_custom_domain_entry_returns_none_when_disabled() { + assert_eq!(ii_custom_domain_entry(false, "localhost"), None); + } + #[test] fn canister_gateway_url_with_friendly_domain() { let base: Url = "http://localhost:8000".parse().unwrap(); diff --git a/crates/icp/src/network/managed/docker.rs b/crates/icp/src/network/managed/docker.rs index 8a1a6eac..435f9b9c 100644 --- a/crates/icp/src/network/managed/docker.rs +++ b/crates/icp/src/network/managed/docker.rs @@ -5,11 +5,11 @@ use async_trait::async_trait; use bollard::{ Docker, errors::Error as BollardError, + models::{ContainerCreateBody, HostConfig, Mount, MountTypeEnum, PortBinding}, query_parameters::{ CreateContainerOptions, CreateImageOptions, InspectContainerOptions, RemoveContainerOptions, StartContainerOptions, StopContainerOptions, WaitContainerOptions, }, - secret::{ContainerCreateBody, HostConfig, Mount, MountTypeEnum, PortBinding}, }; use futures::{StreamExt, TryStreamExt}; use itertools::Itertools; diff --git a/crates/icp/src/network/managed/run.rs b/crates/icp/src/network/managed/run.rs index 8122f44b..ece48d1b 100644 --- a/crates/icp/src/network/managed/run.rs +++ b/crates/icp/src/network/managed/run.rs @@ -8,6 +8,7 @@ use ic_agent::{ identity::{AnonymousIdentity, Secp256k1Identity}, }; use ic_ledger_types::{AccountIdentifier, Memo, Subaccount, Tokens, TransferArgs, TransferResult}; +use ic_management_canister_types::CanisterSettings; use ic_utils::interfaces::management_canister::builders::CanisterInstallMode; use icp_canister_interfaces::{ cycles_ledger::{ @@ -19,7 +20,6 @@ use icp_canister_interfaces::{ NotifyMintArgs, NotifyMintResponse, }, icp_ledger::{ICP_LEDGER_BLOCK_FEE_E8S, ICP_LEDGER_PRINCIPAL}, - management_canister::CanisterSettingsArg, }; use icrc_ledger_types::icrc1::{ account::Account, @@ -241,10 +241,12 @@ async fn run_network_launcher( info!("Network started on port {}", instance.gateway_port); } + let gateway_url: Url = format!("http://{}:{}", gateway.host, gateway.port) + .parse() + .unwrap(); + let (candid_ui_canister_id, proxy_canister_id) = initialize_network( - &format!("http://{}:{}", gateway.host, gateway.port) - .parse() - .unwrap(), + &gateway_url, &instance.root_key, all_identities, default_identity, @@ -253,6 +255,8 @@ async fn run_network_launcher( ) .await?; + let ii = matches!(&config.mode, ManagedMode::Launcher(cfg) if cfg.ii); + network_root .with_write(async |root| -> Result<_, RunNetworkLauncherError> { // Acquire locks for all fixed ports @@ -274,6 +278,7 @@ async fn run_network_launcher( pocketic_instance_id: instance.pocketic_instance_id, candid_ui_canister_id, proxy_canister_id, + ii, status_dir: Some(status_dir_path.clone()), use_friendly_domains: instance.use_friendly_domains, }; @@ -284,6 +289,23 @@ async fn run_network_launcher( Ok(()) }) .await??; + + // Write initial custom-domains.txt with system canister entries (e.g. II) + if instance.use_friendly_domains + && let Some(domain) = crate::network::custom_domains::gateway_domain(&gateway_url) + { + let extra: Vec<_> = crate::network::custom_domains::ii_custom_domain_entry(ii, domain) + .into_iter() + .collect(); + if !extra.is_empty() { + let _ = crate::network::custom_domains::write_custom_domains( + &status_dir_path, + domain, + &std::collections::BTreeMap::new(), + &extra, + ); + } + } if background { info!("To stop the network, run `icp network stop`"); guard.defuse(); @@ -302,7 +324,7 @@ async fn run_network_launcher( } fn transform_native_launcher_to_container(config: &ManagedLauncherConfig) -> ManagedImageOptions { - use bollard::secret::PortBinding; + use bollard::models::PortBinding; use std::collections::HashMap; use super::docker::{docker_extra_hosts_for_addrs, translate_launcher_args_for_docker}; @@ -820,13 +842,9 @@ async fn install_proxy( let creation_args = if !controllers.is_empty() { Some(CreationArgs { subnet_selection: None, - settings: Some(CanisterSettingsArg { + settings: Some(CanisterSettings { controllers: Some(controllers.clone()), - freezing_threshold: None, - reserved_cycles_limit: None, - log_visibility: None, - memory_allocation: None, - compute_allocation: None, + ..Default::default() }), }) } else { diff --git a/crates/icp/src/project.rs b/crates/icp/src/project.rs index 60171bae..1e48d133 100644 --- a/crates/icp/src/project.rs +++ b/crates/icp/src/project.rs @@ -8,7 +8,7 @@ use crate::{ context::IC_ROOT_KEY, fs, manifest::{ - CANISTER_MANIFEST, CanisterManifest, EnvironmentManifest, InitArgsFormat, Item, + ArgsFormat, CANISTER_MANIFEST, CanisterManifest, EnvironmentManifest, Item, LoadManifestFromPathError, ManifestInitArgs, NetworkManifest, ProjectManifest, ProjectRootLocateError, canister::{Instructions, SyncSteps}, @@ -108,12 +108,12 @@ fn resolve_manifest_init_args( match manifest_init_args { ManifestInitArgs::String(content) => Ok(InitArgs::Text { content: content.trim().to_owned(), - format: InitArgsFormat::Candid, + format: ArgsFormat::Candid, }), ManifestInitArgs::Path { path, format } => { let file_path = base_path.join(path); match format { - InitArgsFormat::Bin => { + ArgsFormat::Bin => { let bytes = fs::read(&file_path).context(ReadInitArgsSnafu { canister })?; Ok(InitArgs::Binary(bytes)) } @@ -128,7 +128,7 @@ fn resolve_manifest_init_args( } } ManifestInitArgs::Value { value, format } => match format { - InitArgsFormat::Bin => BinFormatInlineContentSnafu { canister }.fail(), + ArgsFormat::Bin => BinFormatInlineContentSnafu { canister }.fail(), fmt => Ok(InitArgs::Text { content: value.trim().to_owned(), format: fmt.clone(), diff --git a/dist-workspace.toml b/dist-workspace.toml index 28d9b2ef..a222a5e6 100644 --- a/dist-workspace.toml +++ b/dist-workspace.toml @@ -4,19 +4,13 @@ members = ["cargo:."] # Config for 'dist' [dist] # The preferred dist version to use in CI (Cargo.toml SemVer syntax) -cargo-dist-version = "0.30.3" +cargo-dist-version = "0.31.0" # CI backends to support ci = "github" # The installers to generate for each app installers = ["shell", "powershell"] # Target platforms to build apps for (Rust target-triple syntax) -targets = [ - "aarch64-apple-darwin", - "aarch64-unknown-linux-gnu", - "x86_64-apple-darwin", - "x86_64-unknown-linux-gnu", - "x86_64-pc-windows-msvc", -] +targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] # Path that installers should place binaries in install-path = "CARGO_HOME" # Whether to install an updater program diff --git a/docs-site/.icp/data/mappings/ic.ids.json b/docs-site/.icp/data/mappings/ic.ids.json new file mode 100644 index 00000000..14cdb9e5 --- /dev/null +++ b/docs-site/.icp/data/mappings/ic.ids.json @@ -0,0 +1,3 @@ +{ + "docs": "ak73b-maaaa-aaaad-qlbgq-cai" +} diff --git a/docs-site/README.md b/docs-site/README.md index 14658e94..2bb855f8 100644 --- a/docs-site/README.md +++ b/docs-site/README.md @@ -10,15 +10,19 @@ The documentation site is built with [Astro](https://astro.build/) and [Starligh ``` docs-site/ -├── astro.config.mjs # Starlight configuration (sidebar, theme) +├── astro.config.mjs # Starlight configuration (sidebar, theme, SEO meta tags, JSON-LD) +├── versions.json # Version registry (controls root redirect and robots.txt) ├── plugins/ -│ └── rehype-rewrite-links.mjs # Rewrites .md links for Starlight's clean URLs +│ ├── rehype-rewrite-links.mjs # Rewrites .md links for Starlight's clean URLs +│ └── astro-agent-docs.mjs # Generates llms.txt, feed.xml, og-image.png, sitemap lastmod ├── src/ │ ├── content.config.ts # Content loader configuration -│ ├── components/ # Custom Starlight component overrides +│ ├── components/ # Custom Starlight component overrides (Footer, Banner, SiteTitle) │ ├── assets/ # Logo and static assets │ └── styles/ # DFINITY theme CSS -├── public/ # Static files (favicon, etc.) +├── public/ +│ ├── og-image.svg # Source SVG for the social sharing image (converted to PNG at build time) +│ └── ... # Other static files (favicon, well-known, etc.) └── package.json # Dependencies and scripts ``` @@ -35,6 +39,12 @@ docs-site/ 2. **Rehype plugin** (`plugins/rehype-rewrite-links.mjs`) strips `.md` extensions from relative links and adjusts paths for Astro's directory-based output 3. **DFINITY theme CSS** is applied for consistent branding 4. **Static HTML** is produced in `dist/` +5. **`astro-agent-docs` plugin** runs in the `astro:build:done` hook and generates additional files: + - `llms.txt` — LLM/agent-friendly page index (agentdocsspec.com) + - `llms-full.txt` — full content dump for RAG pipelines + - `feed.xml` — RSS 2.0 feed with git-accurate publish dates per page + - `og-image.png` — social sharing preview image, rendered from `public/og-image.svg` via `@resvg/resvg-js` + - Sitemap `` injection — adds git commit dates to the Starlight-generated sitemap Source docs use `.md` extensions in links (GitHub-friendly), and the rehype plugin transforms them to clean URLs at build time. @@ -93,26 +103,45 @@ Removes `dist/` and `.astro/` directories - `build` - Builds for production - `preview` - Previews production build locally - `clean` - Removes build artifacts (`dist/`, `.astro/`) +- `test:versions` - Simulates the full multi-version production layout locally for testing the version switcher UI ## Deployment -The site is automatically deployed to GitHub Pages: -- **URL**: https://dfinity.github.io/icp-cli/ -- **Workflow**: `.github/workflows/docs.yml` -- **Trigger**: Push to `main` branch (docs or docs-site changes) +The site is hosted on an IC asset canister and served at `https://cli.internetcomputer.org`. -The workflow: -1. Installs dependencies -2. Runs `npm run build` -3. Uploads the `dist/` directory as a GitHub Pages artifact -4. Deploys to GitHub Pages +**Canister ID**: `ak73b-maaaa-aaaad-qlbgq-cai` + +### How it works + +1. **`.github/workflows/docs.yml`** builds documentation and pushes built files to the `docs-deployment` branch (one directory per version: `0.1/`, `0.2/`, `main/`, etc.) +2. **`.github/workflows/docs-deploy.yml`** is called by `docs.yml` after publish jobs complete and deploys the entire `docs-deployment` branch to the IC asset canister + +### Triggers + +- **Push to `main`**: Rebuilds `/main/` docs and root files (`index.html`, `versions.json`, `robots.txt`, `sitemap.xml`, IC config). Also copies `og-image.png`, `llms.txt`, `llms-full.txt`, and `feed.xml` from the latest versioned deployment to the root. +- **Tags (`v*`)**: Builds versioned docs (e.g., `v0.2.0` → `/0.2/`) +- **Branches (`docs/v*`)**: Updates versioned docs (e.g., `docs/v0.1` → `/0.1/`) + +### Root-level files + +Several files must live at the deployment root (not inside a versioned subfolder) to be discovered correctly: + +- **`robots.txt`** — generated dynamically by the CI `publish-root-files` job from `versions.json`; allows only the latest version's path, disallows old versions, and disallows `/main/` (except when no releases exist yet and `/main/` is the fallback). Never placed in versioned build output. +- **`sitemap.xml`** — root sitemap index pointing directly to the latest version's `sitemap-0.xml` (spec-compliant: a sitemapindex must reference sitemaps, not other sitemapindex files); generated by `publish-root-files`. +- **`og-image.png`** — the `og:image` meta tag always references `https://cli.internetcomputer.org/og-image.png`. The CI `publish-root-files` job copies it from the latest versioned build folder to root after each versioned deployment. +- **`llms.txt` / `llms-full.txt`** — same pattern as `og-image.png`; `publish-root-files` prepends a version navigation header to the root `llms.txt`. +- **`feed.xml`** — same pattern; copied from the latest versioned folder to root. + +### Legacy redirect + +The old GitHub Pages site at `https://dfinity.github.io/icp-cli/` redirects all paths to `https://cli.internetcomputer.org/`. ## Configuration ### Site Settings In `astro.config.mjs`: -- `site`: Base URL for the site -- `base`: Base path (currently `/` for root domain) +- `site`: Base URL (`https://cli.internetcomputer.org` in production) +- `base`: Version path (set via `PUBLIC_BASE_PATH`, e.g., `/0.2/`, `/main/`) - `title`, `description`: Site metadata - `logo`: ICP logo configuration - `favicon`: Site favicon diff --git a/docs-site/astro.config.mjs b/docs-site/astro.config.mjs index dfcb4363..b35666ab 100644 --- a/docs-site/astro.config.mjs +++ b/docs-site/astro.config.mjs @@ -4,13 +4,14 @@ import rehypeExternalLinks from 'rehype-external-links'; import rehypeRewriteLinks from './plugins/rehype-rewrite-links.mjs'; import agentDocs from './plugins/astro-agent-docs.mjs'; +const SITE = (process.env.PUBLIC_SITE || 'https://cli.internetcomputer.org').replace(/\/$/, ''); + // https://astro.build/config export default defineConfig({ - site: process.env.PUBLIC_SITE, - // For versioned deployments: /icp-cli/0.1/, /icp-cli/0.2/, etc. - // For non-versioned: /icp-cli/ in production, / in development - // Defaults are set in the workflow, not here - base: process.env.PUBLIC_BASE_PATH || (process.env.NODE_ENV === 'production' ? process.env.PUBLIC_BASE_PREFIX + '/' : '/'), + site: SITE, + // For versioned deployments: /0.1/, /0.2/, etc. + // PUBLIC_BASE_PATH is set per-version in CI (e.g., /0.2/, /main/) + base: process.env.PUBLIC_BASE_PATH || '/', markdown: { rehypePlugins: [ // Rewrite relative .md links for Astro's directory-based output @@ -27,6 +28,7 @@ export default defineConfig({ components: { SiteTitle: './src/components/SiteTitle.astro', Banner: './src/components/Banner.astro', + Footer: './src/components/Footer.astro', }, head: [ { @@ -35,11 +37,63 @@ export default defineConfig({ tag: 'link', attrs: { rel: 'help', - href: `${process.env.PUBLIC_BASE_PATH || '/'}llms.txt`, + href: '/llms.txt', type: 'text/plain', title: 'LLM-friendly documentation index', }, }, + { + tag: 'link', + attrs: { + rel: 'alternate', + type: 'application/rss+xml', + title: 'ICP CLI Documentation', + href: '/feed.xml', + }, + }, + { + tag: 'meta', + attrs: { name: 'robots', content: 'index, follow, max-image-preview:large' }, + }, + { + tag: 'meta', + attrs: { name: 'author', content: 'DFINITY Foundation' }, + }, + { + tag: 'meta', + attrs: { property: 'og:image', content: `${SITE}/og-image.png` }, + }, + { + tag: 'meta', + attrs: { property: 'og:image:alt', content: 'ICP CLI Documentation — Build on Internet Computer' }, + }, + { + tag: 'meta', + attrs: { name: 'twitter:image', content: `${SITE}/og-image.png` }, + }, + { + tag: 'script', + attrs: { type: 'application/ld+json' }, + content: JSON.stringify({ + '@context': 'https://schema.org', + '@graph': [ + { + '@type': 'WebSite', + '@id': `${SITE}/#website`, + 'name': 'ICP CLI', + 'description': 'Command-line tool for developing and deploying applications on the Internet Computer Protocol (ICP)', + 'url': SITE, + 'publisher': { '@id': `${SITE}/#organization` }, + }, + { + '@type': 'Organization', + '@id': `${SITE}/#organization`, + 'name': 'DFINITY Foundation', + 'url': 'https://dfinity.org', + }, + ], + }), + }, { tag: 'script', attrs: {}, diff --git a/docs-site/icp.yaml b/docs-site/icp.yaml new file mode 100644 index 00000000..dd940646 --- /dev/null +++ b/docs-site/icp.yaml @@ -0,0 +1,9 @@ +# yaml-language-server: $schema=https://github.com/dfinity/icp-cli/raw/refs/heads/main/docs/schemas/icp-yaml-schema.json + +canisters: + - name: docs + recipe: + type: "@dfinity/asset-canister@v2.1.0" + configuration: + version: 0.29.2 + dir: . diff --git a/docs-site/package-lock.json b/docs-site/package-lock.json index 5a06c7f2..318f4189 100644 --- a/docs-site/package-lock.json +++ b/docs-site/package-lock.json @@ -10,11 +10,13 @@ "dependencies": { "@astrojs/check": "^0.9.4", "@astrojs/starlight": "^0.37.3", - "astro": "^5.6.1", + "astro": "^5.18.1", "gray-matter": "^4.0.3", "sharp": "^0.33.5" }, "devDependencies": { + "@fontsource/inter": "^5.1.1", + "@resvg/resvg-js": "^2.6.2", "rehype-external-links": "^3.0.0", "typescript": "^5.7.3", "unist-util-visit": "^5.1.0" @@ -51,9 +53,9 @@ "license": "MIT" }, "node_modules/@astrojs/language-server": { - "version": "2.16.4", - "resolved": "https://registry.npmjs.org/@astrojs/language-server/-/language-server-2.16.4.tgz", - "integrity": "sha512-42oqz9uX+hU1/rFniJvtYW9FbfZJ6syM2fYZFi7Ub71/kOvF1GSeMS8sA3Ogs3iOeNUWefk/ImwBiiHeNmJfSA==", + "version": "2.16.6", + "resolved": "https://registry.npmjs.org/@astrojs/language-server/-/language-server-2.16.6.tgz", + "integrity": "sha512-N990lu+HSFiG57owR0XBkr02BYMgiLCshLf+4QG4v6jjSWkBeQGnzqi+E1L08xFPPJ7eEeXnxPXGLaVv5pa4Ug==", "license": "MIT", "dependencies": { "@astrojs/compiler": "^2.13.1", @@ -65,14 +67,14 @@ "@volar/language-service": "~2.4.28", "muggle-string": "^0.4.1", "tinyglobby": "^0.2.15", - "volar-service-css": "0.0.68", - "volar-service-emmet": "0.0.68", - "volar-service-html": "0.0.68", - "volar-service-prettier": "0.0.68", - "volar-service-typescript": "0.0.68", - "volar-service-typescript-twoslash-queries": "0.0.68", - "volar-service-yaml": "0.0.68", - "vscode-html-languageservice": "^5.6.1", + "volar-service-css": "0.0.70", + "volar-service-emmet": "0.0.70", + "volar-service-html": "0.0.70", + "volar-service-prettier": "0.0.70", + "volar-service-typescript": "0.0.70", + "volar-service-typescript-twoslash-queries": "0.0.70", + "volar-service-yaml": "0.0.70", + "vscode-html-languageservice": "^5.6.2", "vscode-uri": "^3.1.0" }, "bin": { @@ -838,6 +840,16 @@ "@expressive-code/core": "^0.41.6" } }, + "node_modules/@fontsource/inter": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.2.8.tgz", + "integrity": "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg==", + "dev": true, + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@img/colour": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", @@ -1437,6 +1449,234 @@ "win32" ] }, + "node_modules/@resvg/resvg-js": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js/-/resvg-js-2.6.2.tgz", + "integrity": "sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@resvg/resvg-js-android-arm-eabi": "2.6.2", + "@resvg/resvg-js-android-arm64": "2.6.2", + "@resvg/resvg-js-darwin-arm64": "2.6.2", + "@resvg/resvg-js-darwin-x64": "2.6.2", + "@resvg/resvg-js-linux-arm-gnueabihf": "2.6.2", + "@resvg/resvg-js-linux-arm64-gnu": "2.6.2", + "@resvg/resvg-js-linux-arm64-musl": "2.6.2", + "@resvg/resvg-js-linux-x64-gnu": "2.6.2", + "@resvg/resvg-js-linux-x64-musl": "2.6.2", + "@resvg/resvg-js-win32-arm64-msvc": "2.6.2", + "@resvg/resvg-js-win32-ia32-msvc": "2.6.2", + "@resvg/resvg-js-win32-x64-msvc": "2.6.2" + } + }, + "node_modules/@resvg/resvg-js-android-arm-eabi": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm-eabi/-/resvg-js-android-arm-eabi-2.6.2.tgz", + "integrity": "sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-android-arm64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm64/-/resvg-js-android-arm64-2.6.2.tgz", + "integrity": "sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-darwin-arm64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-arm64/-/resvg-js-darwin-arm64-2.6.2.tgz", + "integrity": "sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-darwin-x64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-x64/-/resvg-js-darwin-x64-2.6.2.tgz", + "integrity": "sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-arm-gnueabihf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm-gnueabihf/-/resvg-js-linux-arm-gnueabihf-2.6.2.tgz", + "integrity": "sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-arm64-gnu": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-gnu/-/resvg-js-linux-arm64-gnu-2.6.2.tgz", + "integrity": "sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-arm64-musl": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-musl/-/resvg-js-linux-arm64-musl-2.6.2.tgz", + "integrity": "sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-x64-gnu": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-gnu/-/resvg-js-linux-x64-gnu-2.6.2.tgz", + "integrity": "sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-x64-musl": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-musl/-/resvg-js-linux-x64-musl-2.6.2.tgz", + "integrity": "sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-win32-arm64-msvc": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-arm64-msvc/-/resvg-js-win32-arm64-msvc-2.6.2.tgz", + "integrity": "sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-win32-ia32-msvc": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-ia32-msvc/-/resvg-js-win32-ia32-msvc-2.6.2.tgz", + "integrity": "sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-win32-x64-msvc": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-x64-msvc/-/resvg-js-win32-x64-msvc-2.6.2.tgz", + "integrity": "sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@rollup/pluginutils": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", @@ -2142,199 +2382,650 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-iterate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/array-iterate/-/array-iterate-2.0.1.tgz", + "integrity": "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, + "node_modules/astro": { + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/astro/-/astro-5.18.1.tgz", + "integrity": "sha512-m4VWilWZ+Xt6NPoYzC4CgGZim/zQUO7WFL0RHCH0AiEavF1153iC3+me2atDvXpf/yX4PyGUeD8wZLq1cirT3g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@astrojs/compiler": "^2.13.0", + "@astrojs/internal-helpers": "0.7.6", + "@astrojs/markdown-remark": "6.3.11", + "@astrojs/telemetry": "3.3.0", + "@capsizecss/unpack": "^4.0.0", + "@oslojs/encoding": "^1.1.0", + "@rollup/pluginutils": "^5.3.0", + "acorn": "^8.15.0", + "aria-query": "^5.3.2", + "axobject-query": "^4.1.0", + "boxen": "8.0.1", + "ci-info": "^4.3.1", + "clsx": "^2.1.1", + "common-ancestor-path": "^1.0.1", + "cookie": "^1.1.1", + "cssesc": "^3.0.0", + "debug": "^4.4.3", + "deterministic-object-hash": "^2.0.2", + "devalue": "^5.6.2", + "diff": "^8.0.3", + "dlv": "^1.1.3", + "dset": "^3.1.4", + "es-module-lexer": "^1.7.0", + "esbuild": "^0.27.3", + "estree-walker": "^3.0.3", + "flattie": "^1.1.1", + "fontace": "~0.4.0", + "github-slugger": "^2.0.0", + "html-escaper": "3.0.3", + "http-cache-semantics": "^4.2.0", + "import-meta-resolve": "^4.2.0", + "js-yaml": "^4.1.1", + "magic-string": "^0.30.21", + "magicast": "^0.5.1", + "mrmime": "^2.0.1", + "neotraverse": "^0.6.18", + "p-limit": "^6.2.0", + "p-queue": "^8.1.1", + "package-manager-detector": "^1.6.0", + "piccolore": "^0.1.3", + "picomatch": "^4.0.3", + "prompts": "^2.4.2", + "rehype": "^13.0.2", + "semver": "^7.7.3", + "shiki": "^3.21.0", + "smol-toml": "^1.6.0", + "svgo": "^4.0.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tsconfck": "^3.1.6", + "ultrahtml": "^1.6.0", + "unifont": "~0.7.3", + "unist-util-visit": "^5.0.0", + "unstorage": "^1.17.4", + "vfile": "^6.0.3", + "vite": "^6.4.1", + "vitefu": "^1.1.1", + "xxhash-wasm": "^1.1.0", + "yargs-parser": "^21.1.1", + "yocto-spinner": "^0.2.3", + "zod": "^3.25.76", + "zod-to-json-schema": "^3.25.1", + "zod-to-ts": "^1.2.0" + }, + "bin": { + "astro": "astro.js" + }, + "engines": { + "node": "18.20.8 || ^20.3.0 || >=22.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/astrodotbuild" + }, + "optionalDependencies": { + "sharp": "^0.34.0" + } + }, + "node_modules/astro-expressive-code": { + "version": "0.41.6", + "resolved": "https://registry.npmjs.org/astro-expressive-code/-/astro-expressive-code-0.41.6.tgz", + "integrity": "sha512-l47tb1uhmVIebHUkw+HEPtU/av0G4O8Q34g2cbkPvC7/e9ZhANcjUUciKt9Hp6gSVDdIuXBBLwJQn2LkeGMOAw==", + "license": "MIT", + "dependencies": { + "rehype-expressive-code": "^0.41.6" + }, + "peerDependencies": { + "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta" + } + }, + "node_modules/astro/node_modules/@astrojs/internal-helpers": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.7.6.tgz", + "integrity": "sha512-GOle7smBWKfMSP8osUIGOlB5kaHdQLV3foCsf+5Q9Wsuu+C6Fs3Ez/ttXmhjZ1HkSgsogcM1RXSjjOVieHq16Q==", + "license": "MIT" + }, + "node_modules/astro/node_modules/@astrojs/markdown-remark": { + "version": "6.3.11", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-6.3.11.tgz", + "integrity": "sha512-hcaxX/5aC6lQgHeGh1i+aauvSwIT6cfyFjKWvExYSxUhZZBBdvCliOtu06gbQyhbe0pGJNoNmqNlQZ5zYUuIyQ==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.7.6", + "@astrojs/prism": "3.3.0", + "github-slugger": "^2.0.0", + "hast-util-from-html": "^2.0.3", + "hast-util-to-text": "^4.0.2", + "import-meta-resolve": "^4.2.0", + "js-yaml": "^4.1.1", + "mdast-util-definitions": "^6.0.0", + "rehype-raw": "^7.0.0", + "rehype-stringify": "^10.0.1", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "remark-smartypants": "^3.0.2", + "shiki": "^3.21.0", + "smol-toml": "^1.6.0", + "unified": "^11.0.5", + "unist-util-remove-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "unist-util-visit-parents": "^6.0.2", + "vfile": "^6.0.3" + } + }, + "node_modules/astro/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/astro/node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/astro/node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/astro/node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/astro/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/astro/node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/astro/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/astro/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/astro/node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/astro/node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/astro/node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/astro/node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/astro/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/astro/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/astro/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/astro/node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/astro/node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "node_modules/astro/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "node": ">=18" } }, - "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "node_modules/astro/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=18" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, + "node_modules/astro/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">= 8" + "node": ">=18" } }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "node_modules/astro/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "node": ">=18" } }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, - "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", - "license": "Apache-2.0", + "node_modules/astro/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], "engines": { - "node": ">= 0.4" + "node": ">=18" } }, - "node_modules/array-iterate": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/array-iterate/-/array-iterate-2.0.1.tgz", - "integrity": "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==", + "node_modules/astro/node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" } }, - "node_modules/astring": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", - "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "node_modules/astro/node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], "license": "MIT", - "bin": { - "astring": "bin/astring" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, - "node_modules/astro": { - "version": "5.16.11", - "resolved": "https://registry.npmjs.org/astro/-/astro-5.16.11.tgz", - "integrity": "sha512-Z7kvkTTT5n6Hn5lCm6T3WU6pkxx84Hn25dtQ6dR7ATrBGq9eVa8EuB/h1S8xvaoVyCMZnIESu99Z9RJfdLRLDA==", + "node_modules/astro/node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], "license": "MIT", - "peer": true, - "dependencies": { - "@astrojs/compiler": "^2.13.0", - "@astrojs/internal-helpers": "0.7.5", - "@astrojs/markdown-remark": "6.3.10", - "@astrojs/telemetry": "3.3.0", - "@capsizecss/unpack": "^4.0.0", - "@oslojs/encoding": "^1.1.0", - "@rollup/pluginutils": "^5.3.0", - "acorn": "^8.15.0", - "aria-query": "^5.3.2", - "axobject-query": "^4.1.0", - "boxen": "8.0.1", - "ci-info": "^4.3.1", - "clsx": "^2.1.1", - "common-ancestor-path": "^1.0.1", - "cookie": "^1.1.1", - "cssesc": "^3.0.0", - "debug": "^4.4.3", - "deterministic-object-hash": "^2.0.2", - "devalue": "^5.6.2", - "diff": "^8.0.3", - "dlv": "^1.1.3", - "dset": "^3.1.4", - "es-module-lexer": "^1.7.0", - "esbuild": "^0.25.0", - "estree-walker": "^3.0.3", - "flattie": "^1.1.1", - "fontace": "~0.4.0", - "github-slugger": "^2.0.0", - "html-escaper": "3.0.3", - "http-cache-semantics": "^4.2.0", - "import-meta-resolve": "^4.2.0", - "js-yaml": "^4.1.1", - "magic-string": "^0.30.21", - "magicast": "^0.5.1", - "mrmime": "^2.0.1", - "neotraverse": "^0.6.18", - "p-limit": "^6.2.0", - "p-queue": "^8.1.1", - "package-manager-detector": "^1.6.0", - "piccolore": "^0.1.3", - "picomatch": "^4.0.3", - "prompts": "^2.4.2", - "rehype": "^13.0.2", - "semver": "^7.7.3", - "shiki": "^3.20.0", - "smol-toml": "^1.6.0", - "svgo": "^4.0.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tsconfck": "^3.1.6", - "ultrahtml": "^1.6.0", - "unifont": "~0.7.1", - "unist-util-visit": "^5.0.0", - "unstorage": "^1.17.3", - "vfile": "^6.0.3", - "vite": "^6.4.1", - "vitefu": "^1.1.1", - "xxhash-wasm": "^1.1.0", - "yargs-parser": "^21.1.1", - "yocto-spinner": "^0.2.3", - "zod": "^3.25.76", - "zod-to-json-schema": "^3.25.1", - "zod-to-ts": "^1.2.0" - }, - "bin": { - "astro": "astro.js" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "18.20.8 || ^20.3.0 || >=22.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/astrodotbuild" - }, - "optionalDependencies": { - "sharp": "^0.34.0" + "node": ">=18" } }, - "node_modules/astro-expressive-code": { - "version": "0.41.6", - "resolved": "https://registry.npmjs.org/astro-expressive-code/-/astro-expressive-code-0.41.6.tgz", - "integrity": "sha512-l47tb1uhmVIebHUkw+HEPtU/av0G4O8Q34g2cbkPvC7/e9ZhANcjUUciKt9Hp6gSVDdIuXBBLwJQn2LkeGMOAw==", + "node_modules/astro/node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], "license": "MIT", - "dependencies": { - "rehype-expressive-code": "^0.41.6" - }, - "peerDependencies": { - "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, "node_modules/astro/node_modules/@img/sharp-darwin-arm64": { @@ -2698,6 +3389,47 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/astro/node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, "node_modules/astro/node_modules/sharp": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", @@ -3270,9 +4002,9 @@ } }, "node_modules/defu": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", - "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.6.tgz", + "integrity": "sha512-f8mefEW4WIVg4LckePx3mALjQSPQgFlg9U8yaPdlsbdYcHQyj9n2zL2LJEA52smeYxOvmd/nB7TpMtHGMTHcug==", "license": "MIT" }, "node_modules/dequal": { @@ -3856,9 +4588,9 @@ } }, "node_modules/h3": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.5.tgz", - "integrity": "sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==", + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.9.tgz", + "integrity": "sha512-H7UPnyIupUOYUQu7f2x7ABVeMyF/IbJjqn20WSXpMdnQB260luADUkSgJU7QTWLutq8h3tUayMQ1DdbSYX5LkA==", "license": "MIT", "dependencies": { "cookie-es": "^1.2.2", @@ -4509,12 +5241,6 @@ "node": ">= 8" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -5909,9 +6635,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", "engines": { "node": ">=12" @@ -5988,9 +6714,9 @@ } }, "node_modules/prettier": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.0.tgz", - "integrity": "sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "license": "MIT", "peer": true, "bin": { @@ -6643,9 +7369,9 @@ "license": "MIT" }, "node_modules/smol-toml": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.0.tgz", - "integrity": "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz", + "integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==", "license": "BSD-3-Clause", "engines": { "node": ">= 18" @@ -7274,9 +8000,9 @@ } }, "node_modules/vite": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", - "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", "license": "MIT", "peer": true, "dependencies": { @@ -7368,9 +8094,9 @@ } }, "node_modules/volar-service-css": { - "version": "0.0.68", - "resolved": "https://registry.npmjs.org/volar-service-css/-/volar-service-css-0.0.68.tgz", - "integrity": "sha512-lJSMh6f3QzZ1tdLOZOzovLX0xzAadPhx8EKwraDLPxBndLCYfoTvnNuiFFV8FARrpAlW5C0WkH+TstPaCxr00Q==", + "version": "0.0.70", + "resolved": "https://registry.npmjs.org/volar-service-css/-/volar-service-css-0.0.70.tgz", + "integrity": "sha512-K1qyOvBpE3rzdAv3e4/6Rv5yizrYPy5R/ne3IWCAzLBuMO4qBMV3kSqWzj6KUVe6S0AnN6wxF7cRkiaKfYMYJw==", "license": "MIT", "dependencies": { "vscode-css-languageservice": "^6.3.0", @@ -7387,9 +8113,9 @@ } }, "node_modules/volar-service-emmet": { - "version": "0.0.68", - "resolved": "https://registry.npmjs.org/volar-service-emmet/-/volar-service-emmet-0.0.68.tgz", - "integrity": "sha512-nHvixrRQ83EzkQ4G/jFxu9Y4eSsXS/X2cltEPDM+K9qZmIv+Ey1w0tg1+6caSe8TU5Hgw4oSTwNMf/6cQb3LzQ==", + "version": "0.0.70", + "resolved": "https://registry.npmjs.org/volar-service-emmet/-/volar-service-emmet-0.0.70.tgz", + "integrity": "sha512-xi5bC4m/VyE3zy/n2CXspKeDZs3qA41tHLTw275/7dNWM/RqE2z3BnDICQybHIVp/6G1iOQj5c1qXMgQC08TNg==", "license": "MIT", "dependencies": { "@emmetio/css-parser": "^0.4.1", @@ -7407,9 +8133,9 @@ } }, "node_modules/volar-service-html": { - "version": "0.0.68", - "resolved": "https://registry.npmjs.org/volar-service-html/-/volar-service-html-0.0.68.tgz", - "integrity": "sha512-fru9gsLJxy33xAltXOh4TEdi312HP80hpuKhpYQD4O5hDnkNPEBdcQkpB+gcX0oK0VxRv1UOzcGQEUzWCVHLfA==", + "version": "0.0.70", + "resolved": "https://registry.npmjs.org/volar-service-html/-/volar-service-html-0.0.70.tgz", + "integrity": "sha512-eR6vCgMdmYAo4n+gcT7DSyBQbwB8S3HZZvSagTf0sxNaD4WppMCFfpqWnkrlGStPKMZvMiejRRVmqsX9dYcTvQ==", "license": "MIT", "dependencies": { "vscode-html-languageservice": "^5.3.0", @@ -7426,9 +8152,9 @@ } }, "node_modules/volar-service-prettier": { - "version": "0.0.68", - "resolved": "https://registry.npmjs.org/volar-service-prettier/-/volar-service-prettier-0.0.68.tgz", - "integrity": "sha512-grUmWHkHlebMOd6V8vXs2eNQUw/bJGJMjekh/EPf/p2ZNTK0Uyz7hoBRngcvGfJHMsSXZH8w/dZTForIW/4ihw==", + "version": "0.0.70", + "resolved": "https://registry.npmjs.org/volar-service-prettier/-/volar-service-prettier-0.0.70.tgz", + "integrity": "sha512-Z6BCFSpGVCd8BPAsZ785Kce1BGlWd5ODqmqZGVuB14MJvrR4+CYz6cDy4F+igmE1gMifqfvMhdgT8Aud4M5ngg==", "license": "MIT", "dependencies": { "vscode-uri": "^3.0.8" @@ -7447,9 +8173,9 @@ } }, "node_modules/volar-service-typescript": { - "version": "0.0.68", - "resolved": "https://registry.npmjs.org/volar-service-typescript/-/volar-service-typescript-0.0.68.tgz", - "integrity": "sha512-z7B/7CnJ0+TWWFp/gh2r5/QwMObHNDiQiv4C9pTBNI2Wxuwymd4bjEORzrJ/hJ5Yd5+OzeYK+nFCKevoGEEeKw==", + "version": "0.0.70", + "resolved": "https://registry.npmjs.org/volar-service-typescript/-/volar-service-typescript-0.0.70.tgz", + "integrity": "sha512-l46Bx4cokkUedTd74ojO5H/zqHZJ8SUuyZ0IB8JN4jfRqUM3bQFBHoOwlZCyZmOeO0A3RQNkMnFclxO4c++gsg==", "license": "MIT", "dependencies": { "path-browserify": "^1.0.1", @@ -7469,9 +8195,9 @@ } }, "node_modules/volar-service-typescript-twoslash-queries": { - "version": "0.0.68", - "resolved": "https://registry.npmjs.org/volar-service-typescript-twoslash-queries/-/volar-service-typescript-twoslash-queries-0.0.68.tgz", - "integrity": "sha512-NugzXcM0iwuZFLCJg47vI93su5YhTIweQuLmZxvz5ZPTaman16JCvmDZexx2rd5T/75SNuvvZmrTOTNYUsfe5w==", + "version": "0.0.70", + "resolved": "https://registry.npmjs.org/volar-service-typescript-twoslash-queries/-/volar-service-typescript-twoslash-queries-0.0.70.tgz", + "integrity": "sha512-IdD13Z9N2Bu8EM6CM0fDV1E69olEYGHDU25X51YXmq8Y0CmJ2LNj6gOiBJgpS5JGUqFzECVhMNBW7R0sPdRTMQ==", "license": "MIT", "dependencies": { "vscode-uri": "^3.0.8" @@ -7486,13 +8212,13 @@ } }, "node_modules/volar-service-yaml": { - "version": "0.0.68", - "resolved": "https://registry.npmjs.org/volar-service-yaml/-/volar-service-yaml-0.0.68.tgz", - "integrity": "sha512-84XgE02LV0OvTcwfqhcSwVg4of3MLNUWPMArO6Aj8YXqyEVnPu8xTEMY2btKSq37mVAPuaEVASI4e3ptObmqcA==", + "version": "0.0.70", + "resolved": "https://registry.npmjs.org/volar-service-yaml/-/volar-service-yaml-0.0.70.tgz", + "integrity": "sha512-0c8bXDBeoATF9F6iPIlOuYTuZAC4c+yi0siQo920u7eiBJk8oQmUmg9cDUbR4+Gl++bvGP4plj3fErbJuPqdcQ==", "license": "MIT", "dependencies": { "vscode-uri": "^3.0.8", - "yaml-language-server": "~1.19.2" + "yaml-language-server": "~1.20.0" }, "peerDependencies": { "@volar/language-service": "~2.4.0" @@ -7504,9 +8230,9 @@ } }, "node_modules/vscode-css-languageservice": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-6.3.9.tgz", - "integrity": "sha512-1tLWfp+TDM5ZuVWht3jmaY5y7O6aZmpeXLoHl5bv1QtRsRKt4xYGRMmdJa5Pqx/FTkgRbsna9R+Gn2xE+evVuA==", + "version": "6.3.10", + "resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-6.3.10.tgz", + "integrity": "sha512-eq5N9Er3fC4vA9zd9EFhyBG90wtCCuXgRSpAndaOgXMh1Wgep5lBgRIeDgjZBW9pa+332yC9+49cZMW8jcL3MA==", "license": "MIT", "dependencies": { "@vscode/l10n": "^0.0.18", @@ -7516,9 +8242,9 @@ } }, "node_modules/vscode-html-languageservice": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-5.6.1.tgz", - "integrity": "sha512-5Mrqy5CLfFZUgkyhNZLA1Ye5g12Cb/v6VM7SxUzZUaRKWMDz4md+y26PrfRTSU0/eQAl3XpO9m2og+GGtDMuaA==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-5.6.2.tgz", + "integrity": "sha512-ulCrSnFnfQ16YzvwnYUgEbUEl/ZG7u2eV27YhvLObSHKkb8fw1Z9cgsnUwjTEeDIdJDoTDTDpxuhQwoenoLNMg==", "license": "MIT", "dependencies": { "@vscode/l10n": "^0.0.18", @@ -7686,15 +8412,14 @@ } }, "node_modules/yaml-language-server": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/yaml-language-server/-/yaml-language-server-1.19.2.tgz", - "integrity": "sha512-9F3myNmJzUN/679jycdMxqtydPSDRAarSj3wPiF7pchEPnO9Dg07Oc+gIYLqXR4L+g+FSEVXXv2+mr54StLFOg==", + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/yaml-language-server/-/yaml-language-server-1.20.0.tgz", + "integrity": "sha512-qhjK/bzSRZ6HtTvgeFvjNPJGWdZ0+x5NREV/9XZWFjIGezew2b4r5JPy66IfOhd5OA7KeFwk1JfmEbnTvev0cA==", "license": "MIT", "dependencies": { "@vscode/l10n": "^0.0.18", "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", - "lodash": "4.17.21", "prettier": "^3.5.0", "request-light": "^0.5.7", "vscode-json-languageservice": "4.1.8", diff --git a/docs-site/package.json b/docs-site/package.json index d0a83b63..735b1ebe 100644 --- a/docs-site/package.json +++ b/docs-site/package.json @@ -7,16 +7,19 @@ "dev": "npm run clean && astro dev", "build": "astro build", "preview": "npm run build && astro preview", - "clean": "rm -rf dist .astro" + "clean": "rm -rf dist .astro", + "test:versions": "./test-version-switcher.sh" }, "dependencies": { "@astrojs/check": "^0.9.4", "@astrojs/starlight": "^0.37.3", - "astro": "^5.6.1", + "astro": "^5.18.1", "gray-matter": "^4.0.3", "sharp": "^0.33.5" }, "devDependencies": { + "@fontsource/inter": "^5.1.1", + "@resvg/resvg-js": "^2.6.2", "rehype-external-links": "^3.0.0", "typescript": "^5.7.3", "unist-util-visit": "^5.1.0" diff --git a/docs-site/plugins/astro-agent-docs.mjs b/docs-site/plugins/astro-agent-docs.mjs index 0297a3ed..370ecafe 100644 --- a/docs-site/plugins/astro-agent-docs.mjs +++ b/docs-site/plugins/astro-agent-docs.mjs @@ -12,6 +12,8 @@ import fs from "node:fs"; import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { Resvg } from "@resvg/resvg-js"; import { fileURLToPath } from "node:url"; import matter from "gray-matter"; @@ -283,6 +285,45 @@ function generateLlmsTxt(pages, siteUrl, basePath, cliSubPages) { return lines.join("\n"); } +const gitDateCache = new Map(); + +/** Get last git commit date (ISO 8601) for a file, or null if unavailable. */ +function getGitDate(filePath) { + if (gitDateCache.has(filePath)) return gitDateCache.get(filePath); + let result = null; + const { stdout, status } = spawnSync( + "git", + ["log", "-1", "--format=%cI", "--", filePath], + { encoding: "utf-8" } + ); + if (status === 0) result = stdout.trim() || null; + gitDateCache.set(filePath, result); + return result; +} + +/** Try .md then .mdx source; return git date for whichever exists. */ +function getPageGitDate(pageFile, docsDir) { + for (const f of [ + path.join(docsDir, pageFile), + path.join(docsDir, pageFile.replace(/\.md$/, ".mdx")), + ]) { + if (fs.existsSync(f)) { + const d = getGitDate(f); + if (d) return d; + } + } + return null; +} + +/** Escape special XML characters. */ +function escapeXml(str) { + return String(str) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} + export default function agentDocs() { let siteUrl = ""; let basePath = "/"; @@ -342,12 +383,109 @@ export default function agentDocs() { `Generated llms.txt (${llmsTxt.length} chars, ${pages.length} pages)` ); - // 3. Inject agent signaling directive into HTML pages + // 3. Generate llms-full.txt (full content dump for bulk ingestion / RAG pipelines) + const fullParts = [llmsTxt]; + for (const page of [...pages].sort((a, b) => a.file.localeCompare(b.file))) { + const mdContent = fs.readFileSync(path.join(outDir, page.file), "utf-8").replace(/^\uFEFF/, ""); + fullParts.push("\n---\n", mdContent); + } + fs.writeFileSync(path.join(outDir, "llms-full.txt"), fullParts.join("\n")); + logger.info(`Generated llms-full.txt (${pages.length} pages)`); + + // 4. Generate RSS feed + const base = (siteUrl + basePath).replace(/\/$/, ""); + const feedItems = pages + .map((p) => { + const slug = p.file.replace(/\.md$/, "").replace(/(?:^|\/)index$/, ""); + const url = slug ? `${base}/${slug}/` : `${base}/`; + const date = getPageGitDate(p.file, docsDir); + return { ...p, url, date }; + }) + .sort((a, b) => { + if (a.date && b.date) return b.date.localeCompare(a.date); + return a.date ? -1 : b.date ? 1 : 0; + }); + + const channelPubDate = feedItems.find((i) => i.date); + const feedXml = [ + '', + '', + " ", + " ICP CLI Documentation", + ` ${base}/`, + " Command-line tool for developing and deploying applications on the Internet Computer Protocol (ICP).", + " en-us", + " DFINITY Foundation", + ` ${new Date().toUTCString()}`, + channelPubDate + ? ` ${new Date(channelPubDate.date).toUTCString()}` + : "", + ` `, + ...feedItems.map((item) => + [ + " ", + ` ${escapeXml(item.title)}`, + ` ${item.url}`, + item.description + ? ` ${escapeXml(item.description)}` + : "", + item.date + ? ` ${new Date(item.date).toUTCString()}` + : "", + ` ${item.url}`, + " DFINITY Foundation", + " ", + ] + .filter(Boolean) + .join("\n") + ), + " ", + "", + ] + .filter(Boolean) + .join("\n"); + + fs.writeFileSync(path.join(outDir, "feed.xml"), feedXml); + logger.info(`Generated feed.xml (${feedItems.length} items)`); + + // 5. Inject accurate git-based lastmod into sitemap + const sitemapFiles = (await fs.promises.readdir(outDir)) + .filter((f) => /^sitemap-\d+\.xml$/.test(f)); + let lastmodCount = 0; + for (const sitemapFile of sitemapFiles) { + const sitemapPath = path.join(outDir, sitemapFile); + const content = fs.readFileSync(sitemapPath, "utf-8"); + const modified = content.replace( + /\s*([^<]+)<\/loc>\s*<\/url>/g, + (match, rawUrl) => { + const url = rawUrl.trim(); + const pathname = url + .replace(siteUrl, "") + .replace(basePath, "") + .replace(/^\//, "") + .replace(/\/$/, ""); + const pageFile = (pathname || "index") + ".md"; + let date = getPageGitDate(pageFile, docsDir); + if (!date && pathname) { + date = getPageGitDate(pathname + "/index.md", docsDir); + } + if (!date) return match; + lastmodCount++; + return `${url}${date.slice(0, 10)}`; + } + ); + fs.writeFileSync(sitemapPath, modified); + } + if (sitemapFiles.length > 0) { + logger.info(`Injected lastmod into ${lastmodCount} sitemap URLs`); + } + + // 6. Inject agent signaling directive into HTML pages // Places a visually-hidden blockquote right after so it appears // early in the document (within the first ~15%), before nav/sidebar. // Uses CSS clip-rect (not display:none) so it survives HTML-to-markdown // conversion. See: https://agentdocsspec.com - const llmsTxtUrl = `${basePath}llms.txt`; + const llmsTxtUrl = siteUrl ? `${siteUrl}/llms.txt` : `${basePath}llms.txt`; const directive = `
` + `

For AI agents: Documentation index at ` + @@ -370,7 +508,7 @@ export default function agentDocs() { } logger.info(`Injected agent signaling into ${injected} HTML pages`); - // 4. Alias sitemap-index.xml → sitemap.xml + // 7. Alias sitemap-index.xml → sitemap.xml // Astro's sitemap integration outputs sitemap-index.xml, but crawlers // and the agentdocsspec checker expect /sitemap.xml by convention. const sitemapIndex = path.join(outDir, "sitemap-index.xml"); @@ -379,6 +517,31 @@ export default function agentDocs() { fs.copyFileSync(sitemapIndex, sitemapAlias); logger.info("Copied sitemap-index.xml → sitemap.xml"); } + + // 8. Convert og-image.svg → og-image.png + // SVG is the source of truth; PNG is what og:image / twitter:image reference + // because Twitter/X rejects SVG for social sharing previews. + const ogSvgPath = path.join(outDir, "og-image.svg"); + if (fs.existsSync(ogSvgPath)) { + const fontDir = path.resolve("node_modules/@fontsource/inter/files"); + const fontBuffers = ["400", "500", "600", "700"] + .map((w) => { + const p = path.join(fontDir, `inter-latin-${w}-normal.woff`); + return fs.existsSync(p) ? fs.readFileSync(p) : null; + }) + .filter(Boolean); + + const svg = fs.readFileSync(ogSvgPath, "utf-8"); + const resvg = new Resvg(svg, { + font: fontBuffers.length > 0 + ? { fontBuffers, loadSystemFonts: false, defaultFontFamily: "Inter", sansSerifFamily: "Inter" } + : { loadSystemFonts: true }, + fitTo: { mode: "original" }, + }); + const pngBuffer = resvg.render().asPng(); + fs.writeFileSync(path.join(outDir, "og-image.png"), pngBuffer); + logger.info("Generated og-image.png from og-image.svg"); + } }, }, }; diff --git a/docs-site/public/.well-known/ic-domains b/docs-site/public/.well-known/ic-domains new file mode 100644 index 00000000..d540913d --- /dev/null +++ b/docs-site/public/.well-known/ic-domains @@ -0,0 +1 @@ +cli.internetcomputer.org diff --git a/docs-site/public/og-image.svg b/docs-site/public/og-image.svg new file mode 100644 index 00000000..e79026ec --- /dev/null +++ b/docs-site/public/og-image.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DOCUMENTATION + + + ICP CLI + + + Build and deploy Internet Computer apps from the terminal. + + + + + + + DFINITY Foundation · cli.internetcomputer.org + diff --git a/docs-site/src/components/Footer.astro b/docs-site/src/components/Footer.astro new file mode 100644 index 00000000..7b448a2e --- /dev/null +++ b/docs-site/src/components/Footer.astro @@ -0,0 +1,90 @@ +--- +import EditLink from 'virtual:starlight/components/EditLink'; +import LastUpdated from 'virtual:starlight/components/LastUpdated'; +import Pagination from 'virtual:starlight/components/Pagination'; +import config from 'virtual:starlight/user-config'; +import { Icon } from '@astrojs/starlight/components'; + +--- + +

+ + diff --git a/docs-site/test-version-switcher.sh b/docs-site/test-version-switcher.sh index f0b33161..0e402a73 100755 --- a/docs-site/test-version-switcher.sh +++ b/docs-site/test-version-switcher.sh @@ -19,8 +19,7 @@ cleanup() { # Set up trap to catch Ctrl+C and other termination signals trap cleanup SIGINT SIGTERM -# Configuration - matches production setup -BASE_PREFIX="/icp-cli" +# Configuration - matches production setup (versions at domain root) TEST_DIR="dist-test" TEST_PORT=4321 @@ -32,17 +31,16 @@ echo "" # Clean everything for a fresh start echo "Cleaning all previous builds and caches..." rm -rf "$TEST_DIR" dist .astro -mkdir -p "$TEST_DIR$BASE_PREFIX" +mkdir -p "$TEST_DIR" # Set common environment variables export NODE_ENV=production export PUBLIC_SITE=http://localhost:4321 -export PUBLIC_BASE_PREFIX="$BASE_PREFIX" # Function to build a version build_version() { local version=$1 - local version_path="${BASE_PREFIX}/${version}/" + local version_path="/${version}/" echo "" echo "Building version $version..." @@ -70,11 +68,11 @@ build_version() { echo " Built with BASE_URL: $BUILT_BASE" # Copy to test directory - mkdir -p "$TEST_DIR$BASE_PREFIX/$version" - cp -r dist/* "$TEST_DIR$BASE_PREFIX/$version/" + mkdir -p "$TEST_DIR/$version" + cp -r dist/* "$TEST_DIR/$version/" # Verify copy succeeded - local file_count=$(ls "$TEST_DIR$BASE_PREFIX/$version" | wc -l) + local file_count=$(ls "$TEST_DIR/$version" | wc -l) echo " Copied $file_count files to test directory" echo "✓ Version $version built successfully" @@ -88,7 +86,7 @@ build_version "main" # Generate test versions.json echo "" echo "Generating test versions.json..." -cat > "$TEST_DIR$BASE_PREFIX/versions.json" << 'EOF' +cat > "$TEST_DIR/versions.json" << 'EOF' { "$comment": "Test versions.json for local testing", "versions": [ @@ -104,24 +102,70 @@ cat > "$TEST_DIR$BASE_PREFIX/versions.json" << 'EOF' EOF echo "✓ versions.json created" +# Determine latest version (mirrors publish-root-files CI logic) +LATEST_VERSION=$(jq -r ".versions[] | select(.latest == true) | .version" "$TEST_DIR/versions.json") +if [[ -z "$LATEST_VERSION" ]]; then + LATEST_VERSION="main" +fi +echo "Latest version: $LATEST_VERSION" + # Generate redirect index.html echo "" echo "Generating root redirect..." -cat > "$TEST_DIR$BASE_PREFIX/index.html" << EOF +cat > "$TEST_DIR/index.html" << EOF - + Redirecting to latest version... -

Redirecting to latest version...

+

Redirecting to latest version...

EOF echo "✓ index.html created" +# Copy root-level files from latest version (mirrors publish-root-files CI logic) +echo "" +echo "Copying root-level files from $LATEST_VERSION/..." +for f in llms.txt llms-full.txt feed.xml og-image.png; do + if [ -f "$TEST_DIR/$LATEST_VERSION/$f" ]; then + cp "$TEST_DIR/$LATEST_VERSION/$f" "$TEST_DIR/$f" + echo "✓ Copied $f from $LATEST_VERSION/" + else + echo "⚠️ $f not found in $LATEST_VERSION/ — skipping" + fi +done + +# Generate robots.txt (mirrors publish-root-files CI logic) +# /main/ is disallowed unless it IS the latest version (no releases yet fallback). +{ + echo "User-agent: *" + echo "Allow: /${LATEST_VERSION}/" + for version in $(jq -r '.versions[].version' "$TEST_DIR/versions.json"); do + if [[ "$version" != "$LATEST_VERSION" ]]; then + echo "Disallow: /${version}/" + fi + done + if [[ "$LATEST_VERSION" != "main" ]]; then echo "Disallow: /main/"; fi + echo "" + echo "Sitemap: http://localhost:${TEST_PORT}/sitemap.xml" +} > "$TEST_DIR/robots.txt" +echo "✓ robots.txt generated" + +# Generate root sitemap.xml (mirrors publish-root-files CI logic) +{ + echo '' + echo '' + echo ' ' + echo " http://localhost:${TEST_PORT}/${LATEST_VERSION}/sitemap-0.xml" + echo ' ' + echo '' +} > "$TEST_DIR/sitemap.xml" +echo "✓ sitemap.xml generated" + echo "" echo "==================================================" echo "✓ All versions built successfully!" @@ -130,33 +174,33 @@ echo "" # Verify the structure echo "Verifying test structure..." -echo "Directory: $TEST_DIR$BASE_PREFIX/" -ls -lah "$TEST_DIR$BASE_PREFIX/" +echo "Directory: $TEST_DIR/" +ls -lah "$TEST_DIR/" echo "" echo "Checking version subdirectories..." for version in 0.1 0.2 main; do echo "" - if [ -d "$TEST_DIR$BASE_PREFIX/$version" ]; then + if [ -d "$TEST_DIR/$version" ]; then echo "✓ $version/ exists" - echo " Files: $(ls "$TEST_DIR$BASE_PREFIX/$version" | wc -l)" + echo " Files: $(ls "$TEST_DIR/$version" | wc -l)" - if [ -f "$TEST_DIR$BASE_PREFIX/$version/index.html" ]; then + if [ -f "$TEST_DIR/$version/index.html" ]; then echo " index.html: ✓" # Check BASE_URL in the built HTML - BASE_URL_IN_HTML=$(grep -o 'import\.meta\.env\.BASE_URL[^"]*"[^"]*"' "$TEST_DIR$BASE_PREFIX/$version/index.html" | head -1 || echo "not found") + BASE_URL_IN_HTML=$(grep -o 'import\.meta\.env\.BASE_URL[^"]*"[^"]*"' "$TEST_DIR/$version/index.html" | head -1 || echo "not found") echo " BASE_URL in HTML: $BASE_URL_IN_HTML" # Check if version switcher is present - if grep -q "version-switcher" "$TEST_DIR$BASE_PREFIX/$version/index.html"; then + if grep -q "version-switcher" "$TEST_DIR/$version/index.html"; then echo " VersionSwitcher: ✓ present" # Check what's rendered (dev/main badge or button) - if grep -q "version-button" "$TEST_DIR$BASE_PREFIX/$version/index.html"; then + if grep -q "version-button" "$TEST_DIR/$version/index.html"; then echo " Renders: version button (interactive dropdown)" - elif grep -q ">main<" "$TEST_DIR$BASE_PREFIX/$version/index.html"; then + elif grep -q ">main<" "$TEST_DIR/$version/index.html"; then echo " Renders: 'main' badge (⚠️ unexpected for $version)" - elif grep -q ">dev<" "$TEST_DIR$BASE_PREFIX/$version/index.html"; then + elif grep -q ">dev<" "$TEST_DIR/$version/index.html"; then echo " Renders: 'dev' badge (⚠️ unexpected)" else echo " Renders: unknown" @@ -169,8 +213,8 @@ for version in 0.1 0.2 main; do fi # Check for assets directory - if [ -d "$TEST_DIR$BASE_PREFIX/$version/_astro" ]; then - echo " _astro/ assets: ✓ ($(ls "$TEST_DIR$BASE_PREFIX/$version/_astro" | wc -l) files)" + if [ -d "$TEST_DIR/$version/_astro" ]; then + echo " _astro/ assets: ✓ ($(ls "$TEST_DIR/$version/_astro" | wc -l) files)" else echo " _astro/ assets: ✗ MISSING" fi @@ -183,7 +227,7 @@ echo "" # Start Python HTTP server (most reliable for static files) if ! command -v python3 &> /dev/null; then echo "⚠️ Warning: python3 not found. Cannot start server." - echo "Files are built in: $TEST_DIR$BASE_PREFIX/" + echo "Files are built in: $TEST_DIR/" else echo "Starting local server with Python..." echo "Server starting at: http://localhost:${TEST_PORT}" @@ -191,10 +235,14 @@ else echo "Press Ctrl+C to stop the server when done testing" echo "" echo "Test URLs:" - echo " - http://localhost:${TEST_PORT}$BASE_PREFIX/ (should redirect to 0.2)" - echo " - http://localhost:${TEST_PORT}$BASE_PREFIX/0.2/ (version 0.2)" - echo " - http://localhost:${TEST_PORT}$BASE_PREFIX/0.1/ (version 0.1)" - echo " - http://localhost:${TEST_PORT}$BASE_PREFIX/main/ (main branch)" + echo " - http://localhost:${TEST_PORT}/ (should redirect to $LATEST_VERSION)" + echo " - http://localhost:${TEST_PORT}/0.2/ (version 0.2)" + echo " - http://localhost:${TEST_PORT}/0.1/ (version 0.1)" + echo " - http://localhost:${TEST_PORT}/main/ (main branch)" + echo " - http://localhost:${TEST_PORT}/feed.xml (RSS feed, latest version)" + echo " - http://localhost:${TEST_PORT}/llms.txt (agent index, latest version)" + echo " - http://localhost:${TEST_PORT}/robots.txt (robots, latest version)" + echo " - http://localhost:${TEST_PORT}/sitemap.xml (root sitemap index)" echo "" echo "Expected behavior:" echo " ✓ Version 0.2: Button shows 'v0.2', dropdown shows both versions" diff --git a/docs/reference/cli.md b/docs/reference/cli.md index ebe00c6f..22562467 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -197,6 +197,7 @@ Make a canister call - `hex`: Print raw response as hex +* `--json` — Output command results as JSON @@ -241,7 +242,11 @@ Examples: Default value: `2000000000000` * `--subnet ` — The subnet to create canisters on +* `--proxy ` — Principal of a proxy canister to route the create_canister call through. + + When specified, the canister will be created on the same subnet as the proxy canister by forwarding the management canister call through the proxy's `proxy` method. * `--detached` — Create a canister detached from any project configuration. The canister id will be printed out but not recorded in the project configuration. Not valid if `Canister` is provided +* `--json` — Output command results as JSON @@ -261,6 +266,7 @@ Delete a canister from a network * `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as +* `--proxy ` — Principal of a proxy canister to route the management canister call through @@ -283,9 +289,9 @@ Install a built WASM to a canister on a network Possible values: `auto`, `install`, `reinstall`, `upgrade` * `--wasm ` — Path to the WASM file to install. Uses the build output if not explicitly provided -* `--args ` — Inline initialization arguments, interpreted per `--args-format` (Candid by default) -* `--args-file ` — Path to a file containing initialization arguments -* `--args-format ` — Format of the initialization arguments +* `--args ` — Inline arguments, interpreted per `--args-format` (Candid by default) +* `--args-file ` — Path to a file containing arguments +* `--args-format ` — Format of the arguments Default value: `candid` @@ -302,6 +308,7 @@ Install a built WASM to a canister on a network * `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as +* `--proxy ` — Principal of a proxy canister to route the management canister call through @@ -314,6 +321,7 @@ List the canisters in an environment ###### **Options:** * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used +* `--json` — Output command results as JSON @@ -341,6 +349,7 @@ Fetch and display canister logs * `--until ` — Show logs before this timestamp (exclusive). Accepts nanoseconds since Unix epoch or RFC3339 (e.g. '2024-01-01T00:00:00Z'). Cannot be used with --follow * `--since-index ` — Show logs at or after this log index (inclusive). Cannot be used with --follow * `--until-index ` — Show logs before this log index (exclusive). Cannot be used with --follow +* `--json` — Output command results as JSON @@ -361,6 +370,7 @@ Read a metadata section from a canister * `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as +* `--json` — Output command results as JSON @@ -384,6 +394,7 @@ Migrate a canister ID from one subnet to another * `-y`, `--yes` — Skip confirmation prompts * `--resume-watch` — Resume watching an already-initiated migration (skips validation and initiation) * `--skip-watch` — Exit as soon as the migrated canister is deleted (don't wait for full completion) +* `--proxy ` — Principal of a proxy canister to route the management canister calls through @@ -422,6 +433,7 @@ By default this queries the status endpoint of the management canister. If the c * `-i`, `--id-only` — Only print the canister ids * `--json` — Format output in json * `-p`, `--public` — Show the only the public information. Skips trying to get the status from the management canister and looks up public information from the state tree +* `--proxy ` — Principal of a proxy canister to route the management canister call through @@ -462,6 +474,7 @@ Change a canister's settings to specified values * `--set-log-viewer ` * `--add-environment-variable ` * `--remove-environment-variable ` +* `--proxy ` — Principal of a proxy canister to route the management canister calls through @@ -481,6 +494,7 @@ Synchronize a canister's settings with those defined in the project * `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as +* `--proxy ` — Principal of a proxy canister to route the management canister calls through @@ -518,6 +532,9 @@ Create a snapshot of a canister's state * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as * `--replace ` — Replace an existing snapshot instead of creating a new one. The old snapshot will be deleted once the new one is successfully created +* `--json` — Output command results as JSON +* `-q`, `--quiet` — Suppress human-readable output; print only snapshot ID +* `--proxy ` — Principal of a proxy canister to route the management canister calls through @@ -538,6 +555,7 @@ Delete a canister snapshot * `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as +* `--proxy ` — Principal of a proxy canister to route the management canister call through @@ -560,6 +578,7 @@ Download a snapshot to local disk * `--identity ` — The user identity to run this command as * `-o`, `--output ` — Output directory for the snapshot files * `--resume` — Resume a previously interrupted download +* `--proxy ` — Principal of a proxy canister to route the management canister calls through @@ -579,6 +598,9 @@ List all snapshots for a canister * `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as +* `--json` — Output command results as JSON +* `-q`, `--quiet` — Suppress human-readable output; print only snapshot IDs +* `--proxy ` — Principal of a proxy canister to route the management canister call through @@ -599,6 +621,7 @@ Restore a canister from a snapshot * `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as +* `--proxy ` — Principal of a proxy canister to route the management canister calls through @@ -621,6 +644,9 @@ Upload a snapshot from local disk * `-i`, `--input ` — Input directory containing the snapshot files * `--replace ` — Replace an existing snapshot instead of creating a new one * `--resume` — Resume a previously interrupted upload +* `--json` — Output command results as JSON +* `-q`, `--quiet` — Suppress human-readable output; print only snapshot ID +* `--proxy ` — Principal of a proxy canister to route the management canister calls through @@ -640,6 +666,7 @@ Start a canister on a network * `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as +* `--proxy ` — Principal of a proxy canister to route the management canister call through @@ -664,6 +691,7 @@ By default this queries the status endpoint of the management canister. If the c * `-i`, `--id-only` — Only print the canister ids * `--json` — Format output in json * `-p`, `--public` — Show the only the public information. Skips trying to get the status from the management canister and looks up public information from the state tree +* `--proxy ` — Principal of a proxy canister to route the management canister call through @@ -683,6 +711,7 @@ Stop a canister on a network * `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as +* `--proxy ` — Principal of a proxy canister to route the management canister call through @@ -733,6 +762,8 @@ Display the cycles balance * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as * `--subaccount ` — The subaccount to check the balance for +* `--json` — Output command results as JSON +* `-q`, `--quiet` — Suppress human-readable output; print only the balance @@ -752,6 +783,7 @@ Convert icp to cycles * `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as +* `--json` — Output command results as JSON @@ -774,6 +806,8 @@ Transfer cycles to another principal * `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as +* `--json` — Output command results as JSON +* `-q`, `--quiet` — Suppress human-readable output; print only the block index @@ -783,6 +817,19 @@ Deploy a project to an environment **Usage:** `icp deploy [OPTIONS] [NAMES]...` +When deploying a single canister, you can pass arguments to the install call +using --args or --args-file: + + # Pass inline Candid arguments + icp deploy my_canister --args '(42 : nat)' + + # Pass arguments from a file + icp deploy my_canister --args-file ./args.did + + # Pass raw bytes + icp deploy my_canister --args-file ./args.bin --args-format bin + + ###### **Arguments:** * `` — Canister names @@ -796,6 +843,7 @@ Deploy a project to an environment Possible values: `auto`, `install`, `reinstall`, `upgrade` * `--subnet ` — The subnet to use for the canisters being deployed +* `--proxy ` — Principal of a proxy canister to route management canister calls through * `--controller ` — One or more controllers for the canisters being deployed. Repeat `--controller` to specify multiple * `--cycles ` — Cycles to fund canister creation. Supports suffixes: k (thousand), m (million), b (billion), t (trillion) @@ -803,6 +851,21 @@ Deploy a project to an environment * `-y`, `--yes` — Skip confirmation prompts, including the Candid interface compatibility check * `--identity ` — The user identity to run this command as * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used +* `--json` — Output command results as JSON +* `--args ` — Inline arguments, interpreted per `--args-format` (Candid by default) +* `--args-file ` — Path to a file containing arguments +* `--args-format ` — Format of the arguments + + Default value: `candid` + + Possible values: + - `hex`: + Hex-encoded bytes + - `candid`: + Candid text format + - `bin`: + Raw binary (only valid for file references) + @@ -980,7 +1043,12 @@ Link an HSM key to a new identity List the identities -**Usage:** `icp identity list` +**Usage:** `icp identity list [OPTIONS]` + +###### **Options:** + +* `--json` — Output command results as JSON +* `-q`, `--quiet` — Suppress human-readable output; print only identity names @@ -1004,6 +1072,8 @@ Create a new identity * `--storage-password-file ` — Read the storage password from a file instead of prompting (for --storage password) * `--output-seed ` — Write the seed phrase to a file instead of printing to stdout +* `--json` — Output command results as JSON +* `-q`, `--quiet` — Suppress human-readable output; print only the seed phrase @@ -1407,6 +1477,8 @@ Display the token balance on the ledger (default token: icp) * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as * `--subaccount ` — The subaccount to check the balance for +* `--json` — Output command results as JSON +* `-q`, `--quiet` — Suppress human-readable output; print only the balance @@ -1429,6 +1501,8 @@ Transfer ICP or ICRC1 tokens through their ledger (default token: icp) * `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as +* `--json` — Output command results as JSON +* `-q`, `--quiet` — Suppress human-readable output; print only the block index diff --git a/docs/schemas/canister-yaml-schema.json b/docs/schemas/canister-yaml-schema.json index 5e4982b3..ba1ac963 100644 --- a/docs/schemas/canister-yaml-schema.json +++ b/docs/schemas/canister-yaml-schema.json @@ -89,6 +89,26 @@ ], "type": "object" }, + "ArgsFormat": { + "description": "Format specifier for canister call/install args content.", + "oneOf": [ + { + "const": "hex", + "description": "Hex-encoded bytes", + "type": "string" + }, + { + "const": "candid", + "description": "Candid text format", + "type": "string" + }, + { + "const": "bin", + "description": "Raw binary (only valid for file references)", + "type": "string" + } + ] + }, "BuildStep": { "description": "Identifies the type of adapter used to build the canister,\nalong with its configuration.\n\nThe adapter type is specified via the `type` field in the YAML file.\nFor example:\n\n```yaml\ntype: script\ncommand: do_something.sh\n```", "oneOf": [ @@ -163,26 +183,6 @@ ], "description": "A duration in seconds.\n\nDeserializes from a number (seconds) or a string with duration suffix (s, m, h, d, w)\nand optional underscore separators.\n\nSuffixes (case-insensitive):\n- `s` — seconds\n- `m` — minutes (×60)\n- `h` — hours (×3600)\n- `d` — days (×86400)\n- `w` — weeks (×604800)\n\nA bare number without suffix is treated as seconds." }, - "InitArgsFormat": { - "description": "Format specifier for init args content.", - "oneOf": [ - { - "const": "hex", - "description": "Hex-encoded bytes", - "type": "string" - }, - { - "const": "candid", - "description": "Candid text format", - "type": "string" - }, - { - "const": "bin", - "description": "Raw binary (only valid for file references)", - "type": "string" - } - ] - }, "LocalSource": { "properties": { "path": { @@ -236,7 +236,7 @@ "description": "File reference with explicit format.", "properties": { "format": { - "$ref": "#/$defs/InitArgsFormat", + "$ref": "#/$defs/ArgsFormat", "default": "candid" }, "path": { @@ -252,7 +252,7 @@ "description": "Inline value with explicit format.", "properties": { "format": { - "$ref": "#/$defs/InitArgsFormat", + "$ref": "#/$defs/ArgsFormat", "default": "candid" }, "value": { diff --git a/docs/schemas/environment-yaml-schema.json b/docs/schemas/environment-yaml-schema.json index 5dfceca4..82595812 100644 --- a/docs/schemas/environment-yaml-schema.json +++ b/docs/schemas/environment-yaml-schema.json @@ -1,5 +1,25 @@ { "$defs": { + "ArgsFormat": { + "description": "Format specifier for canister call/install args content.", + "oneOf": [ + { + "const": "hex", + "description": "Hex-encoded bytes", + "type": "string" + }, + { + "const": "candid", + "description": "Candid text format", + "type": "string" + }, + { + "const": "bin", + "description": "Raw binary (only valid for file references)", + "type": "string" + } + ] + }, "CyclesAmount": { "anyOf": [ { @@ -26,26 +46,6 @@ ], "description": "A duration in seconds.\n\nDeserializes from a number (seconds) or a string with duration suffix (s, m, h, d, w)\nand optional underscore separators.\n\nSuffixes (case-insensitive):\n- `s` — seconds\n- `m` — minutes (×60)\n- `h` — hours (×3600)\n- `d` — days (×86400)\n- `w` — weeks (×604800)\n\nA bare number without suffix is treated as seconds." }, - "InitArgsFormat": { - "description": "Format specifier for init args content.", - "oneOf": [ - { - "const": "hex", - "description": "Hex-encoded bytes", - "type": "string" - }, - { - "const": "candid", - "description": "Candid text format", - "type": "string" - }, - { - "const": "bin", - "description": "Raw binary (only valid for file references)", - "type": "string" - } - ] - }, "LogVisibility": { "description": "Controls who can read canister logs.", "oneOf": [ @@ -87,7 +87,7 @@ "description": "File reference with explicit format.", "properties": { "format": { - "$ref": "#/$defs/InitArgsFormat", + "$ref": "#/$defs/ArgsFormat", "default": "candid" }, "path": { @@ -103,7 +103,7 @@ "description": "Inline value with explicit format.", "properties": { "format": { - "$ref": "#/$defs/InitArgsFormat", + "$ref": "#/$defs/ArgsFormat", "default": "candid" }, "value": { diff --git a/docs/schemas/icp-yaml-schema.json b/docs/schemas/icp-yaml-schema.json index 63a012e9..efd5d1bc 100644 --- a/docs/schemas/icp-yaml-schema.json +++ b/docs/schemas/icp-yaml-schema.json @@ -89,6 +89,26 @@ ], "type": "object" }, + "ArgsFormat": { + "description": "Format specifier for canister call/install args content.", + "oneOf": [ + { + "const": "hex", + "description": "Hex-encoded bytes", + "type": "string" + }, + { + "const": "candid", + "description": "Candid text format", + "type": "string" + }, + { + "const": "bin", + "description": "Raw binary (only valid for file references)", + "type": "string" + } + ] + }, "BuildStep": { "description": "Identifies the type of adapter used to build the canister,\nalong with its configuration.\n\nThe adapter type is specified via the `type` field in the YAML file.\nFor example:\n\n```yaml\ntype: script\ncommand: do_something.sh\n```", "oneOf": [ @@ -374,26 +394,6 @@ }, "type": "object" }, - "InitArgsFormat": { - "description": "Format specifier for init args content.", - "oneOf": [ - { - "const": "hex", - "description": "Hex-encoded bytes", - "type": "string" - }, - { - "const": "candid", - "description": "Candid text format", - "type": "string" - }, - { - "const": "bin", - "description": "Raw binary (only valid for file references)", - "type": "string" - } - ] - }, "Item": { "anyOf": [ { @@ -680,7 +680,7 @@ "description": "File reference with explicit format.", "properties": { "format": { - "$ref": "#/$defs/InitArgsFormat", + "$ref": "#/$defs/ArgsFormat", "default": "candid" }, "path": { @@ -696,7 +696,7 @@ "description": "Inline value with explicit format.", "properties": { "format": { - "$ref": "#/$defs/InitArgsFormat", + "$ref": "#/$defs/ArgsFormat", "default": "candid" }, "value": { diff --git a/examples/icp-frontend-environment-variables/frontend/app/package.json b/examples/icp-frontend-environment-variables/frontend/app/package.json index d48dd893..910a9d6e 100644 --- a/examples/icp-frontend-environment-variables/frontend/app/package.json +++ b/examples/icp-frontend-environment-variables/frontend/app/package.json @@ -28,6 +28,6 @@ "globals": "^16.3.0", "typescript": "~5.8.3", "typescript-eslint": "^8.39.1", - "vite": "^7.1.11" + "vite": "^7.3.2" } } diff --git a/examples/icp-frontend-environment-variables/frontend/package-lock.json b/examples/icp-frontend-environment-variables/frontend/package-lock.json index 63d12574..a5558846 100644 --- a/examples/icp-frontend-environment-variables/frontend/package-lock.json +++ b/examples/icp-frontend-environment-variables/frontend/package-lock.json @@ -29,7 +29,7 @@ "globals": "^16.3.0", "typescript": "~5.8.3", "typescript-eslint": "^8.39.1", - "vite": "^7.1.11" + "vite": "^7.3.2" } }, "node_modules/@babel/code-frame": { @@ -63,7 +63,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -322,9 +321,9 @@ "license": "Apache-2.0" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", - "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", "cpu": [ "ppc64" ], @@ -339,9 +338,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz", - "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", "cpu": [ "arm" ], @@ -356,9 +355,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", - "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", "cpu": [ "arm64" ], @@ -373,9 +372,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz", - "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", "cpu": [ "x64" ], @@ -390,9 +389,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", - "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", "cpu": [ "arm64" ], @@ -407,9 +406,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", - "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", "cpu": [ "x64" ], @@ -424,9 +423,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", - "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", "cpu": [ "arm64" ], @@ -441,9 +440,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", - "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", "cpu": [ "x64" ], @@ -458,9 +457,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", - "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", "cpu": [ "arm" ], @@ -475,9 +474,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", - "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", "cpu": [ "arm64" ], @@ -492,9 +491,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", - "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", "cpu": [ "ia32" ], @@ -509,9 +508,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", - "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", "cpu": [ "loong64" ], @@ -526,9 +525,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", - "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", "cpu": [ "mips64el" ], @@ -543,9 +542,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", - "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", "cpu": [ "ppc64" ], @@ -560,9 +559,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", - "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", "cpu": [ "riscv64" ], @@ -577,9 +576,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", - "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", "cpu": [ "s390x" ], @@ -594,9 +593,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", - "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", "cpu": [ "x64" ], @@ -611,9 +610,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", - "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", "cpu": [ "arm64" ], @@ -628,9 +627,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", - "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", "cpu": [ "x64" ], @@ -645,9 +644,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", - "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", "cpu": [ "arm64" ], @@ -662,9 +661,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", - "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", "cpu": [ "x64" ], @@ -679,9 +678,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", - "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", "cpu": [ "arm64" ], @@ -696,9 +695,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", - "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", "cpu": [ "x64" ], @@ -713,9 +712,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", - "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", "cpu": [ "arm64" ], @@ -730,9 +729,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", - "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", "cpu": [ "ia32" ], @@ -747,9 +746,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", - "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", "cpu": [ "x64" ], @@ -1530,7 +1529,6 @@ "integrity": "sha512-ebO/Yl+EAvVe8DnMfi+iaAyIqYdK0q/q0y0rw82INWEKJOBe6b/P3YWE8NW7oOlF/nXFNrHwhARrN/hdgDkraA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1541,7 +1539,6 @@ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1602,7 +1599,6 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -1855,7 +1851,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1992,7 +1987,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -2154,9 +2148,9 @@ "license": "ISC" }, "node_modules/esbuild": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz", - "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2167,32 +2161,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.11", - "@esbuild/android-arm": "0.25.11", - "@esbuild/android-arm64": "0.25.11", - "@esbuild/android-x64": "0.25.11", - "@esbuild/darwin-arm64": "0.25.11", - "@esbuild/darwin-x64": "0.25.11", - "@esbuild/freebsd-arm64": "0.25.11", - "@esbuild/freebsd-x64": "0.25.11", - "@esbuild/linux-arm": "0.25.11", - "@esbuild/linux-arm64": "0.25.11", - "@esbuild/linux-ia32": "0.25.11", - "@esbuild/linux-loong64": "0.25.11", - "@esbuild/linux-mips64el": "0.25.11", - "@esbuild/linux-ppc64": "0.25.11", - "@esbuild/linux-riscv64": "0.25.11", - "@esbuild/linux-s390x": "0.25.11", - "@esbuild/linux-x64": "0.25.11", - "@esbuild/netbsd-arm64": "0.25.11", - "@esbuild/netbsd-x64": "0.25.11", - "@esbuild/openbsd-arm64": "0.25.11", - "@esbuild/openbsd-x64": "0.25.11", - "@esbuild/openharmony-arm64": "0.25.11", - "@esbuild/sunos-x64": "0.25.11", - "@esbuild/win32-arm64": "0.25.11", - "@esbuild/win32-ia32": "0.25.11", - "@esbuild/win32-x64": "0.25.11" + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" } }, "node_modules/escalade": { @@ -2224,7 +2218,6 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3063,7 +3056,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -3293,7 +3285,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3352,7 +3343,6 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3434,14 +3424,13 @@ } }, "node_modules/vite": { - "version": "7.1.12", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", - "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "esbuild": "^0.25.0", + "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", @@ -3533,7 +3522,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, diff --git a/taplo.toml b/taplo.toml index b6b6c072..4a58bdbd 100644 --- a/taplo.toml +++ b/taplo.toml @@ -1,3 +1,6 @@ +# dist-workspace.toml is auto-generated by `dist init` +exclude = ["dist-workspace.toml"] + [formatting] compact_arrays = true allowed_blank_lines = 1