diff --git a/.github/actions/install-node-dependencies/action.yml b/.github/actions/install-node-dependencies/action.yml index 053fcdbe0fbc..4163150f47e2 100644 --- a/.github/actions/install-node-dependencies/action.yml +++ b/.github/actions/install-node-dependencies/action.yml @@ -2,6 +2,14 @@ name: Install Node dependencies description: Installs the Node toolchain and repository dependencies for CI jobs, with optional Electron archive caching. inputs: + cache-pnpm-store: + description: Restore or save the pnpm download store; verification and native caches are independent. + required: false + default: 'true' + cache-pnpm-store-lookup-only: + description: Auto uses measured hosted Node 24 root installs; true forces lookup, false retains archive restoration. + required: false + default: auto cache-pnpm-verification: description: Restore pnpm's policy-checked lockfile verification record. required: false @@ -28,9 +36,12 @@ inputs: default: 'false' outputs: + pnpm-store-cache-hit: + description: Whether the requested download store matched an existing cache. + value: ${{ steps.pnpm-store-lookup.outputs.cache-hit || steps.pnpm-store-restore.outputs.cache-hit || steps.requested-node.outputs.cache-hit || steps.default-node.outputs.cache-hit }} verification-cache-hit: description: Whether pnpm's verification record was restored. - value: ${{ steps.verification-cache-restore.outputs.cache-hit }} + value: ${{ steps.verification-cache.outputs.cache-hit }} verification-cache-path: description: The small pnpm-owned verification record, without registry metadata. value: ${{ steps.verification-cache.outputs.path }} @@ -56,6 +67,30 @@ outputs: runs: using: composite steps: + - name: Resolve pnpm store mode + id: pnpm-store-mode + if: >- + github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && + (inputs.cache-pnpm-store-lookup-only == 'true' || + (inputs.cache-pnpm-store-lookup-only == 'auto' && + inputs.cache-dependency-path == 'pnpm-lock.yaml' && + runner.environment == 'github-hosted' && job.container.id == '' && + (runner.os == 'Linux' || runner.os == 'macOS' || runner.os == 'Windows') && + (runner.arch == 'X64' || runner.arch == 'ARM64') && + (inputs.node-version == '' || inputs.node-version == '24'))) + shell: bash + env: + LOOKUP_REQUEST: ${{ inputs.cache-pnpm-store-lookup-only }} + run: | + lookup_only=true + case "$LOOKUP_REQUEST" in + [aA][uU][tT][oO]) + # Hosted runners have Node for this manifest-only check before toolchain setup. + lookup_only="$(node -p 'const p = require("./package.json"); p.engines?.node === "24" && typeof p.packageManager === "string" && p.packageManager.split("+")[0] === "pnpm@12.8.1"')" + ;; + esac + printf 'lookup-only=%s\n' "$lookup_only" >> "$GITHUB_OUTPUT" + # setup-node needs pnpm on PATH to locate and restore its store. - name: Setup pnpm uses: pnpm/setup@v2 @@ -69,7 +104,7 @@ runs: uses: actions/setup-node@v6 with: node-version-file: package.json - cache: ${{ github.event_name != 'pull_request' && 'pnpm' || '' }} + cache: ${{ github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && steps.pnpm-store-mode.outputs.lookup-only != 'true' && 'pnpm' || '' }} cache-dependency-path: ${{ inputs.cache-dependency-path }} package-manager-cache: false @@ -79,7 +114,7 @@ runs: uses: actions/setup-node@v6 with: node-version: ${{ inputs.node-version }} - cache: ${{ github.event_name != 'pull_request' && 'pnpm' || '' }} + cache: ${{ github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && steps.pnpm-store-mode.outputs.lookup-only != 'true' && 'pnpm' || '' }} cache-dependency-path: ${{ inputs.cache-dependency-path }} package-manager-cache: false @@ -87,51 +122,58 @@ runs: - name: Resolve pnpm download store id: pnpm-store if: >- - github.event_name == 'pull_request' && - (runner.os != 'Windows' || runner.arch != 'X64' || !contains(inputs.cache-dependency-path, 'mobile/pnpm-lock.yaml')) + github.event_name == 'pull_request' && inputs.cache-pnpm-store != 'false' && + !((runner.os == 'Linux' || runner.os == 'macOS') && (runner.arch == 'X64' || runner.arch == 'ARM64') && inputs.cache-dependency-path == 'pnpm-lock.yaml') && + (runner.os != 'Windows' || + !(runner.arch == 'X64' && contains(inputs.cache-dependency-path, 'mobile/pnpm-lock.yaml')) && + !((runner.arch == 'X64' || runner.arch == 'ARM64') && inputs.cache-dependency-path == 'pnpm-lock.yaml')) || + (github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && + steps.pnpm-store-mode.outputs.lookup-only == 'true') shell: bash env: LOCKFILE_HASH: ${{ hashFiles(inputs.cache-dependency-path) }} + STORE_LOOKUP_ONLY: ${{ steps.pnpm-store-mode.outputs.lookup-only == 'true' }} run: | test -n "$LOCKFILE_HASH" cache_path="$(pnpm store path --silent)" test -n "$cache_path" printf 'path=%s\n' "$cache_path" >> "$GITHUB_OUTPUT" printf 'arch=%s\n' "$(node -p 'require("node:os").arch()')" >> "$GITHUB_OUTPUT" + if [ "$STORE_LOOKUP_ONLY" = 'true' ]; then + printf 'ORCA_PNPM_STORE_CACHE_PATH=%s\n' "$cache_path" >> "$GITHUB_ENV" + fi # Match setup-node's key and path so existing default-branch stores remain reusable. - # Hosted Windows x64 mixed installs cost less than restoring their root/mobile store. + # Direct downloads beat store restoration for the measured Linux, macOS and Windows installs. - name: Restore pnpm download store without saving + id: pnpm-store-restore if: >- - github.event_name == 'pull_request' && - (runner.os != 'Windows' || runner.arch != 'X64' || !contains(inputs.cache-dependency-path, 'mobile/pnpm-lock.yaml')) + github.event_name == 'pull_request' && inputs.cache-pnpm-store != 'false' && + !((runner.os == 'Linux' || runner.os == 'macOS') && (runner.arch == 'X64' || runner.arch == 'ARM64') && inputs.cache-dependency-path == 'pnpm-lock.yaml') && + (runner.os != 'Windows' || + !(runner.arch == 'X64' && contains(inputs.cache-dependency-path, 'mobile/pnpm-lock.yaml')) && + !((runner.arch == 'X64' || runner.arch == 'ARM64') && inputs.cache-dependency-path == 'pnpm-lock.yaml')) uses: actions/cache/restore@v5 with: path: ${{ steps.pnpm-store.outputs.path }} key: node-cache-${{ runner.os }}-${{ steps.pnpm-store.outputs.arch }}-pnpm-${{ hashFiles(inputs.cache-dependency-path) }} - - name: Resolve pnpm verification cache - id: verification-cache - if: >- - inputs.cache-pnpm-verification == 'true' && - (runner.os == 'Linux' || - (runner.os == 'Windows' && (runner.arch == 'X64' || runner.arch == 'ARM64')) || - (runner.os == 'macOS' && runner.arch == 'X64')) - shell: bash - env: - POLICY_HASH: ${{ hashFiles('pnpm-lock.yaml', 'pnpm-workspace.yaml', '.npmrc') }} - run: | - printf 'path=%s/lockfile-verified.jsonl\n' "$(pnpm cache path)" >> "$GITHUB_OUTPUT" - printf 'key=pnpm-verification-v1-%s-%s-%s-%s\n' "$RUNNER_OS" "$RUNNER_ARCH" "$(pnpm --version)" "$POLICY_HASH" >> "$GITHUB_OUTPUT" + # Producers can refresh access and publish misses without downloading existing archives. + - name: Keep pnpm download store without restoring + id: pnpm-store-lookup + if: steps.pnpm-store-mode.outputs.lookup-only == 'true' + uses: actions/cache@v5 + with: + # Twice-nested composite cleanup loses internal step outputs. + path: ${{ env.ORCA_PNPM_STORE_CACHE_PATH }} + key: node-cache-${{ runner.os }}-${{ steps.pnpm-store.outputs.arch }}-pnpm-${{ hashFiles(inputs.cache-dependency-path) }} + lookup-only: true - name: Restore pnpm verification record - id: verification-cache-restore - if: steps.verification-cache.outputs.key != '' - continue-on-error: true - uses: actions/cache/restore@v5 + id: verification-cache + uses: ./.github/actions/restore-pnpm-verification with: - path: ${{ steps.verification-cache.outputs.path }} - key: ${{ steps.verification-cache.outputs.key }} + enabled: ${{ inputs.cache-pnpm-verification }} - name: Validate native runtime shell: bash @@ -169,7 +211,7 @@ runs: # pnpm checks the cached record's policy and validity; never bypass verification. - name: Save pnpm verification record on main - if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' && steps.verification-cache.outputs.key != '' && steps.verification-cache-restore.outputs.cache-hit != 'true' + if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' && steps.verification-cache.outputs.key != '' && steps.verification-cache.outputs.cache-hit != 'true' continue-on-error: true uses: actions/cache/save@v5 with: diff --git a/.github/actions/prepare-git-compatibility/action.yml b/.github/actions/prepare-git-compatibility/action.yml index 058d31a064c7..96aa4ef9e15b 100644 --- a/.github/actions/prepare-git-compatibility/action.yml +++ b/.github/actions/prepare-git-compatibility/action.yml @@ -10,7 +10,7 @@ runs: uses: actions/cache@v5 with: path: ~/.cache/orca-git-compat/git-2.25.5 - key: git-compat-baseline-${{ runner.os }}-${{ runner.arch }}-2.25.5 + key: git-compat-baseline-${{ runner.os }}-${{ runner.arch }}-2.25.5-submodule # Finish the CPU-heavy build before any timed compatibility lanes start. - name: Build the baseline Git binary @@ -18,7 +18,9 @@ runs: run: | archive="$RUNNER_TEMP/git-2.25.5.tar.gz" source="$HOME/.cache/orca-git-compat/git-2.25.5" - if [ -x "$source/git" ]; then + if [ -x "$source/git" ] && [ -x "$source/git-submodule" ] \ + && [ -f "$source/git-sh-setup" ] && [ -f "$source/git-sh-i18n" ] \ + && [ -f "$source/git-parse-remote" ] && [ -x "$source/git-sh-i18n--envsubst" ]; then exit 0 fi curl -fsSL https://www.kernel.org/pub/software/scm/git/git-2.25.5.tar.gz -o "$archive" @@ -27,6 +29,7 @@ runs: mkdir -p "$source" tar -xzf "$archive" -C "$source" --strip-components=1 make -C "$source" -j"$(nproc)" \ - NO_GETTEXT=YesPlease NO_TCLTK=YesPlease NO_PYTHON=YesPlease git + NO_GETTEXT=YesPlease NO_TCLTK=YesPlease NO_PYTHON=YesPlease \ + git git-submodule git-sh-setup git-sh-i18n git-parse-remote git-sh-i18n--envsubst # Object files are no longer needed after linking the cached binary. find "$source" -name '*.o' -delete diff --git a/.github/actions/prepare-headless-compiler/action.yml b/.github/actions/prepare-headless-compiler/action.yml new file mode 100644 index 000000000000..9e71e6df1b07 --- /dev/null +++ b/.github/actions/prepare-headless-compiler/action.yml @@ -0,0 +1,45 @@ +name: Prepare headless detector compiler +description: Reuse the policy-checked compiler from main; callers install normally on a miss. +inputs: + seed: + description: Pack an already installed compiler instead of activating a cached compiler. + default: 'false' +outputs: + available: + description: Whether the cached compiler was validated and activated. + value: ${{ steps.activate.outputs.available }} +runs: + using: composite + steps: + - id: identity + shell: bash + env: + COMPILER_POLICY_HASH: ${{ hashFiles('package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml', '.npmrc', '.pnpmfile.cjs', 'config/patches/**', '.github/actions/install-node-dependencies/**', '.github/actions/restore-pnpm-verification/**', 'config/scripts/headless-detector-compiler-cache.mjs', '.github/actions/prepare-headless-compiler/action.yml') }} + run: node config/scripts/headless-detector-compiler-cache.mjs identity + - id: cache + uses: actions/cache/restore@v5 + continue-on-error: true + with: + path: ${{ steps.identity.outputs.path }} + key: ${{ steps.identity.outputs.key }} + - id: activate + if: inputs.seed != 'true' && steps.cache.outputs.cache-hit == 'true' + shell: bash + env: + COMPILER_CACHE_KEY: ${{ steps.identity.outputs.key }} + COMPILER_CACHE_PATH: ${{ steps.identity.outputs.path }} + run: node config/scripts/headless-detector-compiler-cache.mjs activate + - name: Pack installed compiler + if: inputs.seed == 'true' && steps.cache.outputs.cache-hit != 'true' + shell: bash + env: + COMPILER_CACHE_KEY: ${{ steps.identity.outputs.key }} + COMPILER_CACHE_PATH: ${{ steps.identity.outputs.path }} + run: node config/scripts/headless-detector-compiler-cache.mjs pack + - name: Save compiler only from main + if: inputs.seed == 'true' && steps.cache.outputs.cache-hit != 'true' && github.ref == 'refs/heads/main' && github.event_name != 'pull_request' + uses: actions/cache/save@v5 + continue-on-error: true + with: + path: ${{ steps.identity.outputs.path }} + key: ${{ steps.identity.outputs.key }} diff --git a/.github/actions/restore-pnpm-verification/action.yml b/.github/actions/restore-pnpm-verification/action.yml new file mode 100644 index 000000000000..fcd428f7c802 --- /dev/null +++ b/.github/actions/restore-pnpm-verification/action.yml @@ -0,0 +1,58 @@ +name: Restore pnpm verification +description: Restore the pnpm-owned lockfile verdict without installing dependencies. + +inputs: + enabled: + description: Restore a verification record on supported runners. + default: 'true' + container-toolchain: + description: Use the manifest's pnpm version and default Linux cache path without installing host pnpm. + default: 'false' + +outputs: + path: + description: The pnpm-owned verification record. + value: ${{ steps.verification-cache.outputs.path }} + key: + description: Exact OS, architecture, pnpm version and policy key. + value: ${{ steps.verification-cache.outputs.key }} + cache-hit: + description: Whether the exact record was restored. + value: ${{ steps.verification-cache-restore.outputs.cache-hit }} + +runs: + using: composite + steps: + - name: Resolve pnpm verification cache + id: verification-cache + if: >- + inputs.enabled == 'true' && + (runner.os == 'Linux' || + (runner.os == 'Windows' && (runner.arch == 'X64' || runner.arch == 'ARM64')) || + (runner.os == 'macOS' && runner.arch == 'X64')) + shell: bash + env: + POLICY_HASH: ${{ hashFiles('pnpm-lock.yaml', 'pnpm-workspace.yaml', '.npmrc') }} + CONTAINER_TOOLCHAIN: ${{ inputs.container-toolchain }} + run: | + if [ "$CONTAINER_TOOLCHAIN" = true ]; then + test "$RUNNER_OS" = Linux + # Cache versions include paths, so match the native Linux producer. + cache_path="$(node -p "require('node:path').join(process.env.XDG_CACHE_HOME || require('node:path').join(require('node:os').homedir(), '.cache'), 'pnpm')")" + pnpm_version="$(node -p "require('./package.json').packageManager.match(/^pnpm@([^+]+)/)[1]")" + mkdir -p "$cache_path" + else + cache_path="$(pnpm cache path)" + pnpm_version="$(pnpm --version)" + fi + printf 'path=%s/lockfile-verified.jsonl\n' "$cache_path" >> "$GITHUB_OUTPUT" + printf 'key=pnpm-verification-v1-%s-%s-%s-%s\n' "$RUNNER_OS" "$RUNNER_ARCH" "$pnpm_version" "$POLICY_HASH" >> "$GITHUB_OUTPUT" + + - name: Restore pnpm verification record + id: verification-cache-restore + if: steps.verification-cache.outputs.key != '' + continue-on-error: true + uses: actions/cache/restore@v5 + with: + path: ${{ steps.verification-cache.outputs.path }} + key: ${{ steps.verification-cache.outputs.key }} diff --git a/.github/scripts/render-readme-downloads-badge.mjs b/.github/scripts/render-readme-downloads-badge.mjs index efe0d670cf5c..9c98bf9229ea 100644 --- a/.github/scripts/render-readme-downloads-badge.mjs +++ b/.github/scripts/render-readme-downloads-badge.mjs @@ -1,5 +1,7 @@ import { mkdir, writeFile } from 'node:fs/promises' import { dirname } from 'node:path' +import { pathToFileURL } from 'node:url' +import { isAgentStateRulesTag } from '../../config/scripts/release-tag-patterns.mjs' const repository = process.env.GITHUB_REPOSITORY ?? 'stablyai/orca' const token = process.env.GITHUB_TOKEN @@ -24,6 +26,22 @@ async function fetchJson(url) { return response.json() } +// Why rules releases are excluded: every running app fetches them every few hours, so they +// count fetches, not installs. +export function countReleaseDownloads(releases) { + let total = 0 + for (const release of releases) { + if (release.draft || isAgentStateRulesTag(release.tag_name ?? '')) { + continue + } + + for (const asset of release.assets ?? []) { + total += asset.download_count ?? 0 + } + } + return total +} + async function getTotalReleaseDownloads() { let page = 1 let total = 0 @@ -37,16 +55,7 @@ async function getTotalReleaseDownloads() { return total } - for (const release of releases) { - if (release.draft) { - continue - } - - for (const asset of release.assets ?? []) { - total += asset.download_count ?? 0 - } - } - + total += countReleaseDownloads(releases) page += 1 } } @@ -104,9 +113,11 @@ function renderBadge(value) { ` } -const total = await getTotalReleaseDownloads() -const badge = renderBadge(formatDownloads(total)) +if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) { + const total = await getTotalReleaseDownloads() + const badge = renderBadge(formatDownloads(total)) -await mkdir(dirname(outputPath), { recursive: true }) -await writeFile(outputPath, badge) -console.log(`Rendered ${outputPath} from ${total} downloads.`) + await mkdir(dirname(outputPath), { recursive: true }) + await writeFile(outputPath, badge) + console.log(`Rendered ${outputPath} from ${total} downloads.`) +} diff --git a/.github/workflows/agent-state-rules-publish.yml b/.github/workflows/agent-state-rules-publish.yml new file mode 100644 index 000000000000..8a24adbc73f4 --- /dev/null +++ b/.github/workflows/agent-state-rules-publish.yml @@ -0,0 +1,94 @@ +name: Publish agent state rules + +# The only publisher of agent state rules releases. Merging never publishes: a maintainer runs +# this from main, first to `next` (RC and dev builds), then promotes the identical file to +# `stable` after a soak. Every job runs on the dispatched commit, so the publish job runs exactly +# the script and rules the gate tested. +# +# Why GITHUB_TOKEN: release-policy.yml deletes an agent-state-rules-* release any other author +# publishes. Its releases start no workflow, so release-policy.yml sees them only when someone +# edits one by hand, and then accepts them as bot-authored prereleases. +on: + workflow_dispatch: + inputs: + action: + description: 'publish-next builds from main and publishes to next; promote-stable copies the next file to stable' + required: true + type: choice + options: + - publish-next + - promote-stable + bundled_only: + description: 'publish-next: tell apps to fall back to the rules they shipped with; promote-stable then carries it to stable' + required: false + type: boolean + default: false + +permissions: + contents: read + +concurrency: + group: agent-state-rules-publish + cancel-in-progress: false + +jobs: + gate: + if: github.ref == 'refs/heads/main' && inputs.action == 'publish-next' + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: ./.github/actions/install-node-dependencies + with: + native-runtime: node + - name: Run the rules gate (schema, regex safety, the bundle, every transcript replay) + shell: bash + run: | + set -euo pipefail + mapfile -t gate_files < <(git ls-files \ + ':(glob)src/main/runtime/agent-state-rules/**/*.test.ts' \ + ':(glob)src/main/runtime/readiness-census*.test.ts' \ + ':(glob)src/main/runtime/*transcript.test.ts' \ + ':(glob)src/main/runtime/*transcripts.test.ts' \ + config/scripts/agent-state-rules-bundle.test.mjs) + pnpm exec vitest run --config config/vitest.config.ts "${gate_files[@]}" + + publish-next: + needs: gate + runs-on: ubuntu-latest + timeout-minutes: 10 + environment: agent-state-rules + permissions: + contents: write + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - name: Publish to next + env: + GH_TOKEN: ${{ github.token }} + BUNDLED_ONLY: ${{ inputs.bundled_only }} + shell: bash + run: | + set -euo pipefail + flags=() + [[ "$BUNDLED_ONLY" == "true" ]] && flags+=(--bundled-only) + node config/scripts/agent-state-rules-bundle.mjs publish-next "${flags[@]}" + + promote-stable: + if: github.ref == 'refs/heads/main' && inputs.action == 'promote-stable' + runs-on: ubuntu-latest + timeout-minutes: 10 + environment: agent-state-rules + permissions: + contents: write + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - name: Copy the next file to stable + env: + GH_TOKEN: ${{ github.token }} + run: node config/scripts/agent-state-rules-bundle.mjs promote-stable diff --git a/.github/workflows/ci-cache-warmup.yml b/.github/workflows/ci-cache-warmup.yml index 84ed72cc68b5..39f1bb4b888b 100644 --- a/.github/workflows/ci-cache-warmup.yml +++ b/.github/workflows/ci-cache-warmup.yml @@ -1,16 +1,20 @@ name: Warm shared CI caches on: + # Cache-input pushes seed immediately; this schedule repairs eviction and image changes. schedule: - - cron: '41 * * * *' + - cron: '41 */6 * * *' workflow_dispatch: push: branches: [main] paths: - '.github/workflows/ci-cache-warmup.yml' - '.github/actions/install-node-dependencies/**' + - '.github/actions/restore-pnpm-verification/**' - '.github/actions/prepare-native-runtime/**' - '.github/actions/prepare-git-compatibility/**' + - '.github/actions/prepare-headless-compiler/**' + - 'config/scripts/headless-detector-compiler-cache*' - '.github/actions/prepare-linux-package-fixture/**' - 'config/docker/headless-serve-shutdown/**' - 'config/docker/cli-launch-contract/**' @@ -33,13 +37,14 @@ on: - 'src/shared/zip-extractor-command.ts' - 'config/scripts/shared-electron-dist-cache.mjs' - 'config/scripts/space-sharing-copy.mjs' - - 'config/patches/node-pty@1.1.0.patch' - - 'config/patches/@vscode__windows-process-tree@0.8.0.patch' + - 'config/patches/**' - 'native/windows-registry/**' pull_request: paths: - '.github/workflows/ci-cache-warmup.yml' - '.github/actions/prepare-git-compatibility/**' + - '.github/actions/prepare-headless-compiler/**' + - 'config/scripts/headless-detector-compiler-cache*' - '.github/actions/prepare-linux-package-fixture/**' - 'config/docker/headless-serve-shutdown/**' - 'config/docker/cli-launch-contract/**' @@ -51,7 +56,7 @@ permissions: concurrency: group: ci-cache-warmup-${{ github.event.pull_request.number || github.ref }} - # Hourly retries must let an active warmer finish publishing its caches. + # Scheduled retries must let an active warmer finish publishing its caches. cancel-in-progress: ${{ github.event_name != 'schedule' }} jobs: @@ -71,6 +76,11 @@ jobs: native-runtime: node node-version: '24' cache-electron-package: 'true' + cache-pnpm-store-lookup-only: 'true' + + - uses: ./.github/actions/prepare-headless-compiler + with: + seed: 'true' - name: Populate shared Electron archive run: node config/scripts/install-electron-package-binary.mjs @@ -104,6 +114,7 @@ jobs: native-runtime: node node-version: '24' cache-electron-package: 'true' + cache-pnpm-store-lookup-only: 'true' - name: Populate shared Electron archive run: node config/scripts/install-electron-package-binary.mjs - name: Verify native cache is usable @@ -125,6 +136,7 @@ jobs: - uses: ./.github/actions/install-node-dependencies with: native-runtime: node + cache-pnpm-store-lookup-only: 'true' - name: Verify native cache is usable run: node config/scripts/ensure-native-runtime.mjs --check-only diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 69a487607b0f..7830df9eacf4 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -49,7 +49,7 @@ jobs: # v5 avoids the v6 bootstrap/shim regression when pinning pnpm 10. uses: pnpm/action-setup@v5 with: - version: 10.24.0 + version: 10.34.6 package_json_file: docs/site/package.json run_install: false @@ -224,7 +224,7 @@ jobs: # v5 avoids the v6 bootstrap/shim regression when pinning pnpm 10. uses: pnpm/action-setup@v5 with: - version: 10.24.0 + version: 10.34.6 package_json_file: docs/site/package.json run_install: false diff --git a/.github/workflows/mobile.yml b/.github/workflows/mobile.yml index 8835c6876b3d..ae695b8e3eb6 100644 --- a/.github/workflows/mobile.yml +++ b/.github/workflows/mobile.yml @@ -26,6 +26,7 @@ on: # it or to the release workflow it guards must re-run them. - '.github/workflows/mobile.yml' - '.github/actions/install-node-dependencies/**' + - '.github/actions/restore-pnpm-verification/**' - '.github/workflows/mobile-ios-release.yml' - 'config/scripts/mobile-release-check-scope*' - 'config/scripts/mobile-test-change-scope*' @@ -113,20 +114,20 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - # Both compilers are read-only; finish them before starting the test workers. + # Call installed tools so pnpm's dependency refresh cannot race between checks. - name: Typecheck id: production-types background: true - run: pnpm typecheck + run: node node_modules/typescript/bin/tsc --noEmit + + - wait: production-types # Why a ratchet and not the raw typecheck: mobile/tsconfig.json excludes test files, so until # tsconfig.test.json existed nothing checked them, and at introduction 127 of the 632 had # drifted. This fails when a test file that checks today stops checking, when a test leaves # the program, and on @ts-nocheck; the baseline may only shrink. - name: Typecheck tests (ratchet) - run: pnpm run check:tests-typecheck - - - wait: production-types + run: node scripts/check-tests-typecheck-ratchet.mjs # This includes the bridged replay of the whole recording corpus, which used to be a second # step of its own behind RPC_FOUNDATION_BRIDGE=1. A gate nobody can forget to set is the point: diff --git a/.github/workflows/node-server-tests.yml b/.github/workflows/node-server-tests.yml index d8d6802576ce..0b121c5bb2e1 100644 --- a/.github/workflows/node-server-tests.yml +++ b/.github/workflows/node-server-tests.yml @@ -16,6 +16,8 @@ on: - '.npmrc' - '.pnpmfile.cjs' - '.github/actions/install-node-dependencies/**' + - '.github/actions/restore-pnpm-verification/**' + - '.github/actions/prepare-headless-compiler/**' - '.github/actions/prepare-native-runtime/**' - '.github/actions/prepare-orcad-prebuilds/**' - '.github/workflows/node-server-tests.yml' @@ -35,6 +37,8 @@ on: - '.npmrc' - '.pnpmfile.cjs' - '.github/actions/install-node-dependencies/**' + - '.github/actions/restore-pnpm-verification/**' + - '.github/actions/prepare-headless-compiler/**' - '.github/actions/prepare-native-runtime/**' - '.github/actions/prepare-orcad-prebuilds/**' - '.github/workflows/node-server-tests.yml' @@ -74,15 +78,18 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 outputs: - should_run: ${{ steps.scope.outputs.should_run }} - qualification: ${{ steps.scope.outputs.qualification }} - runners: ${{ steps.scope.outputs.runners }} + should_run: ${{ steps.scope.outputs.should_run || steps.graph.outputs.should_run }} + qualification: ${{ steps.scope.outputs.qualification || steps.graph.outputs.qualification }} + runners: ${{ steps.scope.outputs.runners || steps.graph.outputs.runners }} steps: - uses: actions/checkout@v6 with: fetch-depth: 2 persist-credentials: false - - uses: ./.github/actions/install-node-dependencies + - uses: actions/setup-node@v6 + with: + node-version-file: package.json + package-manager-cache: false - name: Detect headless-server build and test inputs id: scope shell: bash @@ -94,7 +101,7 @@ jobs: # Compare the entire push, including multi-commit pushes and removed files. if git fetch --no-tags --depth=1 origin "$PUSH_BASE" && git diff --name-only --no-renames -z "$PUSH_BASE" HEAD > "$RUNNER_TEMP/node-server-changes"; then - node config/scripts/node-server-change-scope.mjs "$RUNNER_TEMP/node-server-changes" --full-qualification + node config/scripts/node-server-change-scope.mjs "$RUNNER_TEMP/node-server-changes" --defer-graph --full-qualification else echo 'should_run=true' >> "$GITHUB_OUTPUT" fi @@ -102,11 +109,30 @@ jobs: fi # Compare the tested merge with its base, retaining both sides of renames. if git diff --name-only --no-renames -z HEAD^1 HEAD > "$RUNNER_TEMP/node-server-changes"; then - node config/scripts/node-server-change-scope.mjs "$RUNNER_TEMP/node-server-changes" + node config/scripts/node-server-change-scope.mjs "$RUNNER_TEMP/node-server-changes" --defer-graph else echo 'should_run=true' >> "$GITHUB_OUTPUT" fi + - uses: ./.github/actions/prepare-headless-compiler + id: compiler + if: steps.scope.outputs.graph_required == 'true' + continue-on-error: true + - uses: ./.github/actions/install-node-dependencies + if: steps.scope.outputs.graph_required == 'true' && steps.compiler.outputs.available != 'true' + - name: Check the headless import graph + id: graph + if: steps.scope.outputs.graph_required == 'true' + shell: bash + env: + EVENT_NAME: ${{ github.event_name }} + run: | + if [ "$EVENT_NAME" = push ]; then + node config/scripts/node-server-change-scope.mjs "$RUNNER_TEMP/node-server-changes" --full-qualification + else + node config/scripts/node-server-change-scope.mjs "$RUNNER_TEMP/node-server-changes" + fi + persistence: needs: changes concurrency: @@ -121,7 +147,7 @@ jobs: strategy: fail-fast: false matrix: - os: ${{ fromJSON(needs.changes.outputs.runners || '["ubuntu-22.04","ubuntu-24.04-arm","macos-14","macos-15-intel","windows-2022","windows-11-arm"]') }} + os: ${{ fromJSON(needs.changes.outputs.runners || '["ubuntu-22.04","ubuntu-24.04-arm","macos-15","macos-15-intel","windows-2022","windows-11-arm"]') }} runs-on: ${{ matrix.os }} timeout-minutes: 20 env: @@ -134,36 +160,44 @@ jobs: - uses: ./.github/actions/install-node-dependencies with: native-runtime: ${{ runner.os == 'Windows' && 'node' || 'none' }} - # Linux release slots come from the floor and Alpine lanes; this slot serves local tests. - - uses: ./.github/actions/prepare-orcad-prebuilds - id: orcad-prebuild - with: - resolve-windows-cache: >- - ${{ !inputs.build_template && inputs.ref == '' && - (github.event_name == 'pull_request' || - (github.ref == 'refs/heads/main' && contains(fromJSON('["push","schedule","workflow_dispatch"]'), github.event_name))) }} - restore-windows-cache: ${{ github.event_name == 'pull_request' || github.event_name == 'push' }} - - run: pnpm build:orcad + cache-pnpm-store: ${{ runner.os != 'Windows' }} + cache-pnpm-store-lookup-only: 'true' # Design D7 upgrade and rollback: the last Bun orcad, built from a main commit that shipped # it, beside this checkout's Node slot; the live-terminal hand-over skips once PROTOCOL_VERSION - # moves past the Bun daemon's. Same lockfile, so its build reuses this checkout's node_modules. + # moves past the Bun daemon's. Its build uses this checkout's installed dependencies. - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 if: runner.os == 'Linux' with: bun-version: 1.4.2 - name: Build the last Bun orcad for the cross-runtime tests - if: runner.os == 'Linux' + id: bun-orcad + background: true shell: bash env: BUN_ORCAD_COMMIT: f4092c06d639ee13ad446261dcabc78b27a21fbc run: | + if [ "$RUNNER_OS" != Linux ]; then exit 0; fi git fetch --no-tags --depth=1 origin "$BUN_ORCAD_COMMIT" git worktree add --detach "$RUNNER_TEMP/bun-orcad-source" "$BUN_ORCAD_COMMIT" ln -s "$GITHUB_WORKSPACE/node_modules" "$RUNNER_TEMP/bun-orcad-source/node_modules" node "$RUNNER_TEMP/bun-orcad-source/config/scripts/build-orcad-bun.mjs" --out-dir "$RUNNER_TEMP/bun-orcad" - echo "ORCA_BUN_ORCAD_SLOT=$RUNNER_TEMP/bun-orcad" >> "$GITHUB_ENV" - echo "BUN_EXECUTABLE=$(command -v bun)" >> "$GITHUB_ENV" + echo "slot=$RUNNER_TEMP/bun-orcad" >> "$GITHUB_OUTPUT" + echo "executable=$(command -v bun)" >> "$GITHUB_OUTPUT" + # Linux release slots come from the floor and Alpine lanes; this slot serves local tests. + - uses: ./.github/actions/prepare-orcad-prebuilds + id: orcad-prebuild + with: + resolve-windows-cache: >- + ${{ !inputs.build_template && inputs.ref == '' && + (github.event_name == 'pull_request' || + (github.ref == 'refs/heads/main' && contains(fromJSON('["push","schedule","workflow_dispatch"]'), github.event_name))) }} + restore-windows-cache: ${{ github.event_name == 'pull_request' || github.event_name == 'push' }} + - run: pnpm build:orcad + - wait: bun-orcad - run: pnpm test:node-server --artifact ${{ runner.os == 'Linux' && '--cross-runtime' || '' }} + env: + ORCA_BUN_ORCAD_SLOT: ${{ steps.bun-orcad.outputs.slot }} + BUN_EXECUTABLE: ${{ steps.bun-orcad.outputs.executable }} # Only a Windows runner compiles it; arm64 cross-compiles here, as release-cut does for the relay. # Before the Node 18 check below: the build script imports TypeScript, which Node 18 cannot load. - name: Build the Windows process-table addons for the desktop template @@ -363,11 +397,19 @@ jobs: with: ref: ${{ inputs.ref }} persist-credentials: false + - uses: ./.github/actions/restore-pnpm-verification + id: pnpm-verification + with: + container-toolchain: 'true' - name: Verify native Alpine artifact and persistence + env: + VERIFICATION_CACHE_PATH: ${{ steps.pnpm-verification.outputs.path }} run: | + touch "$VERIFICATION_CACHE_PATH" # Multi-arch index digest of the tag; re-resolve it whenever NODE_RUNTIME_PIN moves. docker run --rm --init -i \ -e ORCA_BACKGROUND_LAUNCH=1 \ + -v "$VERIFICATION_CACHE_PATH:/root/.cache/pnpm/lockfile-verified.jsonl" \ -v "$GITHUB_WORKSPACE:/work" -w /work \ node:24.21.0-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401c70e0e762c5b53302f905ec1c1 sh -s <<'NODE_SERVER_QUALIFICATION' set -eu diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 32a167cf0716..a90bd45d91e2 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -415,6 +415,8 @@ jobs: - uses: ./.github/actions/prepare-git-compatibility - name: Verify Git binary compatibility matrix + env: + ORCA_BACKGROUND_LAUNCH: '1' run: | specs=( "alpine/git:edge-2.38.1|2.38.1" @@ -429,9 +431,13 @@ jobs: pids=() ( ORCA_GIT_COMPAT_BINARY="$HOME/.cache/orca-git-compat/git-2.25.5/git" \ + GIT_EXEC_PATH="$HOME/.cache/orca-git-compat/git-2.25.5" \ ORCA_GIT_COMPAT_VERSION="2.25.5" \ pnpm exec vitest run --config config/vitest.config.ts \ - src/shared/git-binary-compatibility.test.ts + src/shared/git-binary-compatibility.test.ts \ + src/main/git/worktree-safety-real-git.test.ts \ + src/main/git/worktree-rebase-update-refs-real-git.test.ts \ + src/relay/git-review-draft-binary-compatibility.test.ts ) & pids+=("$!") @@ -441,7 +447,10 @@ jobs: version="${spec#*|}" ORCA_GIT_COMPAT_IMAGE="$image" ORCA_GIT_COMPAT_VERSION="$version" \ pnpm exec vitest run --config config/vitest.config.ts \ - src/shared/git-binary-compatibility.test.ts + src/shared/git-binary-compatibility.test.ts \ + src/main/git/worktree-safety-real-git.test.ts \ + src/main/git/worktree-rebase-update-refs-real-git.test.ts \ + src/relay/git-review-draft-binary-compatibility.test.ts ) & pids+=("$!") done @@ -702,7 +711,7 @@ jobs: # after the last shard. Deliberately absent from verify's needs for the same reason. unit_selection_evidence: needs: [test] - if: ${{ !cancelled() && needs.test.result == 'success' }} + if: ${{ !cancelled() && (needs.test.result == 'success' || needs.test.result == 'failure') }} uses: ./.github/workflows/unit-selection-evidence.yml # Why a separate job: the test needs a real Chrome, and the sharded `test` matrix @@ -1128,6 +1137,7 @@ jobs: src/shared/child-process/windows-cmd-shim-resolution.test.ts src/shared/child-process/windows-cmd-shim-resolution.win32.test.ts src/main/agent-hooks/windows-hook-payload-delivery.test.ts + src/main/jcode/hook-gate-script.test.ts src/main/agent-hooks/windows-direct-cmd-hook-command.test.ts src/main/codex/windows-hook-command.test.ts src/main/codex/windows-hook-upgrade.test.ts diff --git a/.github/workflows/release-cut.yml b/.github/workflows/release-cut.yml index 4228b31fd210..3073d160d11a 100644 --- a/.github/workflows/release-cut.yml +++ b/.github/workflows/release-cut.yml @@ -79,6 +79,7 @@ jobs: source_ref: ${{ steps.resolve.outputs.ref }} source_sha: ${{ steps.resolve.outputs.sha }} source_short_sha: ${{ steps.resolve.outputs.short_sha }} + ships_orcad_template: ${{ steps.orcad-template-support.outputs.ships }} steps: # Why inlined (not m-s-abeer/update-gha-summary-with-workflow-inputs): # this job runs with contents:write and secret scope, so avoid executing @@ -783,6 +784,21 @@ jobs: git push origin "$TAG" fi + # Why: a patch cut from a base older than #24155 has no orcad template to build or ship. + - name: Detect whether the tag ships the orcad template + id: orcad-template-support + if: steps.tag.outputs.tag != '' || steps.version.outputs.recovered_tag != '' + env: + TAG: ${{ steps.tag.outputs.tag || steps.version.outputs.recovered_tag }} + run: | + set -euo pipefail + if git cat-file -e "refs/tags/${TAG}^{commit}:config/scripts/packaged-orcad-template.cjs" 2>/dev/null; then + echo "ships=true" >>"$GITHUB_OUTPUT" + else + echo "ships=false" >>"$GITHUB_OUTPUT" + echo "::notice::$TAG predates the orcad template; skipping its build and download." + fi + - name: Release E2E signal summary if: always() run: | @@ -1154,7 +1170,7 @@ jobs: # signing quota, so it runs beside the release gates instead of behind them. orcad-template: needs: cut - if: needs.cut.outputs.should_release == 'true' + if: needs.cut.outputs.should_release == 'true' && needs.cut.outputs.ships_orcad_template == 'true' permissions: contents: read uses: ./.github/workflows/node-server-tests.yml @@ -1203,7 +1219,16 @@ jobs: - orcad-template - release-preflight - relay-windows-process-tree - if: needs.cut.outputs.should_release == 'true' + # Why not the implicit success(): a tag without the orcad template skips that job on purpose. + if: >- + !cancelled() && + needs.cut.result == 'success' && + needs.cut.outputs.should_release == 'true' && + needs.create-release.result == 'success' && + needs.release-preflight.result == 'success' && + needs.relay-windows-process-tree.result == 'success' && + (needs.orcad-template.result == 'success' || + (needs.orcad-template.result == 'skipped' && needs.cut.outputs.ships_orcad_template == 'false')) env: # beforePack and afterPack fail the package when the template is absent. ORCA_REQUIRE_ORCAD_TEMPLATE: '1' @@ -1463,6 +1488,7 @@ jobs: # After the app build so nothing that cleans out/ can drop it; electron-builder ships it. - name: Download the orcad deployment template + if: needs.cut.outputs.ships_orcad_template == 'true' uses: actions/download-artifact@v8 with: name: orcad-template @@ -1806,7 +1832,7 @@ jobs: # each file against the template manifest, so it must record the signed bytes. - name: Reseal the orcad template over its signed binaries id: reseal-orcad-template - if: matrix.platform == 'win' && github.run_attempt == 1 && steps.restore-signed-inner.outcome == 'success' + if: matrix.platform == 'win' && github.run_attempt == 1 && steps.restore-signed-inner.outcome == 'success' && needs.cut.outputs.ships_orcad_template == 'true' run: node config/scripts/packaged-orcad-template.cjs --reseal-signed dist/win-unpacked inner-signing-list.txt # The uninstaller must return signed before rebuilding the installer. @@ -2306,7 +2332,16 @@ jobs: - release-preflight # release-mac-build.yml downloads the relay addons from this run. - relay-windows-process-tree - if: needs.cut.outputs.should_release == 'true' + # Why not the implicit success(): a tag without the orcad template skips that job on purpose. + if: >- + !cancelled() && + needs.cut.result == 'success' && + needs.cut.outputs.should_release == 'true' && + needs.create-release.result == 'success' && + needs.release-preflight.result == 'success' && + needs.relay-windows-process-tree.result == 'success' && + (needs.orcad-template.result == 'success' || + (needs.orcad-template.result == 'skipped' && needs.cut.outputs.ships_orcad_template == 'false')) # Why: SignPath requires every job in this signing workflow to be # GitHub-hosted. The actual mac build runs in release-mac-build.yml so # Blacksmith stays outside Windows artifact provenance. @@ -2340,6 +2375,15 @@ jobs: - skill-sharing-linux-floor-release-gate - skill-sharing-release-gate - terminal-rendering-golden + # Why explicit: a skipped orcad-template (tags that predate it) would skip every job after it. + if: >- + !cancelled() && + needs.cut.result == 'success' && + needs.build.result == 'success' && + needs.build-mac.result == 'success' && + needs.skill-sharing-linux-floor-release-gate.result == 'success' && + needs.skill-sharing-release-gate.result == 'success' && + needs.terminal-rendering-golden.result == 'success' runs-on: ubuntu-latest permissions: contents: write @@ -2408,7 +2452,8 @@ jobs: needs: - cut - publish-release - if: ${{ needs.cut.outputs.tag != '' }} + # Why explicit: a skipped orcad-template (tags that predate it) would skip every job after it. + if: ${{ !cancelled() && needs.publish-release.result == 'success' && needs.cut.outputs.tag != '' }} runs-on: ubuntu-latest permissions: actions: write @@ -2437,7 +2482,8 @@ jobs: # A release created with GITHUB_TOKEN does not reliably emit a release # event to other workflows. Dispatch the trusted default-branch workflow; # it validates and checks out the released tag before deploying. - if: ${{ needs.cut.outputs.tag != '' }} + # Why explicit: a skipped orcad-template (tags that predate it) would skip every job after it. + if: ${{ !cancelled() && needs.publish-release.result == 'success' && needs.cut.outputs.tag != '' }} runs-on: ubuntu-latest permissions: actions: write @@ -2481,7 +2527,8 @@ jobs: needs: - cut - publish-release - if: ${{ needs.cut.outputs.tag != '' && startsWith(needs.cut.outputs.tag, 'v') }} + # Why explicit: a skipped orcad-template (tags that predate it) would skip every job after it. + if: ${{ !cancelled() && needs.publish-release.result == 'success' && needs.cut.outputs.tag != '' && startsWith(needs.cut.outputs.tag, 'v') }} uses: ./.github/workflows/homebrew-bump.yml with: tag: ${{ needs.cut.outputs.tag }} diff --git a/.github/workflows/release-mac-build.yml b/.github/workflows/release-mac-build.yml index b8555ffbdeb0..80694364bae4 100644 --- a/.github/workflows/release-mac-build.yml +++ b/.github/workflows/release-mac-build.yml @@ -158,6 +158,8 @@ jobs: # Design D2: the parent release-cut run merged it from every node-server lane at this tag. - name: Download the orcad deployment template from the release run + # Why: release-cut skips the template for a tag that predates it. + if: hashFiles('config/scripts/packaged-orcad-template.cjs') != '' uses: actions/download-artifact@v8 with: name: orcad-template diff --git a/.github/workflows/release-policy.yml b/.github/workflows/release-policy.yml index 53decc5e7e39..512a5fb8ea8c 100644 --- a/.github/workflows/release-policy.yml +++ b/.github/workflows/release-policy.yml @@ -22,7 +22,9 @@ jobs: # Why: release events run this file from the tagged commit, so load the module from the same commit. - uses: actions/checkout@v6 with: - sparse-checkout: config/scripts/release-policy.mjs + sparse-checkout: | + config/scripts/release-policy.mjs + config/scripts/release-tag-patterns.mjs sparse-checkout-cone-mode: false persist-credentials: false - name: Enforce release policy diff --git a/.github/workflows/ssh-hostile-hosts.yml b/.github/workflows/ssh-hostile-hosts.yml index a77f66a14b1d..c96382dbb40b 100644 --- a/.github/workflows/ssh-hostile-hosts.yml +++ b/.github/workflows/ssh-hostile-hosts.yml @@ -27,6 +27,7 @@ on: - '!src/**/*.test.ts' - 'src/main/ssh/ssh-relay-hostile-hosts.docker.test.ts' - '.github/workflows/ssh-hostile-hosts.yml' + - '.github/actions/restore-pnpm-verification/**' workflow_dispatch: inputs: cells: @@ -56,7 +57,17 @@ jobs: PYTHON: /opt/python/cp312-cp312/bin/python3 steps: - name: Install glibc 2.28 prerequisites - run: dnf install -y git procps-ng unzip which xz + run: | + missing_tool=false + for tool in git ps unzip which xz; do + if ! command -v "$tool" >/dev/null 2>&1; then + missing_tool=true + fi + done + if [ "$missing_tool" = true ]; then + # The image's source-built Git needs no RPM; missing tools come from AlmaLinux. + dnf --disablerepo='epel*' install -y git procps-ng unzip which xz + fi - uses: actions/checkout@v6 with: persist-credentials: false @@ -89,11 +100,19 @@ jobs: with: name: hostile-hosts-glibc-slot path: out/orcad-prebuilds + - uses: ./.github/actions/restore-pnpm-verification + id: pnpm-verification + with: + container-toolchain: 'true' - name: Build and smoke the linux-x64-musl slot on Alpine + env: + VERIFICATION_CACHE_PATH: ${{ steps.pnpm-verification.outputs.path }} run: | + touch "$VERIFICATION_CACHE_PATH" # Same digest as the headless-server musl lane; re-resolve it whenever NODE_RUNTIME_PIN moves. docker run --rm --init -i \ -e ORCA_BACKGROUND_LAUNCH=1 \ + -v "$VERIFICATION_CACHE_PATH:/root/.cache/pnpm/lockfile-verified.jsonl" \ -v "$GITHUB_WORKSPACE:/work" -w /work \ node:24.21.0-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401c70e0e762c5b53302f905ec1c1 sh -s <<'MUSL_SLOT' set -eu @@ -198,7 +217,7 @@ jobs: fail-fast: false matrix: include: - - os: macos-14 + - os: macos-15 target: darwin-arm64 cell: macos-arm64-local-sshd - os: macos-15-intel diff --git a/.github/workflows/ssh-windows-hosts.yml b/.github/workflows/ssh-windows-hosts.yml index a677bd8589e4..8afae35ca12b 100644 --- a/.github/workflows/ssh-windows-hosts.yml +++ b/.github/workflows/ssh-windows-hosts.yml @@ -36,6 +36,7 @@ on: - 'config/ci/windows-ssh-provider/**' - '.github/workflows/ssh-windows-hosts.yml' - '.github/actions/prepare-orcad-prebuilds/**' + - '.github/actions/restore-pnpm-verification/**' - 'config/scripts/orcad-windows-prebuild-cache.mjs' workflow_dispatch: inputs: @@ -83,10 +84,6 @@ jobs: - uses: actions/checkout@v6 with: persist-credentials: false - - uses: ./.github/actions/install-node-dependencies - id: dependencies - with: - native-runtime: node - name: Self-test the provisioning scripts before touching the machine shell: pwsh run: | @@ -96,6 +93,23 @@ jobs: if($errors.Count){throw "PowerShell parse failed: $($file.FullName)"} } & config/ci/windows-ssh-provider/preview-ssh/test-preview-diagnostics.ps1 + # ARM inbox servicing can finish while the independent Node artifacts are prepared. + - name: Prepare the Windows inbox SSH capability + id: inbox-capability + background: true + shell: pwsh + run: | + if('${{ matrix.server }}' -eq 'inbox' -and '${{ matrix.arch }}' -eq 'arm64'){ + $receipts=Join-Path $pwd '.build/ssh-windows-host-receipts' + New-Item -ItemType Directory -Force -Path $receipts | Out-Null + . config/ci/windows-ssh-provider/preview-ssh/windows-ssh-capability.ps1 + Initialize-WindowsInboxSshCapability -Arch '${{ matrix.arch }}' -Receipt (Join-Path $receipts 'inbox-capability-preparation.json') + } + - uses: ./.github/actions/install-node-dependencies + id: dependencies + with: + native-runtime: node + cache-pnpm-store: 'false' # The deploy materializes rung A from this template; only this runner's slot exists here. # The process-tree addon carries the launcher that starts the relay outside sshd's job; the # orcad slot and the relay both stage it, and a standard-user host has no other launch route. @@ -121,6 +135,7 @@ jobs: pnpm build:orcad-prebuilds --require-slots "win32-${{ matrix.arch }}" node config/scripts/build-orcad-template.mjs --targets "win32-${{ matrix.arch }}" pnpm run build:relay + - wait: inbox-capability - name: Run the Windows host cells against a private ${{ matrix.server }} sshd shell: pwsh timeout-minutes: 50 @@ -134,6 +149,8 @@ jobs: $cells=@($env:CELLS -split ',' | ForEach-Object {$_.Trim()} | Where-Object {$_}) if(-not $cells.Count){$cells=@('pinned-cmd','pinned-powershell','legacy-opt-out')} $archive='' + $preparation='' + if('${{ matrix.server }}' -eq 'inbox' -and '${{ matrix.arch }}' -eq 'arm64'){$preparation=Join-Path $receipts 'inbox-capability-preparation.json'} if('${{ matrix.server }}' -eq 'preview'){ $archive=Join-Path $env:RUNNER_TEMP 'preview-${{ matrix.archive }}' # The provisioning script refuses the archive unless its sha256 and every binary's match the pin. @@ -143,7 +160,7 @@ jobs: $callback={param($context) & (Join-Path $tools 'invoke-pinned-relay-cells.ps1') -SourceRoot $sourceRoot -Context $context -Target 'win32-${{ matrix.arch }}' -ReceiptRoot $receipts -Cells $cells }.GetNewClosure() - & (Join-Path $tools 'preview-ssh/prove-preview-openssh.ps1') -Archive $archive -Arch '${{ matrix.arch }}' -Server '${{ matrix.server }}' -Receipt (Join-Path $receipts 'provider-server.json') -Accounts $cells.Count -HiddenTools @('npm','npx','node-gyp','gcc','g++','cc','c++','make','cl','clang','clang++','msbuild','cmake') -HostCellProbe $callback 2>&1 | Tee-Object (Join-Path $receipts 'provision.log') + & (Join-Path $tools 'preview-ssh/prove-preview-openssh.ps1') -Archive $archive -Arch '${{ matrix.arch }}' -Server '${{ matrix.server }}' -Receipt (Join-Path $receipts 'provider-server.json') -InboxPreparationReceipt $preparation -Accounts $cells.Count -HiddenTools @('npm','npx','node-gyp','gcc','g++','cc','c++','make','cl','clang','clang++','msbuild','cmake') -HostCellProbe $callback 2>&1 | Tee-Object (Join-Path $receipts 'provision.log') - uses: actions/upload-artifact@v7 if: always() with: diff --git a/.github/workflows/terminal-perf.yml b/.github/workflows/terminal-perf.yml index 83ada43d2cf9..2f3a37524ebc 100644 --- a/.github/workflows/terminal-perf.yml +++ b/.github/workflows/terminal-perf.yml @@ -68,25 +68,60 @@ jobs: - name: Install native build tools and xvfb run: sudo apt-get update && sudo apt-get install -y build-essential python3 xvfb zsh + - name: Select dependency preparation + id: install-mode + shell: bash + env: + RUNNER_KIND: ${{ runner.environment }} + JOB_CONTAINER: ${{ job.container.id }} + run: | + node <<'NODE' + const fs = require('node:fs') + const manifest = JSON.parse(fs.readFileSync('package.json', 'utf8')) + const actionPath = '.github/actions/install-node-dependencies/action.yml' + const action = fs.existsSync(actionPath) ? fs.readFileSync(actionPath, 'utf8') : '' + const inputs = action.split(/^inputs:[ \t]*\r?$/m)[1]?.split(/^\S/m)[0] ?? '' + const shared = process.env.RUNNER_KIND === 'github-hosted' && !process.env.JOB_CONTAINER && + process.env.RUNNER_OS === 'Linux' && process.env.RUNNER_ARCH === 'X64' && + manifest.engines?.node === '24' && typeof manifest.packageManager === 'string' && + manifest.packageManager.split('+')[0] === 'pnpm@12.8.1' && + manifest.scripts?.postinstall === 'node config/scripts/rebuild-native-deps.mjs' && + /^ native-runtime:/m.test(inputs) && /^ cache-pnpm-store-lookup-only:/m.test(inputs) && + fs.existsSync('.github/actions/prepare-native-runtime/action.yml') && + fs.existsSync('config/scripts/ensure-native-runtime.mjs') + fs.appendFileSync(process.env.GITHUB_OUTPUT, `shared=${shared}\n`) + NODE + + - name: Prepare current dependencies + if: steps.install-mode.outputs.shared == 'true' + uses: ./.github/actions/install-node-dependencies + with: + native-runtime: electron + cache-electron-package: 'true' + cache-pnpm-store-lookup-only: 'true' + - name: Setup pnpm + if: steps.install-mode.outputs.shared != 'true' uses: pnpm/setup@v2 with: install: false - name: Setup Node.js + if: steps.install-mode.outputs.shared != 'true' uses: actions/setup-node@v6 with: node-version-file: package.json cache: pnpm - # Why: this scheduled/manual workflow uses the same native install path as - # PR and E2E CI, which needs pnpm to bypass its bundled gyp_main.py. + # Historical refs can lack the shared action; retain their original install path. - name: Use external node-gyp to avoid pnpm's bundled copy + if: steps.install-mode.outputs.shared != 'true' run: | npm install -g node-gyp@11.5.0 echo "npm_config_node_gyp=$(npm root -g)/node-gyp/bin/node-gyp.js" >> "$GITHUB_ENV" - name: Install dependencies + if: steps.install-mode.outputs.shared != 'true' run: pnpm install --frozen-lockfile - name: Build Electron app for terminal perf diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 2e6f173acc45..fed0c4c31d1c 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -105,8 +105,8 @@ jobs: - name: Install relay integration dependencies working-directory: cloud run: | - npx --yes pnpm@10.24.0 --filter '@orca-cloud/relay...' install --frozen-lockfile --ignore-scripts - npx --yes pnpm@10.24.0 --filter '@orca-cloud/relay^...' build + npx --yes pnpm@10.34.6 --filter '@orca-cloud/relay...' install --frozen-lockfile --ignore-scripts + npx --yes pnpm@10.34.6 --filter '@orca-cloud/relay^...' build - name: Test relay integration contracts env: diff --git a/.gitignore b/.gitignore index dbb74b9c9504..ffa4bc4f600e 100644 --- a/.gitignore +++ b/.gitignore @@ -128,6 +128,7 @@ docs/** !docs/reference/git-compatibility.md !docs/reference/headless-linux-server.md !docs/reference/ime-regression-checklist.md +!docs/reference/jcode-hook-events.md !docs/reference/linux-glibc-compatibility.md !docs/reference/macos-press-and-hold.md !docs/reference/orcad-operations.md diff --git a/cloud/apps/push/package.json b/cloud/apps/push/package.json index b47964587acb..a4ba2462f5b1 100644 --- a/cloud/apps/push/package.json +++ b/cloud/apps/push/package.json @@ -15,20 +15,20 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@hono/node-server": "^1.19.17", + "@hono/node-server": "^2.1.2", "@orca-cloud/postgres-schema": "workspace:*", "@orca-cloud/push-contract": "workspace:*", "google-auth-library": "^10.5.0", - "hono": "^4.13.7", + "hono": "^4.13.10", "pg": "^8.22.0", "pg-connection-string": "2.14.0", "tweetnacl": "^1.0.3", "zod": "^3.25.76" }, "devDependencies": { - "@types/node": "^24.10.0", + "@types/node": "^24.19.0", "@types/pg": "^8.20.0", - "tsx": "^4.21.0", + "tsx": "^4.23.15", "typescript": "^5.9.3", "vitest": "^4.1.11" } diff --git a/cloud/apps/push/src/durable-push-payload-parse-budget.test.ts b/cloud/apps/push/src/durable-push-payload-parse-budget.test.ts new file mode 100644 index 000000000000..f04c6daadcd3 --- /dev/null +++ b/cloud/apps/push/src/durable-push-payload-parse-budget.test.ts @@ -0,0 +1,244 @@ +import { createHash } from 'node:crypto' +import { afterEach, expect, it, vi } from 'vitest' +import { DurablePushStore } from './durable-push-store.js' +import { buildPushDelivery } from './push-delivery-message.js' +import type { PushDatabase } from './push-database.js' +import { + cleanupDurablePushFixtures, + fixture, + notification +} from './durable-push-store.test-fixture.js' + +type QueryCall = { sql: string; params?: unknown[] } + +afterEach(async () => { + vi.restoreAllMocks() + await cleanupDurablePushFixtures() +}) + +function traceDatabase( + database: PushDatabase, + calls: QueryCall[], + errors: unknown[] +): PushDatabase { + return { + dialect: database.dialect, + query: async (sql, params) => { + calls.push({ sql, params }) + try { + return await database.query(sql, params) + } catch (error) { + errors.push(error) + throw error + } + }, + transaction: (run) => database.transaction((tx) => run(traceDatabase(tx, calls, errors))), + lockQuotaScope: (key) => database.lockQuotaScope(key), + tryLockScope: (key) => database.tryLockScope(key), + tryLockSharedScope: (key) => database.tryLockSharedScope(key), + close: () => database.close() + } +} + +function payloadHash(value: unknown): string { + return createHash('sha256').update(JSON.stringify(value)).digest('hex') +} + +it('parses each leased row once with exact complete payload, serialized key order and SQL sequence', async () => { + const { db, store, clock } = await fixture() + const input = { + ...notification(1), + body: 'Unicode: 🐋\ud800', + extra: { first: [null, false, 3], next: { z: 'last', a: 'first' } } + } + await store.accept('host', 'phone', input) + const [row] = await db.query('SELECT * FROM push_delivery_batches') + if (!row) { + throw new Error('Missing delivery row') + } + const payload = String(row.payload_json) + const calls: QueryCall[] = [] + const errors: unknown[] = [] + const owner = new DurablePushStore(traceDatabase(db, calls, errors), clock) + const parse = vi.spyOn(JSON, 'parse') + const delivery = await owner.claim() + expect(delivery).toEqual({ + id: row.batch_id, + registrationId: 'phone', + hostFingerprint: 'host', + notification: input, + expiresAt: 1_300_000, + lease: expect.any(String), + attempts: 1 + }) + expect(JSON.stringify(delivery?.notification)).toBe(payload) + expect(payloadHash(delivery?.notification)).toBe(payloadHash(input)) + if (!delivery) { + throw new Error('Missing delivery') + } + const published = buildPushDelivery(delivery) + const expected = buildPushDelivery({ ...delivery, notification: input }) + expect(JSON.stringify(published)).toBe(JSON.stringify(expected)) + expect(payloadHash(published)).toBe(payloadHash(expected)) + expect(calls.map(({ sql }) => sql.replace(/\s+/g, ' ').trim())).toEqual([ + `SELECT * FROM push_delivery_batches WHERE state = 'pending' AND lease_until <= ? AND expires_at > ? AND due_at <= ? AND due_at > ? AND NOT EXISTS (SELECT 1 FROM push_delivery_batches busy WHERE busy.registration_id = push_delivery_batches.registration_id AND busy.state = 'pending' AND busy.lease_until > 0 AND busy.lease_until > ?) ORDER BY due_at, created_at, batch_id LIMIT 1${db.dialect === 'postgres' ? ' FOR UPDATE SKIP LOCKED' : ''}`, + "SELECT (SELECT batch_id FROM push_delivery_batches WHERE registration_id = ? AND state = 'pending' AND expires_at > ? AND due_at > ? ORDER BY due_at, created_at, batch_id LIMIT 1) AS head, EXISTS (SELECT 1 FROM push_delivery_batches WHERE registration_id = ? AND state = 'pending' AND lease_until > 0 AND lease_until > ?) AS busy", + 'SELECT notification_seq FROM push_dismissed_events WHERE host_fingerprint = ? AND notification_epoch = ? AND notification_id = ?', + 'UPDATE push_delivery_batches SET lease_token = ?, lease_until = ?, attempts = attempts + 1 WHERE batch_id = ?' + ]) + expect(calls[1]?.params).toEqual(['phone', clock(), clock() - 300_000, 'phone', clock()]) + expect(calls[2]?.params).toEqual(['host', 'epoch', 'notification-1']) + expect(calls[3]?.params).toEqual([delivery?.lease, clock() + 30_000, row.batch_id]) + expect(errors).toEqual([]) + expect(parse.mock.calls.filter(([value]) => value === payload)).toHaveLength(1) +}) + +it('keeps concurrent device claims separate and returns fresh payload objects', async () => { + const { store } = await fixture() + const input = notification(1) + await store.accept('host', 'phone-a', input) + await store.accept('host', 'phone-b', input) + const payload = JSON.stringify(input) + const parse = vi.spyOn(JSON, 'parse') + const claims = await Promise.all(Array.from({ length: 4 }, () => store.claim())) + const delivered = claims.filter((claim) => claim !== null) + expect(delivered).toHaveLength(2) + expect(delivered.map((claim) => claim.registrationId).sort()).toEqual(['phone-a', 'phone-b']) + expect(delivered.every((claim) => JSON.stringify(claim.notification) === payload)).toBe(true) + expect(delivered[0]?.notification).not.toBe(delivered[1]?.notification) + expect(parse.mock.calls.filter(([value]) => value === payload)).toHaveLength(2) +}) + +it('reads changed retry bytes and a later writer update without carrying a parsed result across calls', async () => { + const { db, store, advance } = await fixture() + await store.accept('host', 'phone', notification(1)) + const first = await store.claim() + if (!first) { + throw new Error('Missing first delivery') + } + first.notification.body = 'provider changed this retry' + await store.finish(first, 1000) + advance(1000) + const retriedPayload = JSON.stringify(first.notification) + const parse = vi.spyOn(JSON, 'parse') + const retry = await store.claim() + expect(retry).toEqual({ ...first, lease: expect.any(String), attempts: 2 }) + expect(retry?.lease).not.toBe(first.lease) + expect(retry?.notification).not.toBe(first.notification) + expect(JSON.stringify(retry?.notification)).toBe(retriedPayload) + expect(payloadHash(retry?.notification)).toBe(payloadHash(first.notification)) + const retryParses = parse.mock.calls.filter(([value]) => value === retriedPayload).length + if (!retry) { + throw new Error('Missing retry delivery') + } + await store.finish(retry, 1000) + const changed = { ...notification(1), body: 'fresh database row', title: 'Changed' } + const changedPayload = JSON.stringify(changed) + await db.query('UPDATE push_delivery_batches SET payload_json = ? WHERE batch_id = ?', [ + changedPayload, + first.id + ]) + advance(1000) + const fresh = await store.claim() + expect(fresh).toEqual({ ...retry, notification: changed, lease: expect.any(String), attempts: 3 }) + expect(JSON.stringify(fresh?.notification)).toBe(changedPayload) + expect(payloadHash(fresh?.notification)).toBe(payloadHash(changed)) + expect(retry.notification.body).toBe('provider changed this retry') + expect(retryParses).toBe(1) + expect(parse.mock.calls.filter(([value]) => value === changedPayload)).toHaveLength(1) +}) + +it('keeps dismissed alerts on the original single-parse delete path without leasing', async () => { + const { db, store, clock } = await fixture() + const input = notification(1) + await store.accept('host', 'phone', input) + await db.query( + 'INSERT INTO push_dismissed_events(host_fingerprint, notification_epoch, notification_id, notification_seq, created_at) VALUES (?, ?, ?, ?, ?)', + ['host', 'epoch', input.notificationId, 1, clock()] + ) + const calls: QueryCall[] = [] + const parse = vi.spyOn(JSON, 'parse') + expect(await new DurablePushStore(traceDatabase(db, calls, []), clock).claim()).toBeNull() + expect(await store.pendingCount('phone')).toBe(0) + expect(calls.at(-1)?.sql).toBe('DELETE FROM push_delivery_batches WHERE batch_id = ?') + expect(calls.some(({ sql }) => sql.startsWith('UPDATE'))).toBe(false) + expect(parse.mock.calls.filter(([value]) => value === JSON.stringify(input))).toHaveLength(1) +}) + +it('preserves the existing trust boundary for an object missing notification fields', async () => { + const { db, store } = await fixture() + await store.accept('host', 'phone', notification(1)) + await db.query('UPDATE push_delivery_batches SET payload_json = ?', ['{}']) + const parse = vi.spyOn(JSON, 'parse') + const delivery = await store.claim() + expect(delivery?.notification).toEqual({}) + expect(JSON.stringify(delivery?.notification)).toBe('{}') + expect(parse.mock.calls.filter(([value]) => value === '{}')).toHaveLength(1) +}) + +it.each(['not JSON', 'undefined', 'null', '[]'])( + 'preserves invalid payload rejection and rolls back the lease for %s', + async (payload) => { + const { db, store } = await fixture() + await store.accept('host', 'phone', notification(1)) + await db.query('UPDATE push_delivery_batches SET payload_json = ?', [payload]) + const [before] = await db.query('SELECT * FROM push_delivery_batches') + const parse = vi.spyOn(JSON, 'parse') + let caught: unknown + try { + await store.claim() + } catch (error) { + caught = error + } + expect(caught).toBeInstanceOf(Error) + const index = parse.mock.calls.findIndex(([value]) => value === payload) + expect(index).toBeGreaterThanOrEqual(0) + if (payload === 'not JSON' || payload === 'undefined') { + expect(caught).toBe(parse.mock.results[index]?.value) + expect(caught).toBeInstanceOf(SyntaxError) + } else { + expect(caught).toMatchObject({ message: 'invalid_push_delivery_payload' }) + } + expect(await db.query('SELECT * FROM push_delivery_batches')).toEqual([before]) + expect(parse.mock.calls.filter(([value]) => value === payload)).toHaveLength(1) + } +) + +it('preserves the exact database UPDATE error and retries with a fresh payload after rollback', async () => { + const { db, store, clock } = await fixture() + await store.accept('host', 'phone', notification(1)) + const [before] = await db.query('SELECT * FROM push_delivery_batches') + if (!before) { + throw new Error('Missing delivery row') + } + const originalQuery = db.query.bind(db) + const errors: unknown[] = [] + // SQLite raises a native error in the real transaction; PostgreSQL uses its real constraint. + await originalQuery( + db.dialect === 'sqlite' + ? "CREATE TRIGGER deny_lease BEFORE UPDATE ON push_delivery_batches BEGIN SELECT RAISE(FAIL, 'deny_lease'); END" + : 'ALTER TABLE push_delivery_batches ADD CONSTRAINT deny_lease CHECK (lease_until = 0)' + ) + const owner = new DurablePushStore(traceDatabase(db, [], errors), clock) + const parse = vi.spyOn(JSON, 'parse') + let caught: unknown + try { + await owner.claim() + } catch (error) { + caught = error + } + expect(errors).toHaveLength(1) + expect(caught).toBe(errors[0]) + expect(await originalQuery('SELECT * FROM push_delivery_batches')).toEqual([before]) + expect(parse.mock.calls.filter(([value]) => value === String(before.payload_json))).toHaveLength( + 1 + ) + await originalQuery( + db.dialect === 'sqlite' + ? 'DROP TRIGGER deny_lease' + : 'ALTER TABLE push_delivery_batches DROP CONSTRAINT deny_lease' + ) + const fresh = await owner.claim() + expect(fresh?.notification).toEqual(notification(1)) + expect(fresh?.attempts).toBe(1) +}) diff --git a/cloud/apps/push/src/durable-push-store.ts b/cloud/apps/push/src/durable-push-store.ts index b84ebb178934..9456e25743e9 100644 --- a/cloud/apps/push/src/durable-push-store.ts +++ b/cloud/apps/push/src/durable-push-store.ts @@ -153,15 +153,15 @@ export class DurablePushStore { 'UPDATE push_delivery_batches SET lease_token = ?, lease_until = ?, attempts = attempts + 1 WHERE batch_id = ?', [lease, now + DELIVERY_LEASE_MS, row.batch_id] ) - return this.delivery(row, lease) + return this.delivery(row, lease, notification) } - private delivery(row: SqlRow, lease: string): QueuedPushDelivery { + private delivery(row: SqlRow, lease: string, notification: PushNotification): QueuedPushDelivery { return { id: String(row.batch_id), registrationId: String(row.registration_id), hostFingerprint: String(row.host_fingerprint), - notification: parsePushDeliveryPayload(String(row.payload_json)), + notification, expiresAt: Number(row.expires_at), lease, attempts: Number(row.attempts) + 1 diff --git a/cloud/apps/relay-fence-broker/package.json b/cloud/apps/relay-fence-broker/package.json index b14f162d77dd..6e95ece0581b 100644 --- a/cloud/apps/relay-fence-broker/package.json +++ b/cloud/apps/relay-fence-broker/package.json @@ -14,13 +14,13 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@hono/node-server": "^1.19.17", - "hono": "^4.13.7", + "@hono/node-server": "^2.1.2", + "hono": "^4.13.10", "zod": "^3.25.76" }, "devDependencies": { - "@types/node": "^24.10.0", - "tsx": "^4.21.0", + "@types/node": "^24.19.0", + "tsx": "^4.23.15", "typescript": "^5.9.3", "vitest": "^4.1.11" } diff --git a/cloud/apps/relay-ops/package.json b/cloud/apps/relay-ops/package.json index 5865d6c441d2..d3961198665d 100644 --- a/cloud/apps/relay-ops/package.json +++ b/cloud/apps/relay-ops/package.json @@ -17,13 +17,13 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@hono/node-server": "^1.19.17", - "hono": "^4.13.7", + "@hono/node-server": "^2.1.2", + "hono": "^4.13.10", "zod": "^3.25.76" }, "devDependencies": { - "@types/node": "^24.10.0", - "tsx": "^4.21.0", + "@types/node": "^24.19.0", + "tsx": "^4.23.15", "typescript": "^5.9.3", "vitest": "^4.1.11" } diff --git a/cloud/apps/relay/package.json b/cloud/apps/relay/package.json index e9624bc3080e..0a85bc9343e2 100644 --- a/cloud/apps/relay/package.json +++ b/cloud/apps/relay/package.json @@ -15,21 +15,21 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@hono/node-server": "^1.19.17", + "@hono/node-server": "^2.1.2", "@orca-cloud/postgres-schema": "workspace:*", "@orca-cloud/relay-contract": "workspace:*", - "hono": "^4.13.7", - "jose": "^6.1.3", + "hono": "^4.13.10", + "jose": "^6.2.12", "pg": "^8.22.0", "tweetnacl": "^1.0.3", - "ws": "^8.21.3", + "ws": "^8.22.0", "zod": "^3.25.76" }, "devDependencies": { - "@types/node": "^24.10.0", + "@types/node": "^24.19.0", "@types/pg": "^8.20.0", "@types/ws": "^8.18.1", - "tsx": "^4.21.0", + "tsx": "^4.23.15", "typescript": "^5.9.3", "vitest": "^4.1.11" } diff --git a/cloud/apps/relay/src/regional-rehome-target-row-lock-postgres.test.ts b/cloud/apps/relay/src/regional-rehome-target-row-lock-postgres.test.ts index 314b758b17f3..c61c6b3ff0f3 100644 --- a/cloud/apps/relay/src/regional-rehome-target-row-lock-postgres.test.ts +++ b/cloud/apps/relay/src/regional-rehome-target-row-lock-postgres.test.ts @@ -32,7 +32,7 @@ const CELL_TABLES = [ // The target-row statement locks exactly these, held from it to COMMIT. const TARGET_LOCKED = ['target:relay_cells', 'target:relay_cell_admission'] -type Trip = { sql: string; lockable: Record } +type Trip = { sql: string; lockable: Record; probeMs: number } type DelayControl = StatementDelay & { beforeTrip: (sql: string) => Promise } @@ -159,11 +159,12 @@ describePostgres('PostgreSQL regional rehome target-row lock', () => { bystander: context.bystander.id } control.beforeTrip = async (sql) => { + const probeStartedAt = performance.now() const state: Record = {} for (const [role, cellId] of Object.entries(probed)) { for (const table of CELL_TABLES) state[`${role}:${table}`] = await lockable(table, cellId) } - trips.push({ sql, lockable: state }) + trips.push({ sql, lockable: state, probeMs: performance.now() - probeStartedAt }) } consumeRelayCellInventoryHold(delayed) control.enabled = true @@ -188,13 +189,22 @@ describePostgres('PostgreSQL regional rehome target-row lock', () => { ]) } const counts = consumeRelayCellInventoryHold(delayed) + const commitProbeMs = trips.at(-1)!.probeMs console.info( - JSON.stringify({ event: 'rehome_target_row_hold', trips: trips.length, ...counts }) + JSON.stringify({ + event: 'rehome_target_row_hold', + trips: trips.length, + commitProbeMs, + ...counts + }) ) expect(counts.rehomeTargetRowHolds).toBe(1) expect(counts.cellInventoryHoldMaxSite).toBe('rehome-target-row') expect(counts.rehomeTargetRowHoldMsMax).toBeGreaterThanOrEqual(STATEMENT_DELAY_MS) - expect(counts.rehomeTargetRowHoldMsMax).toBeLessThanOrEqual(2 * STATEMENT_DELAY_MS) + // The COMMIT observer probes run under the lock, but are absent in production. + expect(counts.rehomeTargetRowHoldMsMax - commitProbeMs).toBeLessThanOrEqual( + 2 * STATEMENT_DELAY_MS + ) expect(await reservedRequests(context.target.id)).toBe(context.targetReservedBefore + 2) }) diff --git a/cloud/package.json b/cloud/package.json index 1770e91a7b11..e2c02ba24160 100644 --- a/cloud/package.json +++ b/cloud/package.json @@ -2,7 +2,7 @@ "name": "orca-cloud", "private": true, "version": "0.0.0", - "packageManager": "pnpm@10.24.0", + "packageManager": "pnpm@10.34.6", "engines": { "node": ">=24 <27", "pnpm": ">=10" @@ -26,7 +26,7 @@ "typecheck": "pnpm -r typecheck" }, "devDependencies": { - "@types/node": "^24.10.0", + "@types/node": "^24.19.0", "tsx": "^4.23.15", "typescript": "^5.9.3", "vitest": "^4.1.11" diff --git a/cloud/packages/postgres-schema/package.json b/cloud/packages/postgres-schema/package.json index 05cd4bd72215..594c799a5b5b 100644 --- a/cloud/packages/postgres-schema/package.json +++ b/cloud/packages/postgres-schema/package.json @@ -13,7 +13,7 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "devDependencies": { - "@types/node": "^24.10.0", + "@types/node": "^24.19.0", "typescript": "^5.9.3", "vitest": "^4.1.11" } diff --git a/cloud/packages/push-contract/package.json b/cloud/packages/push-contract/package.json index 75698aa46f95..19f4941ba4a0 100644 --- a/cloud/packages/push-contract/package.json +++ b/cloud/packages/push-contract/package.json @@ -16,7 +16,7 @@ "zod": "^3.25.76" }, "devDependencies": { - "@types/node": "^24.10.0", + "@types/node": "^24.19.0", "typescript": "^5.9.3", "vitest": "^4.1.11" } diff --git a/cloud/packages/relay-contract/package.json b/cloud/packages/relay-contract/package.json index 976840da87df..5aa329a3d81a 100644 --- a/cloud/packages/relay-contract/package.json +++ b/cloud/packages/relay-contract/package.json @@ -16,7 +16,7 @@ "zod": "^3.25.76" }, "devDependencies": { - "@types/node": "^24.10.0", + "@types/node": "^24.19.0", "typescript": "^5.9.3", "vitest": "^4.1.11" } diff --git a/cloud/pnpm-lock.yaml b/cloud/pnpm-lock.yaml index 6042ee85cd45..c688889cb50c 100644 --- a/cloud/pnpm-lock.yaml +++ b/cloud/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: devDependencies: '@types/node': - specifier: ^24.10.0 - version: 24.13.2 + specifier: ^24.19.0 + version: 24.19.0 tsx: specifier: ^4.23.15 version: 4.23.15 @@ -19,13 +19,13 @@ importers: version: 5.9.3 vitest: specifier: ^4.1.11 - version: 4.1.11(@types/node@24.13.2)(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.2)(tsx@4.23.15)) + version: 4.1.11(@types/node@24.19.0)(vite@8.0.16(@types/node@24.19.0)(esbuild@0.28.2)(tsx@4.23.15)) apps/push: dependencies: '@hono/node-server': - specifier: ^1.19.17 - version: 1.19.17(hono@4.13.7) + specifier: ^2.1.2 + version: 2.1.2(hono@4.13.10) '@orca-cloud/postgres-schema': specifier: workspace:* version: link:../../packages/postgres-schema @@ -36,8 +36,8 @@ importers: specifier: ^10.5.0 version: 10.9.1 hono: - specifier: ^4.13.7 - version: 4.13.7 + specifier: ^4.13.10 + version: 4.13.10 pg: specifier: ^8.22.0 version: 8.22.0 @@ -52,26 +52,26 @@ importers: version: 3.25.76 devDependencies: '@types/node': - specifier: ^24.10.0 - version: 24.13.2 + specifier: ^24.19.0 + version: 24.19.0 '@types/pg': specifier: ^8.20.0 version: 8.20.0 tsx: - specifier: ^4.21.0 - version: 4.22.4 + specifier: ^4.23.15 + version: 4.23.15 typescript: specifier: ^5.9.3 version: 5.9.3 vitest: specifier: ^4.1.11 - version: 4.1.11(@types/node@24.13.2)(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.2)(tsx@4.22.4)) + version: 4.1.11(@types/node@24.19.0)(vite@8.0.16(@types/node@24.19.0)(esbuild@0.28.2)(tsx@4.23.15)) apps/relay: dependencies: '@hono/node-server': - specifier: ^1.19.17 - version: 1.19.17(hono@4.13.7) + specifier: ^2.1.2 + version: 2.1.2(hono@4.13.10) '@orca-cloud/postgres-schema': specifier: workspace:* version: link:../../packages/postgres-schema @@ -79,11 +79,11 @@ importers: specifier: workspace:* version: link:../../packages/relay-contract hono: - specifier: ^4.13.7 - version: 4.13.7 + specifier: ^4.13.10 + version: 4.13.10 jose: - specifier: ^6.1.3 - version: 6.2.3 + specifier: ^6.2.12 + version: 6.2.12 pg: specifier: ^8.22.0 version: 8.22.0 @@ -91,15 +91,15 @@ importers: specifier: ^1.0.3 version: 1.0.3 ws: - specifier: ^8.21.3 - version: 8.21.3 + specifier: ^8.22.0 + version: 8.22.0 zod: specifier: ^3.25.76 version: 3.25.76 devDependencies: '@types/node': - specifier: ^24.10.0 - version: 24.13.2 + specifier: ^24.19.0 + version: 24.19.0 '@types/pg': specifier: ^8.20.0 version: 8.20.0 @@ -107,76 +107,76 @@ importers: specifier: ^8.18.1 version: 8.18.1 tsx: - specifier: ^4.21.0 - version: 4.22.4 + specifier: ^4.23.15 + version: 4.23.15 typescript: specifier: ^5.9.3 version: 5.9.3 vitest: specifier: ^4.1.11 - version: 4.1.11(@types/node@24.13.2)(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.2)(tsx@4.22.4)) + version: 4.1.11(@types/node@24.19.0)(vite@8.0.16(@types/node@24.19.0)(esbuild@0.28.2)(tsx@4.23.15)) apps/relay-fence-broker: dependencies: '@hono/node-server': - specifier: ^1.19.17 - version: 1.19.17(hono@4.13.7) + specifier: ^2.1.2 + version: 2.1.2(hono@4.13.10) hono: - specifier: ^4.13.7 - version: 4.13.7 + specifier: ^4.13.10 + version: 4.13.10 zod: specifier: ^3.25.76 version: 3.25.76 devDependencies: '@types/node': - specifier: ^24.10.0 - version: 24.13.2 + specifier: ^24.19.0 + version: 24.19.0 tsx: - specifier: ^4.21.0 - version: 4.22.4 + specifier: ^4.23.15 + version: 4.23.15 typescript: specifier: ^5.9.3 version: 5.9.3 vitest: specifier: ^4.1.11 - version: 4.1.11(@types/node@24.13.2)(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.2)(tsx@4.22.4)) + version: 4.1.11(@types/node@24.19.0)(vite@8.0.16(@types/node@24.19.0)(esbuild@0.28.2)(tsx@4.23.15)) apps/relay-ops: dependencies: '@hono/node-server': - specifier: ^1.19.17 - version: 1.19.17(hono@4.13.7) + specifier: ^2.1.2 + version: 2.1.2(hono@4.13.10) hono: - specifier: ^4.13.7 - version: 4.13.7 + specifier: ^4.13.10 + version: 4.13.10 zod: specifier: ^3.25.76 version: 3.25.76 devDependencies: '@types/node': - specifier: ^24.10.0 - version: 24.13.2 + specifier: ^24.19.0 + version: 24.19.0 tsx: - specifier: ^4.21.0 - version: 4.22.4 + specifier: ^4.23.15 + version: 4.23.15 typescript: specifier: ^5.9.3 version: 5.9.3 vitest: specifier: ^4.1.11 - version: 4.1.11(@types/node@24.13.2)(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.2)(tsx@4.22.4)) + version: 4.1.11(@types/node@24.19.0)(vite@8.0.16(@types/node@24.19.0)(esbuild@0.28.2)(tsx@4.23.15)) packages/postgres-schema: devDependencies: '@types/node': - specifier: ^24.10.0 - version: 24.13.2 + specifier: ^24.19.0 + version: 24.19.0 typescript: specifier: ^5.9.3 version: 5.9.3 vitest: specifier: ^4.1.11 - version: 4.1.11(@types/node@24.13.2)(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.2)(tsx@4.23.15)) + version: 4.1.11(@types/node@24.19.0)(vite@8.0.16(@types/node@24.19.0)(esbuild@0.28.2)(tsx@4.23.15)) packages/push-contract: dependencies: @@ -185,14 +185,14 @@ importers: version: 3.25.76 devDependencies: '@types/node': - specifier: ^24.10.0 - version: 24.13.2 + specifier: ^24.19.0 + version: 24.19.0 typescript: specifier: ^5.9.3 version: 5.9.3 vitest: specifier: ^4.1.11 - version: 4.1.11(@types/node@24.13.2)(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.2)(tsx@4.23.15)) + version: 4.1.11(@types/node@24.19.0)(vite@8.0.16(@types/node@24.19.0)(esbuild@0.28.2)(tsx@4.23.15)) packages/relay-contract: dependencies: @@ -201,14 +201,14 @@ importers: version: 3.25.76 devDependencies: '@types/node': - specifier: ^24.10.0 - version: 24.13.2 + specifier: ^24.19.0 + version: 24.19.0 typescript: specifier: ^5.9.3 version: 5.9.3 vitest: specifier: ^4.1.11 - version: 4.1.11(@types/node@24.13.2)(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.2)(tsx@4.23.15)) + version: 4.1.11(@types/node@24.19.0)(vite@8.0.16(@types/node@24.19.0)(esbuild@0.28.2)(tsx@4.23.15)) packages: @@ -221,321 +221,165 @@ packages: '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - '@esbuild/aix-ppc64@0.28.1': - resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - '@esbuild/aix-ppc64@0.28.2': resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.28.1': - resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm64@0.28.2': resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.28.1': - resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - '@esbuild/android-arm@0.28.2': resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.28.1': - resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - '@esbuild/android-x64@0.28.2': resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.28.1': - resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-arm64@0.28.2': resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.28.1': - resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - '@esbuild/darwin-x64@0.28.2': resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.28.1': - resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-arm64@0.28.2': resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.28.1': - resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - '@esbuild/freebsd-x64@0.28.2': resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.28.1': - resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm64@0.28.2': resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.28.1': - resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - '@esbuild/linux-arm@0.28.2': resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.28.1': - resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-ia32@0.28.2': resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.28.1': - resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-loong64@0.28.2': resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.28.1': - resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-mips64el@0.28.2': resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.28.1': - resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-ppc64@0.28.2': resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.28.1': - resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-riscv64@0.28.2': resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.28.1': - resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-s390x@0.28.2': resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.28.1': - resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - '@esbuild/linux-x64@0.28.2': resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.28.1': - resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - '@esbuild/netbsd-arm64@0.28.2': resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.28.1': - resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - '@esbuild/netbsd-x64@0.28.2': resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.28.1': - resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - '@esbuild/openbsd-arm64@0.28.2': resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.28.1': - resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - '@esbuild/openbsd-x64@0.28.2': resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.28.1': - resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - '@esbuild/openharmony-arm64@0.28.2': resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.28.1': - resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - '@esbuild/sunos-x64@0.28.2': resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.28.1': - resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-arm64@0.28.2': resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.28.1': - resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-ia32@0.28.2': resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.28.1': - resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - '@esbuild/win32-x64@0.28.2': resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} engines: {node: '>=18'} cpu: [x64] os: [win32] - '@hono/node-server@1.19.17': - resolution: {integrity: sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ==} - engines: {node: '>=18.14.1'} + '@hono/node-server@2.1.2': + resolution: {integrity: sha512-w0tqJ/ilmggKokOSvbp9y+F91Y4ROTyCTL/8EKj9B4qOhOZzNm/tyKElzxrz+9+4fJKQrBSaahIuCGRye5RJYg==} + engines: {node: '>=20'} peerDependencies: hono: ^4 @@ -587,36 +431,42 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.0.3': resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@rolldown/binding-linux-ppc64-gnu@1.0.3': resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.0.3': resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.0.3': resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-musl@1.0.3': resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@rolldown/binding-openharmony-arm64@1.0.3': resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} @@ -659,8 +509,8 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - '@types/node@24.13.2': - resolution: {integrity: sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==} + '@types/node@24.19.0': + resolution: {integrity: sha512-zY+5tKxXdhGh1PYI0ac+7juvEu4OI6vWtVVoj5i2m42jxAY1U+zHGt6QCyOFwykdP62sM3MJ9stoYYUw5aCWew==} '@types/pg@8.20.0': resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} @@ -744,11 +594,6 @@ packages: es-module-lexer@2.1.0: resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} - esbuild@0.28.1: - resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} - engines: {node: '>=18'} - hasBin: true - esbuild@0.28.2: resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} engines: {node: '>=18'} @@ -802,16 +647,16 @@ packages: resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} engines: {node: '>=14'} - hono@4.13.7: - resolution: {integrity: sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ==} + hono@4.13.10: + resolution: {integrity: sha512-dQuLsa5oO+47QVMVMaaD9cIv8ctmVtK1iRvwWngkfloFJMeFeuoUFDswIqZGxmGX3hrRzREgEArjkK9OgsQEhA==} engines: {node: '>=16.9.0'} https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} - jose@6.2.3: - resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + jose@6.2.12: + resolution: {integrity: sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==} json-bigint@1.0.0: resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} @@ -857,24 +702,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.33.0: resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.33.0: resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.33.0: resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.33.0: resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} @@ -1027,11 +876,6 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsx@4.22.4: - resolution: {integrity: sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==} - engines: {node: '>=18.0.0'} - hasBin: true - tsx@4.23.15: resolution: {integrity: sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==} engines: {node: '>=18.0.0'} @@ -1045,8 +889,8 @@ packages: engines: {node: '>=14.17'} hasBin: true - undici-types@7.18.2: - resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} vite@8.0.16: resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} @@ -1141,8 +985,8 @@ packages: engines: {node: '>=8'} hasBin: true - ws@8.21.3: - resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + ws@8.22.0: + resolution: {integrity: sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -1178,165 +1022,87 @@ snapshots: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.28.1': - optional: true - '@esbuild/aix-ppc64@0.28.2': optional: true - '@esbuild/android-arm64@0.28.1': - optional: true - '@esbuild/android-arm64@0.28.2': optional: true - '@esbuild/android-arm@0.28.1': - optional: true - '@esbuild/android-arm@0.28.2': optional: true - '@esbuild/android-x64@0.28.1': - optional: true - '@esbuild/android-x64@0.28.2': optional: true - '@esbuild/darwin-arm64@0.28.1': - optional: true - '@esbuild/darwin-arm64@0.28.2': optional: true - '@esbuild/darwin-x64@0.28.1': - optional: true - '@esbuild/darwin-x64@0.28.2': optional: true - '@esbuild/freebsd-arm64@0.28.1': - optional: true - '@esbuild/freebsd-arm64@0.28.2': optional: true - '@esbuild/freebsd-x64@0.28.1': - optional: true - '@esbuild/freebsd-x64@0.28.2': optional: true - '@esbuild/linux-arm64@0.28.1': - optional: true - '@esbuild/linux-arm64@0.28.2': optional: true - '@esbuild/linux-arm@0.28.1': - optional: true - '@esbuild/linux-arm@0.28.2': optional: true - '@esbuild/linux-ia32@0.28.1': - optional: true - '@esbuild/linux-ia32@0.28.2': optional: true - '@esbuild/linux-loong64@0.28.1': - optional: true - '@esbuild/linux-loong64@0.28.2': optional: true - '@esbuild/linux-mips64el@0.28.1': - optional: true - '@esbuild/linux-mips64el@0.28.2': optional: true - '@esbuild/linux-ppc64@0.28.1': - optional: true - '@esbuild/linux-ppc64@0.28.2': optional: true - '@esbuild/linux-riscv64@0.28.1': - optional: true - '@esbuild/linux-riscv64@0.28.2': optional: true - '@esbuild/linux-s390x@0.28.1': - optional: true - '@esbuild/linux-s390x@0.28.2': optional: true - '@esbuild/linux-x64@0.28.1': - optional: true - '@esbuild/linux-x64@0.28.2': optional: true - '@esbuild/netbsd-arm64@0.28.1': - optional: true - '@esbuild/netbsd-arm64@0.28.2': optional: true - '@esbuild/netbsd-x64@0.28.1': - optional: true - '@esbuild/netbsd-x64@0.28.2': optional: true - '@esbuild/openbsd-arm64@0.28.1': - optional: true - '@esbuild/openbsd-arm64@0.28.2': optional: true - '@esbuild/openbsd-x64@0.28.1': - optional: true - '@esbuild/openbsd-x64@0.28.2': optional: true - '@esbuild/openharmony-arm64@0.28.1': - optional: true - '@esbuild/openharmony-arm64@0.28.2': optional: true - '@esbuild/sunos-x64@0.28.1': - optional: true - '@esbuild/sunos-x64@0.28.2': optional: true - '@esbuild/win32-arm64@0.28.1': - optional: true - '@esbuild/win32-arm64@0.28.2': optional: true - '@esbuild/win32-ia32@0.28.1': - optional: true - '@esbuild/win32-ia32@0.28.2': optional: true - '@esbuild/win32-x64@0.28.1': - optional: true - '@esbuild/win32-x64@0.28.2': optional: true - '@hono/node-server@1.19.17(hono@4.13.7)': + '@hono/node-server@2.1.2(hono@4.13.10)': dependencies: - hono: 4.13.7 + hono: 4.13.10 '@jridgewell/sourcemap-codec@1.5.5': {} @@ -1416,19 +1182,19 @@ snapshots: '@types/estree@1.0.9': {} - '@types/node@24.13.2': + '@types/node@24.19.0': dependencies: - undici-types: 7.18.2 + undici-types: 7.24.6 '@types/pg@8.20.0': dependencies: - '@types/node': 24.13.2 + '@types/node': 24.19.0 pg-protocol: 1.15.0 pg-types: 2.2.0 '@types/ws@8.18.1': dependencies: - '@types/node': 24.13.2 + '@types/node': 24.19.0 '@vitest/expect@4.1.11': dependencies: @@ -1439,21 +1205,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.11(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.2)(tsx@4.22.4))': + '@vitest/mocker@4.1.11(vite@8.0.16(@types/node@24.19.0)(esbuild@0.28.2)(tsx@4.23.15))': dependencies: '@vitest/spy': 4.1.11 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.16(@types/node@24.13.2)(esbuild@0.28.2)(tsx@4.22.4) - - '@vitest/mocker@4.1.11(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.2)(tsx@4.23.15))': - dependencies: - '@vitest/spy': 4.1.11 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 8.0.16(@types/node@24.13.2)(esbuild@0.28.2)(tsx@4.23.15) + vite: 8.0.16(@types/node@24.19.0)(esbuild@0.28.2)(tsx@4.23.15) '@vitest/pretty-format@4.1.11': dependencies: @@ -1507,35 +1265,6 @@ snapshots: es-module-lexer@2.1.0: {} - esbuild@0.28.1: - optionalDependencies: - '@esbuild/aix-ppc64': 0.28.1 - '@esbuild/android-arm': 0.28.1 - '@esbuild/android-arm64': 0.28.1 - '@esbuild/android-x64': 0.28.1 - '@esbuild/darwin-arm64': 0.28.1 - '@esbuild/darwin-x64': 0.28.1 - '@esbuild/freebsd-arm64': 0.28.1 - '@esbuild/freebsd-x64': 0.28.1 - '@esbuild/linux-arm': 0.28.1 - '@esbuild/linux-arm64': 0.28.1 - '@esbuild/linux-ia32': 0.28.1 - '@esbuild/linux-loong64': 0.28.1 - '@esbuild/linux-mips64el': 0.28.1 - '@esbuild/linux-ppc64': 0.28.1 - '@esbuild/linux-riscv64': 0.28.1 - '@esbuild/linux-s390x': 0.28.1 - '@esbuild/linux-x64': 0.28.1 - '@esbuild/netbsd-arm64': 0.28.1 - '@esbuild/netbsd-x64': 0.28.1 - '@esbuild/openbsd-arm64': 0.28.1 - '@esbuild/openbsd-x64': 0.28.1 - '@esbuild/openharmony-arm64': 0.28.1 - '@esbuild/sunos-x64': 0.28.1 - '@esbuild/win32-arm64': 0.28.1 - '@esbuild/win32-ia32': 0.28.1 - '@esbuild/win32-x64': 0.28.1 - esbuild@0.28.2: optionalDependencies: '@esbuild/aix-ppc64': 0.28.2 @@ -1618,7 +1347,7 @@ snapshots: google-logging-utils@1.1.3: {} - hono@4.13.7: {} + hono@4.13.10: {} https-proxy-agent@7.0.6: dependencies: @@ -1627,7 +1356,7 @@ snapshots: transitivePeerDependencies: - supports-color - jose@6.2.3: {} + jose@6.2.12: {} json-bigint@1.0.0: dependencies: @@ -1817,12 +1546,6 @@ snapshots: tslib@2.8.1: optional: true - tsx@4.22.4: - dependencies: - esbuild: 0.28.1 - optionalDependencies: - fsevents: 2.3.3 - tsx@4.23.15: dependencies: esbuild: 0.28.2 @@ -1833,22 +1556,9 @@ snapshots: typescript@5.9.3: {} - undici-types@7.18.2: {} - - vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.2)(tsx@4.22.4): - dependencies: - lightningcss: 1.33.0 - picomatch: 4.0.7 - postcss: 8.5.28 - rolldown: 1.0.3 - tinyglobby: 0.2.17 - optionalDependencies: - '@types/node': 24.13.2 - esbuild: 0.28.2 - fsevents: 2.3.3 - tsx: 4.22.4 + undici-types@7.24.6: {} - vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.2)(tsx@4.23.15): + vite@8.0.16(@types/node@24.19.0)(esbuild@0.28.2)(tsx@4.23.15): dependencies: lightningcss: 1.33.0 picomatch: 4.0.7 @@ -1856,42 +1566,15 @@ snapshots: rolldown: 1.0.3 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 24.13.2 + '@types/node': 24.19.0 esbuild: 0.28.2 fsevents: 2.3.3 tsx: 4.23.15 - vitest@4.1.11(@types/node@24.13.2)(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.2)(tsx@4.22.4)): - dependencies: - '@vitest/expect': 4.1.11 - '@vitest/mocker': 4.1.11(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.2)(tsx@4.22.4)) - '@vitest/pretty-format': 4.1.11 - '@vitest/runner': 4.1.11 - '@vitest/snapshot': 4.1.11 - '@vitest/spy': 4.1.11 - '@vitest/utils': 4.1.11 - es-module-lexer: 2.1.0 - expect-type: 1.3.0 - magic-string: 0.30.21 - obug: 2.1.3 - pathe: 2.0.3 - picomatch: 4.0.4 - std-env: 4.1.0 - tinybench: 2.9.0 - tinyexec: 1.2.4 - tinyglobby: 0.2.17 - tinyrainbow: 3.1.0 - vite: 8.0.16(@types/node@24.13.2)(esbuild@0.28.2)(tsx@4.22.4) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 24.13.2 - transitivePeerDependencies: - - msw - - vitest@4.1.11(@types/node@24.13.2)(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.2)(tsx@4.23.15)): + vitest@4.1.11(@types/node@24.19.0)(vite@8.0.16(@types/node@24.19.0)(esbuild@0.28.2)(tsx@4.23.15)): dependencies: '@vitest/expect': 4.1.11 - '@vitest/mocker': 4.1.11(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.2)(tsx@4.23.15)) + '@vitest/mocker': 4.1.11(vite@8.0.16(@types/node@24.19.0)(esbuild@0.28.2)(tsx@4.23.15)) '@vitest/pretty-format': 4.1.11 '@vitest/runner': 4.1.11 '@vitest/snapshot': 4.1.11 @@ -1908,10 +1591,10 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.0.16(@types/node@24.13.2)(esbuild@0.28.2)(tsx@4.23.15) + vite: 8.0.16(@types/node@24.19.0)(esbuild@0.28.2)(tsx@4.23.15) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 24.13.2 + '@types/node': 24.19.0 transitivePeerDependencies: - msw @@ -1922,7 +1605,7 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 - ws@8.21.3: {} + ws@8.22.0: {} xtend@4.0.2: {} diff --git a/cloud/pnpm-workspace.yaml b/cloud/pnpm-workspace.yaml index cc61e60464cb..97bf7cd8c2ef 100644 --- a/cloud/pnpm-workspace.yaml +++ b/cloud/pnpm-workspace.yaml @@ -2,3 +2,4 @@ packages: - apps/* - packages/* +minimumReleaseAge: 4320 diff --git a/config/build-plugins/jsonc-parser-esm.ts b/config/build-plugins/jsonc-parser-esm.ts new file mode 100644 index 000000000000..f3403bc9463a --- /dev/null +++ b/config/build-plugins/jsonc-parser-esm.ts @@ -0,0 +1,6 @@ +import { resolve } from 'node:path' + +// UMD relative requires cannot survive a self-contained bundle. +export const JSONC_PARSER_ESM_ALIAS = { + 'jsonc-parser': resolve(import.meta.dirname, '../../node_modules/jsonc-parser/lib/esm/main.js') +} diff --git a/config/build-plugins/markdown-parser-exports.ts b/config/build-plugins/markdown-parser-exports.ts new file mode 100644 index 000000000000..2b468c0b02fe --- /dev/null +++ b/config/build-plugins/markdown-parser-exports.ts @@ -0,0 +1,11 @@ +import { createRequire } from 'node:module' + +const require = createRequire(import.meta.url) +const parserRequire = createRequire(require.resolve('remark-parse')) +const mathRequire = createRequire(require.resolve('rehype-katex')) + +// Both preview paths use DOM-free parsing so their dependencies also run in workers. +export const markdownParserAliases = { + 'decode-named-character-reference': parserRequire.resolve('decode-named-character-reference'), + 'hast-util-from-html-isomorphic': mathRequire.resolve('hast-util-from-html-isomorphic') +} diff --git a/config/build-plugins/plain-node-entry-guard.ts b/config/build-plugins/plain-node-entry-guard.ts index 31340869e22c..5ea771e293c0 100644 --- a/config/build-plugins/plain-node-entry-guard.ts +++ b/config/build-plugins/plain-node-entry-guard.ts @@ -19,6 +19,7 @@ type OutputChunk = Rollup.OutputChunk // The CLI loads these paths after electron-vite replaces out/main. export const CLI_MAIN_ENTRY_NAMES = [ 'agent-hooks/managed-agent-hook-controls', + 'gitlab/project-ref-parser', 'orca-profiles/profile-index-store', 'claude-accounts/keychain', ...[ @@ -55,9 +56,7 @@ const PLAIN_NODE_ENTRY_NAMES = [ const WORKER_THREAD_ENTRY_NAMES = [ 'stt-worker', 'warp-theme-parser-worker', - 'cursor-desktop-profile-worker-entry', - 'session-scanner-opencode-sqlite-worker-entry', - 'session-scanner-worker-entry', + 'foreign-sqlite-reader-entry', 'main-thread-hang-watchdog-entry', 'port-scan-command-worker-entry', 'usage-scan-worker-entry', diff --git a/config/ci/windows-ssh-provider/preview-ssh/prove-preview-openssh.ps1 b/config/ci/windows-ssh-provider/preview-ssh/prove-preview-openssh.ps1 index 115345cf8a05..10b4d9d67e16 100644 --- a/config/ci/windows-ssh-provider/preview-ssh/prove-preview-openssh.ps1 +++ b/config/ci/windows-ssh-provider/preview-ssh/prove-preview-openssh.ps1 @@ -2,46 +2,34 @@ # -HiddenTools: the private accounts are denied every machine PATH directory holding one of these # executables, and their own PATH carries logging shims for them, so SSH sessions have no host toolchain. # -HostCellProbe receives a context hashtable (accounts, port, keys, shim log) once provisioning passes. -param([Parameter(Mandatory=$true)][string]$Receipt,[string]$Archive,[Parameter(Mandatory=$true)][ValidateSet('arm64','x64')][string]$Arch,[ValidateSet('preview','inbox')][string]$Server='preview',[scriptblock]$ProductionRouteProbe,[ValidateRange(1,4)][int]$Accounts=1,[string[]]$HiddenTools=@(),[scriptblock]$HostCellProbe) +param([Parameter(Mandatory=$true)][string]$Receipt,[string]$Archive,[Parameter(Mandatory=$true)][ValidateSet('arm64','x64')][string]$Arch,[ValidateSet('preview','inbox')][string]$Server='preview',[scriptblock]$ProductionRouteProbe,[ValidateRange(1,4)][int]$Accounts=1,[string[]]$HiddenTools=@(),[scriptblock]$HostCellProbe,[string]$InboxPreparationReceipt) $ErrorActionPreference = 'Stop' +. (Join-Path $PSScriptRoot 'windows-ssh-capability.ps1') $target=@{arm64=@{os='Arm64';folder='OpenSSH-ARM64';machine='0xAA64';archive='698c6aec31c1dd0fb996206e8741f4531a97355686b5431ef347d531b07fcd42'};x64=@{os='X64';folder='OpenSSH-Win64';machine='0x8664';archive='23f50f3458c4c5d0b12217c6a5ddfde0137210a30fa870e98b29827f7b43aba5'}}[$Arch] $scopeServer=if($Server -eq 'inbox'){'Windows inbox OpenSSH.Server capability binaries'}else{'Microsoft Win32-OpenSSH 10.0.0.0p2-Preview'} $report = @{scope="$scopeServer $Arch private loopback authentication and stock cmd.exe dispatch"; server=$Server; status='running'; imageVersion=$env:ImageVersion; cleanup=@('not-confirmed'); globalBootstrapCleanup='Not qualified: service bootstrap may create ProgramData SSH and OpenSSH registry entries; disposable CI VM destruction is the boundary'; observations=@(); stages=@(); diagnosticCaptureFailures=@()} $script:receiptWritten=$false function Write-Stage([string]$Stage) { - $timestamp=[DateTime]::UtcNow.ToString('o') - $report.stages += @{stage=$Stage; utc=$timestamp} - try { - $bytes=[Text.UTF8Encoding]::new($false).GetBytes(($report | ConvertTo-Json -Depth 6)) - $temporary="$Receipt.pending" - $stream=[IO.FileStream]::new($temporary,[IO.FileMode]::Create,[IO.FileAccess]::Write,[IO.FileShare]::Read) - try {$stream.Write($bytes,0,$bytes.Length);$stream.Flush($true)} finally {$stream.Dispose()} - [IO.File]::Move($temporary,$Receipt,$true) - $script:receiptWritten=$true - } catch {Write-Warning 'Progress receipt could not be updated; cleanup must still run'} - Write-Host "Native SSH stage: $Stage ($timestamp)" + if(Write-WindowsSshReceiptStage $report $Receipt $Stage){$script:receiptWritten=$true} } Write-Stage 'preflight-start' if(-not $script:receiptWritten){throw 'Initial progress receipt unavailable; refuse provisioning'} -if ($env:GITHUB_ACTIONS -ne 'true' -or $env:ORCA_ISOLATED_SSH_CI -ne '1' -or [Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() -ne $target.os) { throw "Requires isolated native $Arch GitHub runner" } -$admin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) -if (-not $admin) { throw 'Administrative private service/account setup required' } +Assert-IsolatedWindowsSshCi $Arch Write-Stage 'existing-server-query-start' $inboxDir=Join-Path $env:WINDIR 'System32\OpenSSH' -function Assert-GlobalServerDormant { - $global=Get-CimInstance Win32_Service -Filter "Name='sshd'" - if(-not $global){return} - # Inbox mode may register the global service; it must stay stopped and never be started here. - if($Server -ne 'inbox' -or $global.State -ne 'Stopped' -or $global.PathName.Trim('"') -ne (Join-Path $inboxDir 'sshd.exe')){throw 'Refuse an existing global SSH server'} -} -Assert-GlobalServerDormant +Assert-WindowsSshGlobalServerDormant $Server Write-Stage 'existing-server-query-complete' Write-Stage 'default-shell-query-start' $openSshKey = 'HKLM:\SOFTWARE\OpenSSH' -$registry = Get-ItemProperty -LiteralPath $openSshKey -ErrorAction SilentlyContinue # Host-cell probes may set DefaultShell per cell; cleanup restores this stock state. -if ($registry.DefaultShell -or $registry.DefaultShellCommandOption) { throw 'Requires stock cmd.exe OpenSSH shell at start' } +Assert-WindowsSshStockShell Write-Stage 'default-shell-query-complete' +if($InboxPreparationReceipt){ + if($Server -ne 'inbox'){throw 'Inbox preparation receipt cannot qualify a preview server'} + $preparation=Get-Content -LiteralPath $InboxPreparationReceipt -Raw | ConvertFrom-Json + if($preparation.status -ne 'passed' -or $preparation.arch -ne $Arch -or $preparation.sourceSha -ne $env:GITHUB_SHA -or $preparation.runId -ne $env:GITHUB_RUN_ID -or $preparation.runAttempt -ne $env:GITHUB_RUN_ATTEMPT -or $preparation.runnerName -ne $env:RUNNER_NAME -or $preparation.imageVersion -ne $env:ImageVersion){throw 'Inbox preparation receipt identity or verdict mismatch'} + $report.inboxCapabilityPreparation=$preparation +} $id = [Guid]::NewGuid().ToString('N').Substring(0,10) $name = "orca$id" $accountNames = @($name) + @(if($Accounts -gt 1){2..$Accounts | ForEach-Object {"$name$_"}}) @@ -180,12 +168,7 @@ try { $report.nativeInputs=$verified Write-Stage 'preview-native-input-verification-complete' } else { - Write-Stage 'inbox-capability-start' - $capability=Get-WindowsCapability -Online -Name 'OpenSSH.Server~~~~0.0.1.0' - $report.inboxCapabilityInitialState=[string]$capability.State - if($capability.State -ne 'Installed'){Add-WindowsCapability -Online -Name 'OpenSSH.Server~~~~0.0.1.0' | Out-Null} - Assert-GlobalServerDormant - Write-Stage 'inbox-capability-complete' + Install-WindowsInboxSshCapability $Arch $report {param($stage) Write-Stage $stage} $verified=@() foreach($binary in @('sshd.exe','ssh.exe','ssh-keygen.exe','sftp.exe','sftp-server.exe')){ $path=Join-Path $sshDir $binary @@ -431,21 +414,38 @@ LogLevel DEBUG1 foreach($directory in $deniedToolDirs){Invoke-Bounded icacls.exe (@($directory.TrimEnd('\'),'/remove:d')+@($ownedAccounts | ForEach-Object {"*$($_.sid)"})) 120 | Out-Null} Write-Stage 'cleanup-toolchain-acl-complete' Write-Stage 'cleanup-user-profile-start' + $profileTargets=@(foreach($account in $ownedAccounts){ + if($account.sid){@{sid=$account.sid;watch=$null;done=$false;profiles=@();waitMs=0;disposition=$null}} + }) $report.profileCleanup=@() - foreach($account in $ownedAccounts){ - if(-not $account.sid){continue} - $profileWait=[Diagnostics.Stopwatch]::StartNew() - do { - $profiles=@(Get-CimInstance Win32_UserProfile | Where-Object SID -eq $account.sid) - if(-not @($profiles | Where-Object Loaded).Count){break} - Start-Sleep -Milliseconds 500 - } while($profileWait.Elapsed.TotalSeconds -lt 30) - $report.profileUnloadWaitMs=$profileWait.ElapsedMilliseconds - $report.privateProfile=@($profiles | ForEach-Object {@{loaded=$_.Loaded;status=$_.Status}}) - Write-Stage 'cleanup-user-profile-observed' - $loadedProfiles=@($profiles | Where-Object Loaded) - $report.profileCleanup+=if($loadedProfiles.Count){'Loaded profile retained for disposable CI VM destruction'}else{'Unloaded profile removed'} - $profiles | Where-Object {-not $_.Loaded} | Remove-CimInstance + $report.privateProfiles=@() + do { + foreach($entry in @($profileTargets | Where-Object {-not $_.done})){ + if(-not $entry.watch){$entry.watch=[Diagnostics.Stopwatch]::StartNew()} + $profiles=@(Get-CimInstance Win32_UserProfile -Filter "SID='$($entry.sid)'") + if(@($profiles | Where-Object SID -ne $entry.sid).Count){throw 'Private profile query returned an unrelated SID'} + if(@($profiles | Where-Object Loaded).Count -and $entry.watch.ElapsedMilliseconds -lt 30000){continue} + # A profile may reload after the polling snapshot. + $profiles=@(Get-CimInstance Win32_UserProfile -Filter "SID='$($entry.sid)'") + if(@($profiles | Where-Object SID -ne $entry.sid).Count){throw 'Private profile query returned an unrelated SID'} + $loadedProfiles=@($profiles | Where-Object Loaded) + if($loadedProfiles.Count -and $entry.watch.ElapsedMilliseconds -lt 30000){continue} + $profiles | Where-Object {-not $_.Loaded} | Remove-CimInstance + $entry.profiles=@($profiles | ForEach-Object {@{loaded=$_.Loaded;status=$_.Status}}) + $entry.waitMs=$entry.watch.ElapsedMilliseconds + $entry.disposition=if($loadedProfiles.Count){'Loaded profile retained for disposable CI VM destruction'}else{'Unloaded profile removed'} + $entry.done=$true + $report.profileCleanup=@($profileTargets | Where-Object done | ForEach-Object disposition) + $report.privateProfiles=@($profileTargets | Where-Object done | ForEach-Object {@{sid=$_.sid;waitMs=$_.waitMs;profiles=$_.profiles;disposition=$_.disposition}}) + $report.profileUnloadWaitMs=$entry.waitMs + $report.privateProfile=$entry.profiles + Write-Stage 'cleanup-user-profile-observed' + } + if(@($profileTargets | Where-Object {-not $_.done}).Count){Start-Sleep -Milliseconds 500} + } while(@($profileTargets | Where-Object {-not $_.done}).Count) + if($profileTargets.Count){ + $report.profileUnloadWaitMs=$profileTargets[-1].waitMs + $report.privateProfile=$profileTargets[-1].profiles } Write-Stage 'cleanup-user-profile-complete' Write-Stage 'cleanup-user-start' diff --git a/config/ci/windows-ssh-provider/preview-ssh/test-preview-diagnostics.ps1 b/config/ci/windows-ssh-provider/preview-ssh/test-preview-diagnostics.ps1 index 0304ac7a2755..5bd3ccf30cdc 100644 --- a/config/ci/windows-ssh-provider/preview-ssh/test-preview-diagnostics.ps1 +++ b/config/ci/windows-ssh-provider/preview-ssh/test-preview-diagnostics.ps1 @@ -33,5 +33,166 @@ try { if(($result.hidden -join '|') -ne "$nodeDir|$gccDir"){throw 'Toolchain PATH entries not hidden'} if(($result.kept -join '|') -ne "$plainDir|$(Join-Path $pathRoot 'missing')|Q:\no-such-drive"){throw 'Plain PATH entries not kept in order'} } finally {Remove-Item -LiteralPath $pathRoot -Recurse -Force -ErrorAction SilentlyContinue} +# Mock only the machine boundary; exercise the shared installer without servicing this host. +. (Join-Path $PSScriptRoot 'windows-ssh-capability.ps1') +function Assert-IsolatedWindowsSshCi([string]$Arch){if($script:refuseCapabilityHost){throw 'Injected host refusal'}} +function Assert-WindowsSshGlobalServerDormant([string]$Server){$script:globalChecks++;if($script:globalChecks -eq $script:refuseGlobalCheck){throw 'Injected global server refusal'}} +function Assert-WindowsSshStockShell {$script:shellChecks++;if($script:shellChecks -eq $script:refuseShellCheck){throw 'Injected shell refusal'}} +function Get-WindowsCapability([switch]$Online,[string]$Name){$script:capabilityQueries++;return @{State=$script:capabilityState}} +function Add-WindowsCapability([switch]$Online,[string]$Name){$script:capabilityAdds++;if($script:failCapabilityInstall){throw 'Injected capability install failure'}} +function Reset-CapabilityControl([string]$State){ + $script:capabilityState=$State;$script:capabilityQueries=0;$script:capabilityAdds=0 + $script:globalChecks=0;$script:shellChecks=0;$script:refuseGlobalCheck=0;$script:refuseShellCheck=0 + $script:refuseCapabilityHost=$false;$script:failCapabilityInstall=$false + $script:capabilityStages=[Collections.Generic.List[string]]::new() +} +foreach($state in @('Installed','NotPresent')){ + Reset-CapabilityControl $state + $capabilityReport=@{} + Install-WindowsInboxSshCapability 'x64' $capabilityReport {param($name) $script:capabilityStages.Add($name)} + $expectedAdds=if($state -eq 'Installed'){0}else{1} + if($capabilityReport.inboxCapabilityInitialState -ne $state -or $script:capabilityAdds -ne $expectedAdds -or $script:globalChecks -ne 2 -or $script:shellChecks -ne 2 -or ($script:capabilityStages -join ',') -ne 'inbox-capability-start,inbox-capability-complete'){throw 'Shared capability preparation did not preserve the install and guard boundaries'} +} +foreach($fault in @('host','global-before','shell-before','global-after','shell-after','install')){ + Reset-CapabilityControl 'NotPresent' + switch($fault){ + 'host' {$script:refuseCapabilityHost=$true} + 'global-before' {$script:refuseGlobalCheck=1} + 'shell-before' {$script:refuseShellCheck=1} + 'global-after' {$script:refuseGlobalCheck=2} + 'shell-after' {$script:refuseShellCheck=2} + 'install' {$script:failCapabilityInstall=$true} + } + $rejected=$false + try {Install-WindowsInboxSshCapability 'x64' @{} {param($name) $script:capabilityStages.Add($name)}} catch {$rejected=$true} + if(-not $rejected -or $script:capabilityStages.Contains('inbox-capability-complete')){throw "Capability fault did not fail closed: $fault"} + if($fault -in @('host','global-before','shell-before') -and $script:capabilityAdds){throw 'Capability mutation preceded its host guards'} +} +$capabilityRoot=Join-Path ([IO.Path]::GetTempPath()) ([Guid]::NewGuid().ToString('N')) +try { + New-Item -ItemType Directory -Path $capabilityRoot | Out-Null + $capabilityReceipt=Join-Path $capabilityRoot 'capability.json' + Reset-CapabilityControl 'Installed' + Initialize-WindowsInboxSshCapability 'x64' $capabilityReceipt + $completed=Get-Content -LiteralPath $capabilityReceipt -Raw | ConvertFrom-Json + if($completed.status -ne 'passed' -or $completed.inboxCapabilityInitialState -ne 'Installed'){throw 'Capability success receipt missing its observed initial state'} + Reset-CapabilityControl 'NotPresent';$script:failCapabilityInstall=$true + $rejected=$false + try {Initialize-WindowsInboxSshCapability 'x64' $capabilityReceipt} catch {$rejected=$true} + $failed=Get-Content -LiteralPath $capabilityReceipt -Raw | ConvertFrom-Json + if(-not $rejected -or $failed.status -ne 'failed' -or $failed.inboxCapabilityInitialState -ne 'NotPresent' -or $failed.error -ne 'Injected capability install failure'){throw 'Failed capability preparation masqueraded as a passing receipt'} +} finally {Remove-Item -LiteralPath $capabilityRoot -Recurse -Force -ErrorAction SilentlyContinue} +& { + param($ProofAst) + $cleanup=@($ProofAst.FindAll({param($node) $node -is [Management.Automation.Language.TryStatementAst] -and $node.Body.Extent.Text.Contains("Write-Stage 'cleanup-start'")},$true)) + if($cleanup.Count -ne 1 -or -not $cleanup[0].Extent.Text.Contains('[Diagnostics.Stopwatch]::StartNew()')){throw 'Cleanup clock boundary missing'} + # Run the real cleanup gates with virtual clocks; no Windows machine operation may escape these mocks. + $cleanupBlock=[scriptblock]::Create($cleanup[0].Extent.Text.Replace('[Diagnostics.Stopwatch]::StartNew()','(New-ProfileControlStopwatch)').Replace('[DateTime]::UtcNow','(Get-ProfileControlUtcNow)')) + $ownedSids=@('S-1-5-21-100-200-300-1001','S-1-5-21-100-200-300-1002','S-1-5-21-100-200-300-1003') + $foreignSid='S-1-5-21-100-200-300-9000';$control=@{} + function New-ProfileControlStopwatch { + $watch=[pscustomobject]@{StartedMilliseconds=$control.clockMs;Control=$control} + $watch | Add-Member ScriptProperty ElapsedMilliseconds {$this.Control.clockMs-$this.StartedMilliseconds} + return $watch + } + function Get-ProfileControlUtcNow {[DateTime]::new(2026,10,2,0,0,0,[DateTimeKind]::Utc).AddMilliseconds($control.clockMs)} + function Start-Sleep([int]$Milliseconds,[int]$Seconds){$control.clockMs+=$Milliseconds+1000*$Seconds} + function Write-Stage([string]$Stage){$control.stages.Add($Stage)} + function Record-PrivateServiceDiagnostics([switch]$AfterStop){} + function Test-Path([string]$LiteralPath){if($LiteralPath -ne 'HKLM:\SOFTWARE\OpenSSH'){throw 'Unexpected filesystem query'};return $false} + function Get-ItemProperty([string]$LiteralPath,[object]$ErrorAction){if($LiteralPath -ne 'HKLM:\SOFTWARE\OpenSSH'){throw 'Unexpected registry query'};return $null} + function Remove-ItemProperty {throw 'Unexpected registry mutation'} + function Stop-Service([string]$Name,[switch]$Force,[object]$ErrorAction){if($Name -ne 'orca-sshd-control'){throw 'Foreign service stop'};if($control.case.Fault -ne 'server-exit'){$control.serverLive=$false}} + function Invoke-Bounded([string]$Program,[string[]]$Arguments){if($Program -ne 'sc.exe' -or ($Arguments -join '|') -ne 'delete|orca-sshd-control'){throw 'Unexpected native command'};if($control.case.Fault -ne 'service-absence'){$control.servicePresent=$false}} + function Get-Process([int]$Id,[object]$ErrorAction){if($Id -ne 100){throw 'Foreign process query'};if($control.serverLive){@{Id=100}}} + function Get-Service([string]$Name,[object]$ErrorAction){if($Name -ne 'orca-sshd-control'){throw 'Foreign service query'};if($control.servicePresent){@{Name=$Name}}} + function Get-ProfileControlLoaded([string]$Sid){ + $state=$control.profiles[$Sid] + if($state.Mode -eq 'late'){return $control.clockMs -lt $state.UnloadAtMs} + if($state.Mode -in @('reload','reload-at-deadline')){return $state.CurrentLoaded} + return $state.Mode -eq 'loaded' + } + function Get-CimInstance([Parameter(Position=0)][string]$ClassName,[string]$Filter){ + if($ClassName -eq 'Win32_Service'){ + if($Filter -ne "Name='orca-sshd-control'"){throw 'Foreign service query'} + if($control.servicePresent){@{PathName=$(if($control.case.Fault -eq 'service-identity'){'C:\foreign\sshd.exe'}else{'C:\fake\ossh-control\sshd.exe'});ProcessId=$(if($control.case.Fault -eq 'service-pid'){200}else{100})}};return + } + if($ClassName -eq 'Win32_Process'){ + if($control.case.Fault -eq 'child-exit'){@{ExecutablePath='C:\fake\OpenSSH\session.exe';ProcessId=101;ParentProcessId=100;CreationDate=(Get-ProfileControlUtcNow)}};return + } + if($ClassName -ne 'Win32_UserProfile' -or $Filter -notmatch "^SID='(S-1-5-21-100-200-300-\d+)'$" -or $Matches[1] -notin $ownedSids){throw 'Profile query must target an owned SID'} + $sid=$Matches[1];$control.clockMs+=$control.case.QueryCostMs;$control.profileQueries[$sid]++ + if($control.case.Fault -eq 'query' -and $sid -eq $ownedSids[1]){throw 'Injected profile query failure'} + if($control.case.Fault -eq 'foreign-result' -and $sid -eq $ownedSids[1]){[pscustomobject]@{SID=$foreignSid;Loaded=$false;Status=0};return} + $state=$control.profiles[$sid] + if($state.Mode -eq 'absent' -or $state.Deleted){return} + $loaded=Get-ProfileControlLoaded $sid + if($state.Mode -eq 'reload' -and $control.profileQueries[$sid] -eq 1){$loaded=$false;$state.CurrentLoaded=$true} + if($state.Mode -eq 'reload-at-deadline' -and $control.clockMs -ge 30000 -and -not $state.Reinjected){$loaded=$false;$state.CurrentLoaded=$true;$state.Reinjected=$true} + [pscustomobject]@{SID=$sid;Loaded=$loaded;Status=0} + } + function Remove-CimInstance { + param([Parameter(ValueFromPipeline=$true)][object]$InputObject) + process { + if($InputObject.SID -notin $ownedSids -or $InputObject.Loaded -or (Get-ProfileControlLoaded $InputObject.SID)){throw 'Unsafe profile deletion attempted'} + if($control.case.Fault -eq 'delete'){throw 'Injected profile deletion failure'} + $control.profiles[$InputObject.SID].Deleted=$true;$control.removed.Add($InputObject.SID) + } + } + function Get-LocalUser([string]$Name,[object]$ErrorAction){if(-not $control.users.ContainsKey($Name)){throw 'Foreign account query'};if($control.users[$Name]){@{Name=$Name}}} + function Remove-LocalUser([string]$Name){if(-not $control.users.ContainsKey($Name)){throw 'Foreign account deletion'};if($control.case.Fault -ne 'account'){$control.users[$Name]=$false}} + function Remove-Item([string]$LiteralPath,[switch]$Recurse,[switch]$Force){if($LiteralPath -ne 'C:\fake\ossh-control'){throw 'Foreign filesystem deletion'};if($control.case.Fault -eq 'keys'){throw 'Injected private key deletion failure'};$control.keysRemoved=$true} + function Assert-ProfileCleanup([hashtable]$Case){ + $control.Clear();$control.case=$Case;$control.clockMs=0;$control.serverLive=$true;$control.servicePresent=$true;$control.keysRemoved=$false + $control.stages=[Collections.Generic.List[string]]::new();$control.removed=[Collections.Generic.List[string]]::new() + $control.profiles=@{};$control.users=@{};$control.profileQueries=@{} + $ownedAccounts=@(for($index=0;$index -lt $ownedSids.Count;$index++){ + $sid=$ownedSids[$index];$control.profileQueries[$sid]=0 + $control.profiles[$sid]=@{Mode=$Case.Modes[$index];UnloadAtMs=$Case.UnloadAtMs[$index];CurrentLoaded=$true;Deleted=$false} + $name="orca-control-$index";$control.users[$name]=$true;@{name=$name;sid=$(if($Case.MissingSid -and $index -eq 2){$null}else{$sid})} + }) + $report=@{status='proof-passed-cleanup-pending'};$openSshKey='HKLM:\SOFTWARE\OpenSSH';$serviceName='orca-sshd-control' + $root='C:\fake\ossh-control';$createdService=$true;$ownedServerPid=100;$sshDir='C:\fake\OpenSSH';$preexisting=@();$deniedToolDirs=@() + & $cleanupBlock + if($Case.Fault){ + if($report.status -ne 'failed' -or $report.cleanupError -ne $Case.Error -or $control.keysRemoved -or $control.stages.Contains('cleanup-private-files-complete')){throw "Cleanup fault did not fail at its gate: $($Case.Name)"} + if($Case.Fault -notin @('account','keys') -and @($control.users.Values | Where-Object {-not $_}).Count){throw "Failure bypassed account cleanup gate: $($Case.Name)"} + return + } + if($report.status -ne 'passed' -or @($control.users.Values | Where-Object {$_}).Count -or -not $control.keysRemoved){throw "Cleanup omitted account/key gates: $($Case.Name)"} + if(($control.removed.ToArray() | Sort-Object) -join ',' -ne (($Case.Removed | Sort-Object) -join ',')){throw "Wrong removed SIDs: $($Case.Name)"} + if(@($report.profileCleanup | Where-Object {$_ -eq 'Loaded profile retained for disposable CI VM destruction'}).Count -ne $Case.Retained){throw "Wrong retained disposition: $($Case.Name)"} + foreach($sid in $Case.FullWindow){ + $observed=@($report.privateProfiles | Where-Object sid -eq $sid) + if($observed.Count -ne 1 -or $observed[0].waitMs -lt 30000){throw "Short profile observation window: $($Case.Name)"} + } + if($control.clockMs -gt $Case.MaxClockMs){throw "Profile windows were serialized: $($Case.Name)"} + if($Case.MissingSid -and $control.profileQueries[$ownedSids[2]]){throw 'Missing SID gained profile deletion authority'} + if($report.privateProfiles.Count -and ($report.profileUnloadWaitMs -ne $report.privateProfiles[-1].waitMs -or ($report.privateProfile | ConvertTo-Json -Compress) -ne ($report.privateProfiles[-1].profiles | ConvertTo-Json -Compress))){throw 'Legacy scalar observation lost owned-account order'} + } + $cases=@( + @{Name='three loaded full windows';Modes=@('loaded','loaded','loaded');Retained=3;Removed=@();FullWindow=$ownedSids}, + @{Name='unload at 29999ms';Modes=@('late','unloaded','absent');UnloadAtMs=@(29999,0,0);Retained=0;Removed=$ownedSids[0..1];FullWindow=@($ownedSids[0])}, + @{Name='reload before deletion';Modes=@('reload','unloaded','absent');Retained=1;Removed=@($ownedSids[1]);FullWindow=@($ownedSids[0])}, + @{Name='independent unloads';Modes=@('late','loaded','late');UnloadAtMs=@(5000,0,20000);Retained=1;Removed=@($ownedSids[0],$ownedSids[2]);FullWindow=@($ownedSids[1])}, + @{Name='slow provider queries';Modes=@('loaded','loaded','loaded');QueryCostMs=200;Retained=3;Removed=@();FullWindow=$ownedSids;MaxClockMs=40000}, + @{Name='reload at expired window';Modes=@('reload-at-deadline','unloaded','absent');Retained=1;Removed=@($ownedSids[1]);FullWindow=@($ownedSids[0])}, + @{Name='late unload honestly retained';Modes=@('loaded','loaded','late');UnloadAtMs=@(0,0,45000);Retained=3;Removed=@();FullWindow=$ownedSids}, + @{Name='missing SID is skipped';Modes=@('unloaded','absent','unloaded');Retained=0;Removed=@($ownedSids[0]);MissingSid=$true} + ) + foreach($case in $cases){ + if(-not $case.UnloadAtMs){$case.UnloadAtMs=@(0,0,0)} + if(-not $case.MaxClockMs){$case.MaxClockMs=33000} + Assert-ProfileCleanup $case + } + $failures=@{ + query='Injected profile query failure';delete='Injected profile deletion failure';'foreign-result'='Private profile query returned an unrelated SID' + 'service-identity'='Private service identity changed; refuse stop';'service-pid'='Private service identity changed; refuse stop' + 'server-exit'='Private sshd process still live; no PID-only kill attempted' + 'service-absence'='Private service still registered';'child-exit'='Private SSH child processes remain; preserve files and discard ephemeral runner' + account='Private account still exists';keys='Injected private key deletion failure' + } + foreach($failure in $failures.GetEnumerator()){Assert-ProfileCleanup @{Name=$failure.Key;Fault=$failure.Key;Error=$failure.Value;Modes=@('unloaded','unloaded','unloaded');UnloadAtMs=@(0,0,0)}} +} $ast # Extract functions through the AST: never provision the fixture while testing diagnostics. -'PASS: fixture parse, five numeric-diagnostic cases and the toolchain PATH split' +'PASS: fixture parse, diagnostics, PATH split, capability boundaries, profile windows and cleanup failure gates' diff --git a/config/ci/windows-ssh-provider/preview-ssh/windows-ssh-capability.ps1 b/config/ci/windows-ssh-provider/preview-ssh/windows-ssh-capability.ps1 new file mode 100644 index 000000000000..d2bdc9096a39 --- /dev/null +++ b/config/ci/windows-ssh-provider/preview-ssh/windows-ssh-capability.ps1 @@ -0,0 +1,61 @@ +function Assert-IsolatedWindowsSshCi([ValidateSet('arm64','x64')][string]$Arch) { + $os=@{arm64='Arm64';x64='X64'}[$Arch] + if($env:GITHUB_ACTIONS -ne 'true' -or $env:ORCA_ISOLATED_SSH_CI -ne '1' -or [Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() -ne $os){throw "Requires isolated native $Arch GitHub runner"} + $admin=([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) + if(-not $admin){throw 'Administrative private service/account setup required'} +} + +function Assert-WindowsSshGlobalServerDormant([ValidateSet('preview','inbox')][string]$Server) { + $global=Get-CimInstance Win32_Service -Filter "Name='sshd'" + if(-not $global){return} + $inboxDir=Join-Path $env:WINDIR 'System32\OpenSSH' + # Inbox installation may register the global service; it must never be started here. + if($Server -ne 'inbox' -or $global.State -ne 'Stopped' -or $global.PathName.Trim('"') -ne (Join-Path $inboxDir 'sshd.exe')){throw 'Refuse an existing global SSH server'} +} + +function Assert-WindowsSshStockShell { + $registry=Get-ItemProperty -LiteralPath 'HKLM:\SOFTWARE\OpenSSH' -ErrorAction SilentlyContinue + if($registry.DefaultShell -or $registry.DefaultShellCommandOption){throw 'Requires stock cmd.exe OpenSSH shell at start'} +} + +function Write-WindowsSshReceiptStage([hashtable]$Report,[string]$Receipt,[string]$Stage) { + $timestamp=[DateTime]::UtcNow.ToString('o') + $Report.stages+=@{stage=$Stage;utc=$timestamp} + try { + $bytes=[Text.UTF8Encoding]::new($false).GetBytes(($Report | ConvertTo-Json -Depth 6)) + $temporary="$Receipt.pending" + $stream=[IO.FileStream]::new($temporary,[IO.FileMode]::Create,[IO.FileAccess]::Write,[IO.FileShare]::Read) + try {$stream.Write($bytes,0,$bytes.Length);$stream.Flush($true)} finally {$stream.Dispose()} + [IO.File]::Move($temporary,$Receipt,$true) + $written=$true + } catch {$written=$false;Write-Warning 'Progress receipt could not be updated; cleanup must still run'} + Write-Host "Native SSH stage: $Stage ($timestamp)" + return $written +} + +function Install-WindowsInboxSshCapability([ValidateSet('arm64','x64')][string]$Arch,[hashtable]$Report,[scriptblock]$Stage) { + Assert-IsolatedWindowsSshCi $Arch + Assert-WindowsSshGlobalServerDormant 'inbox' + Assert-WindowsSshStockShell + & $Stage 'inbox-capability-start' + $capability=Get-WindowsCapability -Online -Name 'OpenSSH.Server~~~~0.0.1.0' + $Report.inboxCapabilityInitialState=[string]$capability.State + if($capability.State -ne 'Installed'){Add-WindowsCapability -Online -Name 'OpenSSH.Server~~~~0.0.1.0' | Out-Null} + Assert-WindowsSshGlobalServerDormant 'inbox' + Assert-WindowsSshStockShell + & $Stage 'inbox-capability-complete' +} + +function Initialize-WindowsInboxSshCapability([ValidateSet('arm64','x64')][string]$Arch,[string]$Receipt) { + $report=@{scope='Windows inbox OpenSSH.Server capability preparation only';arch=$Arch;sourceSha=$env:GITHUB_SHA;runId=$env:GITHUB_RUN_ID;runAttempt=$env:GITHUB_RUN_ATTEMPT;runnerName=$env:RUNNER_NAME;imageVersion=$env:ImageVersion;status='running';stages=@();globalBootstrapCleanup='Disposable CI VM destruction is the boundary; no global sshd is started'} + try { + if(-not (Write-WindowsSshReceiptStage $report $Receipt 'preparation-start')){throw 'Initial progress receipt unavailable; refuse provisioning'} + Install-WindowsInboxSshCapability $Arch $report {param($name) + if(-not (Write-WindowsSshReceiptStage $report $Receipt $name)){throw 'Capability preparation progress receipt unavailable'} + } + $report.status='passed' + } catch {$report.status='failed';$report.error=$_.Exception.Message;throw} + finally { + if(-not (Write-WindowsSshReceiptStage $report $Receipt 'preparation-finished')){throw 'Final capability preparation receipt unavailable'} + } +} diff --git a/config/docker/headless-pairing/Dockerfile.build b/config/docker/headless-pairing/Dockerfile.build index 19f76290c751..0300d8a82d49 100644 --- a/config/docker/headless-pairing/Dockerfile.build +++ b/config/docker/headless-pairing/Dockerfile.build @@ -26,7 +26,7 @@ RUN apt-get update \ && rm -rf /var/lib/apt/lists/* RUN corepack enable \ - && corepack prepare pnpm@12.0.0 --activate \ + && corepack prepare pnpm@12.8.1 --activate \ && pnpm --version WORKDIR /workspace diff --git a/config/electron-builder.config.cjs b/config/electron-builder.config.cjs index 20c3334bbdbd..b0666c42a3d6 100644 --- a/config/electron-builder.config.cjs +++ b/config/electron-builder.config.cjs @@ -164,9 +164,10 @@ const rpmElectronRuntimeDependencies = [ ] // Why mirrored, not imported: this config is CJS loaded by electron-builder outside the TS build. -// Keep in sync with isMarkdownDocumentName() in src/main/ipc/markdown-documents.ts and with +// Keep in sync with isOsOpenedDocumentName() in src/main/startup/os-opened-documents.ts and with // config/nsis/orca-installer-hooks.nsh, which registers the same set on Windows. const MARKDOWN_FILE_EXTENSIONS = ['md', 'markdown', 'mdx'] +const TABULAR_FILE_EXTENSIONS = ['csv', 'tsv'] // Why: the config must load on a host-only install without resolving unused Windows addons. // This is load-time tolerance only; beforePack enforces that the target's natives are installed. @@ -283,7 +284,7 @@ module.exports = { // before the GUI process starts, so those deps need the same treatment. // Why: out/package.json pins compiled output to CommonJS so parent // package.json files with type=module cannot change the packaged CLI loader. - // Why: the OpenCode SQLite worker entry is also spawned by the scanner + // Why: the foreign SQLite reader entry is also spawned by the scanner // service, which runs under ELECTRON_RUN_AS_NODE and so cannot see into // app.asar. Left packed, that spawn fails closed and every OpenCode session // disappears from Agent Session History in packaged builds only. Worker @@ -302,6 +303,7 @@ module.exports = { 'out/main/cursor/**', 'out/main/droid/**', 'out/main/gemini/**', + 'out/main/gitlab/project-ref-parser.js', 'out/main/grok/**', 'out/main/hermes/**', 'out/main/orca-profiles/profile-index-store.js', @@ -310,8 +312,7 @@ module.exports = { 'out/main/daemon-entry.js', 'out/main/session-scanner-service-entry.js', 'out/main/wsl-transcript-fs-process-entry.js', - 'out/main/cursor-desktop-profile-worker-entry.js', - 'out/main/session-scanner-opencode-sqlite-worker-entry.js', + 'out/main/foreign-sqlite-reader-entry.js', 'out/main/plugin-host-entry.js', 'out/main/computer-sidecar.js', 'out/main/parcel-watcher-process-entry.js', @@ -510,16 +511,25 @@ module.exports = { include: resolve(__dirname, 'nsis', 'orca-installer-hooks.nsh') }, mac: { - // Why rank Alternate: Orca joins Finder's "Open With" list for Markdown without claiming + // Why rank Alternate: Orca joins Finder's "Open With" list without claiming // LSHandlerRank ownership, so whichever editor the user already prefers stays the default. // Why one entry per extension: app-builder-lib globs `*.${ext}`, which an array would break. - fileAssociations: MARKDOWN_FILE_EXTENSIONS.map((ext) => ({ - ext, - name: 'Markdown Document', - description: 'Markdown Document', - role: 'Editor', - rank: 'Alternate' - })), + fileAssociations: [ + ...MARKDOWN_FILE_EXTENSIONS.map((ext) => ({ + ext, + name: 'Markdown Document', + description: 'Markdown Document', + role: 'Editor', + rank: 'Alternate' + })), + ...TABULAR_FILE_EXTENSIONS.map((ext) => ({ + ext, + name: `${ext.toUpperCase()} Document`, + description: `${ext.toUpperCase()} Document`, + role: 'Editor', + rank: 'Alternate' + })) + ], icon: 'resources/build/icon.icns', entitlements: 'resources/build/entitlements.mac.plist', entitlementsInherit: 'resources/build/entitlements.mac.plist', @@ -612,7 +622,7 @@ module.exports = { // override. A desktop entry's MimeType only adds a handler - mimeapps.list still owns the // default. .mdx is deliberately absent: Ubuntu 24.04's mime database maps it to // application/x-genesis-32x-rom, so claiming it here would need a glob override. - mimeTypes: ['text/markdown'], + mimeTypes: ['text/markdown', 'text/csv', 'text/tab-separated-values'], // Why: Ubuntu desktop ships GNOME Orca as the `orca` package and /usr/bin/orca. // The Linux installer should not claim those system package/file names. executableName: 'orca-ide', diff --git a/config/knip.json b/config/knip.json index 92c55c8ac7d7..98891b9e687a 100644 --- a/config/knip.json +++ b/config/knip.json @@ -9,9 +9,7 @@ "src/main/computer/sidecar-entry.ts", "src/main/speech/stt-worker.ts", "src/main/warp-themes/warp-theme-parser-worker.ts", - "src/main/rate-limits/cursor-desktop-profile-worker-entry.ts", - "src/main/ai-vault/session-scanner-opencode-sqlite-worker-entry.ts", - "src/main/ai-vault/session-scanner-worker-entry.ts", + "src/main/foreign-sqlite-readers/foreign-sqlite-reader-entry.ts", "src/main/ports/port-scan-command-worker-entry.ts", "src/main/ipc/parcel-watcher-process-entry.ts", "src/main/hang-watchdog/main-thread-hang-watchdog-entry.ts", diff --git a/config/nsis/orca-installer-hooks.nsh b/config/nsis/orca-installer-hooks.nsh index a6fb31f9aec9..6d6dd0695922 100644 --- a/config/nsis/orca-installer-hooks.nsh +++ b/config/nsis/orca-installer-hooks.nsh @@ -6,7 +6,7 @@ !include "${__FILEDIR__}\orca-process-check.nsh" ; --------------------------------------------------------------------------- -; Markdown "Open with Orca" (issue #10138) +; Markdown and CSV/TSV "Open with Orca" (issues #10138, #23225) ; ; Why hand-rolled instead of electron-builder's `fileAssociations` on Windows: ; app-builder-lib emits !insertmacro APP_ASSOCIATE, whose first line is @@ -21,29 +21,36 @@ ; exactly where the user left it. Never add a `Software\Classes\.` default ; value here. ; -; MARKDOWN_PROGID must stay in sync with the extension list handled by -; isMarkdownDocumentName() in src/main/ipc/markdown-documents.ts. +; Keep the extension list in sync with isOsOpenedDocumentName(). ; --------------------------------------------------------------------------- !define MARKDOWN_PROGID "Orca.Markdown" +!define TABULAR_PROGID "Orca.Tabular" -!macro ORCA_REGISTER_MARKDOWN_OPEN_WITH EXT - WriteRegNone SHELL_CONTEXT "Software\Classes\${EXT}\OpenWithProgids" "${MARKDOWN_PROGID}" +!macro ORCA_REGISTER_DOCUMENT_OPEN_WITH EXT PROGID + WriteRegNone SHELL_CONTEXT "Software\Classes\${EXT}\OpenWithProgids" "${PROGID}" WriteRegStr SHELL_CONTEXT "Software\Classes\Applications\${APP_EXECUTABLE_FILENAME}\SupportedTypes" "${EXT}" "" !macroend -!macro ORCA_UNREGISTER_MARKDOWN_OPEN_WITH EXT - DeleteRegValue SHELL_CONTEXT "Software\Classes\${EXT}\OpenWithProgids" "${MARKDOWN_PROGID}" +!macro ORCA_UNREGISTER_DOCUMENT_OPEN_WITH EXT PROGID + DeleteRegValue SHELL_CONTEXT "Software\Classes\${EXT}\OpenWithProgids" "${PROGID}" DeleteRegValue SHELL_CONTEXT "Software\Classes\Applications\${APP_EXECUTABLE_FILENAME}\SupportedTypes" "${EXT}" !macroend +!macro ORCA_REGISTER_DOCUMENT_PROGID PROGID NAME + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROGID}" "" "${NAME}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROGID}\DefaultIcon" "" "$appExe,0" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROGID}\shell\open" "" "Open with ${PRODUCT_NAME}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROGID}\shell\open\command" "" '"$appExe" "%1"' +!macroend + !macro customInstall - WriteRegStr SHELL_CONTEXT "Software\Classes\${MARKDOWN_PROGID}" "" "Markdown Document" - WriteRegStr SHELL_CONTEXT "Software\Classes\${MARKDOWN_PROGID}\DefaultIcon" "" "$appExe,0" - WriteRegStr SHELL_CONTEXT "Software\Classes\${MARKDOWN_PROGID}\shell\open" "" "Open with ${PRODUCT_NAME}" - WriteRegStr SHELL_CONTEXT "Software\Classes\${MARKDOWN_PROGID}\shell\open\command" "" '"$appExe" "%1"' - !insertmacro ORCA_REGISTER_MARKDOWN_OPEN_WITH ".md" - !insertmacro ORCA_REGISTER_MARKDOWN_OPEN_WITH ".markdown" - !insertmacro ORCA_REGISTER_MARKDOWN_OPEN_WITH ".mdx" + !insertmacro ORCA_REGISTER_DOCUMENT_PROGID "${MARKDOWN_PROGID}" "Markdown Document" + !insertmacro ORCA_REGISTER_DOCUMENT_PROGID "${TABULAR_PROGID}" "Tabular Document" + !insertmacro ORCA_REGISTER_DOCUMENT_OPEN_WITH ".md" "${MARKDOWN_PROGID}" + !insertmacro ORCA_REGISTER_DOCUMENT_OPEN_WITH ".markdown" "${MARKDOWN_PROGID}" + !insertmacro ORCA_REGISTER_DOCUMENT_OPEN_WITH ".mdx" "${MARKDOWN_PROGID}" + !insertmacro ORCA_REGISTER_DOCUMENT_OPEN_WITH ".csv" "${TABULAR_PROGID}" + !insertmacro ORCA_REGISTER_DOCUMENT_OPEN_WITH ".tsv" "${TABULAR_PROGID}" ; Why: Explorer caches the association list until told otherwise. System::Call "shell32::SHChangeNotify(i,i,i,i) (0x08000000, 0x1000, 0, 0)" !macroend @@ -100,8 +107,11 @@ ; Why outside the ${isUpdated} guard: customInstall rewrites these on every update, so ; dropping them during uninstallOldVersion is correct and keeps the pair symmetric. DeleteRegKey SHELL_CONTEXT "Software\Classes\${MARKDOWN_PROGID}" - !insertmacro ORCA_UNREGISTER_MARKDOWN_OPEN_WITH ".md" - !insertmacro ORCA_UNREGISTER_MARKDOWN_OPEN_WITH ".markdown" - !insertmacro ORCA_UNREGISTER_MARKDOWN_OPEN_WITH ".mdx" + DeleteRegKey SHELL_CONTEXT "Software\Classes\${TABULAR_PROGID}" + !insertmacro ORCA_UNREGISTER_DOCUMENT_OPEN_WITH ".md" "${MARKDOWN_PROGID}" + !insertmacro ORCA_UNREGISTER_DOCUMENT_OPEN_WITH ".markdown" "${MARKDOWN_PROGID}" + !insertmacro ORCA_UNREGISTER_DOCUMENT_OPEN_WITH ".mdx" "${MARKDOWN_PROGID}" + !insertmacro ORCA_UNREGISTER_DOCUMENT_OPEN_WITH ".csv" "${TABULAR_PROGID}" + !insertmacro ORCA_UNREGISTER_DOCUMENT_OPEN_WITH ".tsv" "${TABULAR_PROGID}" System::Call "shell32::SHChangeNotify(i,i,i,i) (0x08000000, 0x1000, 0, 0)" !macroend diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index 5b4c135c1da4..a157e9dc79c2 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -12107,10 +12107,10 @@ "https://github.com/stablyai/orca/pull/12778" ], "invariant": "Typing, focus, terminal switch, workspace switch, visibility resume, resize, render, per-pane liveness, and tab-title synchronization must not call global pty:listSessions or aiVault.listSessions; they must use targeted APIs or cached provider-owned state.", - "oracle": "The current executable slice asserts targeted visibility/first-input liveness, resize re-assertion after visibility resume, light tab/active-state resume, SSH/remote skip behavior, and a closed Resource Manager budget of one readiness seed plus one coalesced inventory read only for unknown spawn IDs. AI Vault title sync deterministically accepts only resolveSessionTitles, batches at most 64 exact identities, serializes worker work, routes requests to the transcript-owning local/SSH/runtime host, and proves zero broad scans for unsupported hosts. The full hot-path oracle still needs instrumentation around raw focus, split focus, workspace switch, render ticks, and high-session PTY fixtures.", + "oracle": "The current executable slice asserts targeted visibility/first-input liveness, resize re-assertion after visibility resume, light tab/active-state resume, SSH/remote skip behavior, and a closed Resource Manager budget of one readiness seed plus one coalesced inventory read only for unknown spawn IDs. AI Vault title sync deterministically accepts only resolveSessionTitles, batches at most 64 exact identities, bounds scanner-service calls at sixteen, routes requests to the transcript-owning local/SSH/runtime host, and proves zero broad scans for unsupported hosts. The full hot-path oracle still needs instrumentation around raw focus, split focus, workspace switch, render ticks, and high-session PTY fixtures.", "commands": [ "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/pty-startup-barrier-and-listing.test.ts src/renderer/src/components/status-bar/use-resource-session-inventory.test.tsx src/renderer/src/components/status-bar/resource-session-inventory.test.ts src/renderer/src/components/status-bar/ResourceUsageStatusSegment.session-polling.test.ts", - "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/lib/ai-vault-tab-title-sync.test.ts src/main/ai-vault/session-scanner-worker-client.test.ts src/main/ai-vault/session-title-file-reader.test.ts src/main/ai-vault/session-parse-cache-persistence.test.ts src/main/ipc/ai-vault.test.ts src/main/runtime/rpc/methods/ai-vault.test.ts src/relay/ai-vault-handler.test.ts" + "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/lib/ai-vault-tab-title-sync.test.ts src/main/ai-vault/session-scanner-service-client.test.ts src/main/ai-vault/session-title-file-reader.test.ts src/main/ai-vault/session-parse-cache-persistence.test.ts src/main/ipc/ai-vault.test.ts src/main/runtime/rpc/methods/ai-vault.test.ts src/relay/ai-vault-handler.test.ts" ], "testFiles": [ "src/main/ipc/pty-startup-barrier-and-listing.test.ts", @@ -12118,7 +12118,7 @@ "src/renderer/src/components/status-bar/resource-session-inventory.test.ts", "src/renderer/src/components/status-bar/ResourceUsageStatusSegment.session-polling.test.ts", "src/renderer/src/lib/ai-vault-tab-title-sync.test.ts", - "src/main/ai-vault/session-scanner-worker-client.test.ts", + "src/main/ai-vault/session-scanner-service-client.test.ts", "src/main/ai-vault/session-title-file-reader.test.ts", "src/main/ai-vault/session-parse-cache-persistence.test.ts", "src/main/ipc/ai-vault.test.ts", @@ -12168,12 +12168,12 @@ ] }, { - "file": "src/main/ai-vault/session-scanner-worker-client.test.ts", + "file": "src/main/ai-vault/session-scanner-service-client.test.ts", "assertions": [ - "full scans and exact-title reads share one serial FIFO worker", - "active cancellation stays serialized and queued work remains bounded", - "worker faults restart queued work and idle time preserves incremental parse state", - "worker disposal rejects retained work and terminates the worker" + "the service waits for ready and runs the cache and interactive lanes independently", + "active and queued calls are bounded together at sixteen", + "cancellation reaches active work and kills a service that ignores it", + "service faults restart queued work under a restart circuit that a forced refresh reopens" ] }, { @@ -12202,13 +12202,13 @@ "summary": "4 files and 358 tests passed, covering readiness seed/recovery, zero interval polling, bounded unknown-spawn reconciliation, concurrent provider starts, exit fencing, cleanup, and out-of-order refresh fencing." }, { - "date": "2026-08-09", + "date": "2026-10-02", "runner": "local", "platform": "macos", - "command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/lib/ai-vault-tab-title-sync.test.ts src/main/ai-vault/session-scanner-worker-client.test.ts src/main/ai-vault/session-title-file-reader.test.ts src/main/ai-vault/session-parse-cache-persistence.test.ts src/main/ipc/ai-vault.test.ts src/main/runtime/rpc/methods/ai-vault.test.ts src/relay/ai-vault-handler.test.ts", + "command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/lib/ai-vault-tab-title-sync.test.ts src/main/ai-vault/session-scanner-service-client.test.ts src/main/ai-vault/session-title-file-reader.test.ts src/main/ai-vault/session-parse-cache-persistence.test.ts src/main/ipc/ai-vault.test.ts src/main/runtime/rpc/methods/ai-vault.test.ts src/relay/ai-vault-handler.test.ts", "result": "passed", - "durationSeconds": 3.1, - "summary": "The focused run passed 112 tests across 7 files, proving exact-title-only renderer requests, provider-isolated batching, persistent serial worker lifecycle and fault recovery, exact transcript identity, host routing, mixed-version degradation, and zero broad-scan fallback." + "durationSeconds": 5.3, + "summary": "The focused run passed 138 tests across 7 files, proving exact-title-only renderer requests, provider-isolated batching, per-lane scanner-service lifecycle and fault recovery, exact transcript identity, host routing, mixed-version degradation, and zero broad-scan fallback." } ], "runtimeBudget": { @@ -12221,11 +12221,11 @@ }, "redGreenEvidence": { "status": "partial", - "evidence": "Tests assert visibility resume prefers targeted hasPty over listSessions, first input after visibility resume calls targeted hasPty once, resize re-assertion after visibility resume uses getSize/resize without listSessions, light tab switches and visible active-state resume avoid listSessions/hasPty/getSize fanout, and the closed Resource Manager performs one readiness seed while known reattach signals and steady time perform no additional reads. For the #12778 regression, title sync no longer receives a listSessions dependency at all: it sends at most 64 exact identities per batch to one serial worker or transcript-owning remote host, and old hosts degrade without broad fallback. Needs broader raw focus/workspace-switch/render/high-session PTY count coverage before promotion." + "evidence": "Tests assert visibility resume prefers targeted hasPty over listSessions, first input after visibility resume calls targeted hasPty once, resize re-assertion after visibility resume uses getSize/resize without listSessions, light tab switches and visible active-state resume avoid listSessions/hasPty/getSize fanout, and the closed Resource Manager performs one readiness seed while known reattach signals and steady time perform no additional reads. For the #12778 regression, title sync no longer receives a listSessions dependency at all: it sends at most 64 exact identities per batch to the local scanner service or transcript-owning remote host, and old hosts degrade without broad fallback. Needs broader raw focus/workspace-switch/render/high-session PTY count coverage before promotion." }, "performanceBudget": { "required": true, - "evidence": "This gate is the performance budget for global session listing in hot paths. AI Vault title sync permits zero global scans, at most 64 exact identities per request, one active worker operation, 16 queued operations, four concurrent transcript parses inside the worker, a 4,096-title index, and no worktree-path-triggered refresh. The worker emits the aiVault.scan.worker span with duration and session count for full scans." + "evidence": "This gate is the performance budget for global session listing in hot paths. AI Vault title sync permits zero global scans, at most 64 exact identities per request, one active scanner-service call per lane (cache, interactive), 16 active plus queued calls, four concurrent transcript parses inside the service, a 4,096-title index, and no worktree-path-triggered refresh. The service emits the aiVault.scan.service span with duration and session count for full scans." }, "promotionCriteria": [ "Add deterministic call-count instrumentation.", diff --git a/config/scripts/agent-state-rules-bundle.mjs b/config/scripts/agent-state-rules-bundle.mjs new file mode 100644 index 000000000000..170d6b4c1220 --- /dev/null +++ b/config/scripts/agent-state-rules-bundle.mjs @@ -0,0 +1,174 @@ +// Builds and publishes the agent state rules bundle (agent-state-rules.json). The app's loader +// validates the same file (src/main/runtime/agent-state-rules/agent-state-rules-bundle.ts), and +// agent-state-rules-bundle.test.mjs proves the build passes it; this script only assembles the +// file and enforces the publishing rules a single file cannot express. +// +// node config/scripts/agent-state-rules-bundle.mjs build [--bundled-only] +// node config/scripts/agent-state-rules-bundle.mjs publish-next [--bundled-only] +// node config/scripts/agent-state-rules-bundle.mjs promote-stable + +import { spawnSync } from 'node:child_process' +import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { pathToFileURL } from 'node:url' +import { agentStateRulesTag } from './release-tag-patterns.mjs' + +const RULES_DIR = join(import.meta.dirname, '..', '..', 'src/main/runtime/agent-state-rules') +export const AGENT_STATE_RULES_ASSET = 'agent-state-rules.json' + +function readJson(path) { + return JSON.parse(readFileSync(path, 'utf8')) +} + +/** The live-updatable agents' files, in release order, under the release's version. */ +export function buildAgentStateRulesBundle({ bundledOnly = false } = {}) { + const release = readJson(join(RULES_DIR, 'agent-state-rules-release.json')) + const files = release.liveUpdatable.map((id) => readJson(join(RULES_DIR, `${id}.json`))) + const bundle = { + version: release.version, + engineVersion: files[0].engineVersion, + ...(bundledOnly ? { bundledOnly: true } : {}), + files + } + return `${JSON.stringify(bundle, null, 2)}\n` +} + +function runGh(args) { + const result = spawnSync('gh', args, { encoding: 'utf8' }) + return { status: result.status ?? 1, stdout: result.stdout ?? '', stderr: result.stderr ?? '' } +} + +function ghOrThrow(gh, args) { + const result = gh(args) + if (result.status !== 0) { + throw new Error(`gh ${args.join(' ')} failed: ${result.stderr.trim()}`) + } + return result.stdout +} + +/** The file on `tag`'s release, or null when there is no release yet. */ +function downloadPublished(gh, repo, tag) { + const args = [ + 'release', + 'download', + tag, + '--repo', + repo, + '-p', + AGENT_STATE_RULES_ASSET, + '-O', + '-' + ] + const result = gh(args) + if (result.status === 0) { + return result.stdout + } + // Why throw on anything else, a missing asset included: that means an earlier upload broke. + if (/release not found/i.test(result.stderr)) { + return null + } + throw new Error(`gh ${args.join(' ')} failed: ${result.stderr.trim()}`) +} + +/** + * Puts `text` on the channel's release for its engine, as a prerelease that never becomes Latest + * (the app updater follows Latest). `gh` is injectable so the sequence is testable. + */ +export function publishAgentStateRules({ repo, channel, text, target, gh = runGh }) { + const candidate = JSON.parse(text) + const tag = agentStateRulesTag(candidate.engineVersion, channel) + const publishedText = downloadPublished(gh, repo, tag) + // Why strictly higher: apps refuse a version they already have, so republishing one would reach + // nobody, and a lower one would reach only apps that never took the higher. + const published = publishedText === null ? null : JSON.parse(publishedText).version + if (published !== null && candidate.version <= published) { + throw new Error( + `version ${candidate.version} is not higher than the published ${published}; bump agent-state-rules-release.json` + ) + } + const file = join(mkdtempSync(join(tmpdir(), 'agent-state-rules-')), AGENT_STATE_RULES_ASSET) + writeFileSync(file, text) + if (published === null) { + ghOrThrow(gh, [ + 'release', + 'create', + tag, + file, + '--repo', + repo, + '--target', + target, + '--prerelease', + '--latest=false', + '--title', + `Agent state rules (${channel})`, + '--notes', + 'Agent state rules for Orca. Running apps download this file; it is not an app release.' + ]) + } else { + // Why --clobber on the same tag: the app's fixed URL stays valid, and a fetch that lands + // mid-upload gets a 404 and keeps its last good copy. + ghOrThrow(gh, ['release', 'upload', tag, file, '--repo', repo, '--clobber']) + } + return { tag, version: candidate.version } +} + +/** + * Why no rebuild and no re-gate: stable gets exactly the bytes RC and dev builds soaked on next, + * which only the gated publish-next job writes. A bundledOnly next is promoted too: that is how + * stable rolls back to the rules it shipped. + */ +export function promoteAgentStateRules({ repo, engineVersion, target, gh = runGh }) { + const nextTag = agentStateRulesTag(engineVersion, 'next') + const text = downloadPublished(gh, repo, nextTag) + if (text === null) { + throw new Error(`${nextTag} has not been published`) + } + return publishAgentStateRules({ repo, channel: 'stable', text, target, gh }) +} + +function requireEnv(name) { + const value = process.env[name] + if (!value) { + throw new Error(`${name} is not set`) + } + return value +} + +function main([command, ...args]) { + const bundledOnly = args.includes('--bundled-only') + switch (command) { + case 'build': { + const out = args.find((arg) => !arg.startsWith('--')) + if (!out) { + throw new Error('usage: build [--bundled-only]') + } + writeFileSync(out, buildAgentStateRulesBundle({ bundledOnly })) + return + } + case 'publish-next': + case 'promote-stable': { + const repo = requireEnv('GITHUB_REPOSITORY') + const target = requireEnv('GITHUB_SHA') + const text = buildAgentStateRulesBundle({ bundledOnly }) + const { tag, version } = + command === 'publish-next' + ? publishAgentStateRules({ repo, channel: 'next', text, target }) + : promoteAgentStateRules({ repo, engineVersion: JSON.parse(text).engineVersion, target }) + console.log(`Published version ${version} to ${tag}.`) + return + } + default: + throw new Error(`unknown command ${command ?? '(none)'}`) + } +} + +if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) { + try { + main(process.argv.slice(2)) + } catch (error) { + console.error(`::error::${error instanceof Error ? error.message : String(error)}`) + process.exitCode = 1 + } +} diff --git a/config/scripts/agent-state-rules-bundle.test.mjs b/config/scripts/agent-state-rules-bundle.test.mjs new file mode 100644 index 000000000000..38fd0de6633e --- /dev/null +++ b/config/scripts/agent-state-rules-bundle.test.mjs @@ -0,0 +1,179 @@ +// The rules-release gate: the bundle a release would publish validates in the app's own loader, +// carries only agents whose transcripts the census replays, and only the protected workflow can +// publish it. +import { readdirSync, readFileSync } from 'node:fs' +import { describe, expect, it } from 'vitest' +import { parse } from 'yaml' +import { + BUNDLED_AGENT_STATE_RULES_VERSION, + LIVE_UPDATABLE_AGENT_STATE_RULE_IDS, + parseAgentStateRulesBundle +} from '../../src/main/runtime/agent-state-rules/agent-state-rules-bundle.ts' +import { BUNDLED_AGENT_STATE_RULE_FILES } from '../../src/main/runtime/agent-state-rules/agent-state-rules-catalog.ts' +import { agentStateRulesDownloadUrl } from '../../src/main/runtime/agent-state-rules/agent-state-rules-live-update.ts' +import { + AGENT_STATE_RULES_ENGINE_VERSION, + UNKNOWN_PANE_RULES_ID +} from '../../src/main/runtime/agent-state-rules/agent-state-rules-schema.ts' +import { CENSUS_TRANSCRIPTS } from '../../src/main/runtime/readiness-census-transcript-catalog.ts' +import { + AGENT_STATE_RULES_ASSET, + buildAgentStateRulesBundle, + promoteAgentStateRules, + publishAgentStateRules +} from './agent-state-rules-bundle.mjs' +import { agentStateRulesTag } from './release-tag-patterns.mjs' + +const REPO = 'stablyai/orca' +const NEXT = agentStateRulesTag(1, 'next') +const STABLE = agentStateRulesTag(1, 'stable') + +describe('agent state rules bundle build', () => { + it('builds a bundle the app accepts, under the bundled version and engine', () => { + const text = buildAgentStateRulesBundle() + const parsed = parseAgentStateRulesBundle(text, 'live-updatable') + expect(parsed.ok).toBe(true) + const bundle = JSON.parse(text) + expect(bundle.engineVersion).toBe(AGENT_STATE_RULES_ENGINE_VERSION) + expect(bundle.version).toBe(BUNDLED_AGENT_STATE_RULES_VERSION) + expect(bundle.files).toEqual( + BUNDLED_AGENT_STATE_RULE_FILES.filter((file) => + LIVE_UPDATABLE_AGENT_STATE_RULE_IDS.has(file.id) + ) + ) + expect(bundle.bundledOnly).toBeUndefined() + expect(JSON.parse(buildAgentStateRulesBundle({ bundledOnly: true })).bundledOnly).toBe(true) + }) + + it('lets a rules release change only agents the readiness census replays', () => { + const replayed = new Set(CENSUS_TRANSCRIPTS.flatMap((transcript) => transcript.agent ?? [])) + // Why unknown-pane: the census replays every recording on an agent-unknown pane too. + replayed.add(UNKNOWN_PANE_RULES_ID) + expect([...LIVE_UPDATABLE_AGENT_STATE_RULE_IDS].filter((id) => !replayed.has(id))).toEqual([]) + }) + + it('publishes to the exact URL the app fetches', () => { + for (const channel of ['next', 'stable']) { + expect(agentStateRulesDownloadUrl(channel)).toBe( + `https://github.com/${REPO}/releases/download/${agentStateRulesTag(AGENT_STATE_RULES_ENGINE_VERSION, channel)}/${AGENT_STATE_RULES_ASSET}` + ) + } + }) +}) + +/** A `gh` stand-in over an in-memory set of releases, recording each call. */ +function fakeGh(releases = {}) { + const calls = [] + const gh = (args) => { + calls.push(args) + const [, verb, tag] = args + if (verb === 'download') { + return tag in releases + ? { status: 0, stdout: releases[tag], stderr: '' } + : { status: 1, stdout: '', stderr: 'release not found' } + } + if (verb === 'upload' || verb === 'create') { + expect(args[3].endsWith(`/${AGENT_STATE_RULES_ASSET}`)).toBe(true) + releases[tag] = readFileSync(args[3], 'utf8') + } + return { status: 0, stdout: '', stderr: '' } + } + return { gh, calls, releases } +} + +const at = (version, engineVersion = 1) => + `${JSON.stringify({ version, engineVersion, files: [] })}\n` + +describe('publishAgentStateRules', () => { + it("creates the engine's channel release as a prerelease that can never be Latest", () => { + const fake = fakeGh() + expect( + publishAgentStateRules({ + repo: REPO, + channel: 'next', + text: at(2), + target: 'abc', + gh: fake.gh + }) + ).toEqual({ tag: NEXT, version: 2 }) + expect(fake.calls.find((args) => args[1] === 'create')).toEqual( + expect.arrayContaining([NEXT, '--prerelease', '--latest=false', '--target', 'abc']) + ) + expect(fake.releases[NEXT]).toBe(at(2)) + }) + + it('replaces the asset in place on an existing release, keeping the tag', () => { + const fake = fakeGh({ [NEXT]: at(1) }) + publishAgentStateRules({ repo: REPO, channel: 'next', text: at(2), target: 'abc', gh: fake.gh }) + expect(fake.calls.map((args) => args[1])).toEqual(['download', 'upload']) + expect(fake.calls[1]).toContain('--clobber') + expect(fake.releases[NEXT]).toBe(at(2)) + }) + + it.each([1, 2])('refuses version %s over a published 2, uploading nothing', (version) => { + const fake = fakeGh({ [NEXT]: at(2) }) + expect(() => + publishAgentStateRules({ + repo: REPO, + channel: 'next', + text: at(version), + target: 'abc', + gh: fake.gh + }) + ).toThrow('bump agent-state-rules-release.json') + expect(fake.calls.map((args) => args[1])).toEqual(['download']) + }) + + it('fails when the release exists but its download fails', () => { + const gh = () => ({ status: 1, stdout: '', stderr: 'no assets match the file pattern' }) + expect(() => + publishAgentStateRules({ repo: REPO, channel: 'next', text: at(2), target: 'abc', gh }) + ).toThrow('no assets match') + }) + + it('promotes the identical next bytes to stable', () => { + const nextText = JSON.stringify({ version: 3, engineVersion: 1, files: [] }, null, 2) + const fake = fakeGh({ [NEXT]: nextText, [STABLE]: at(2) }) + promoteAgentStateRules({ repo: REPO, engineVersion: 1, target: 'abc', gh: fake.gh }) + expect(fake.releases[STABLE]).toBe(nextText) + }) + + it('refuses to promote before next exists', () => { + const fake = fakeGh() + expect(() => + promoteAgentStateRules({ repo: REPO, engineVersion: 1, target: 'abc', gh: fake.gh }) + ).toThrow('has not been published') + }) +}) + +describe('agent state rules workflows', () => { + const read = (name) => parse(readFileSync(`.github/workflows/${name}`, 'utf8')) + const publish = read('agent-state-rules-publish.yml') + + it('publishes only on manual dispatch from main, in the protected environment', () => { + expect(Object.keys(publish.on)).toEqual(['workflow_dispatch']) + for (const name of ['publish-next', 'promote-stable']) { + const job = publish.jobs[name] + expect(job.environment).toBe('agent-state-rules') + expect(job.permissions).toEqual({ contents: 'write' }) + } + expect(publish.permissions).toEqual({ contents: 'read' }) + expect(publish.jobs.gate.if).toContain("github.ref == 'refs/heads/main'") + expect(publish.jobs['promote-stable'].if).toContain("github.ref == 'refs/heads/main'") + expect(publish.jobs['publish-next'].needs).toBe('gate') + // Why: every job must check out the dispatched commit, so publish runs what the gate tested. + const checkouts = Object.values(publish.jobs).flatMap((job) => + job.steps.filter((step) => step.uses?.startsWith('actions/checkout')) + ) + expect(checkouts.map((step) => step.with?.ref)).toEqual([undefined, undefined, undefined]) + }) + + it('is the only workflow that publishes rules releases', () => { + const publishers = readdirSync('.github/workflows').filter((name) => + /agent-state-rules-bundle\.mjs (?:publish|promote)|release create agent-state-rules/.test( + readFileSync(`.github/workflows/${name}`, 'utf8') + ) + ) + expect(publishers).toEqual(['agent-state-rules-publish.yml']) + }) +}) diff --git a/config/scripts/build-mobile-web-app-bundle.mjs b/config/scripts/build-mobile-web-app-bundle.mjs index 09f5f913a94e..40e47746747e 100644 --- a/config/scripts/build-mobile-web-app-bundle.mjs +++ b/config/scripts/build-mobile-web-app-bundle.mjs @@ -363,15 +363,12 @@ export function mobileWebAppBuildOptions(routes) { */ export function entryStaticClosure(metafile, entryOutputPath) { const reached = new Set([entryOutputPath]) - const queue = [entryOutputPath] - while (queue.length > 0) { - const current = queue.shift() + for (const current of reached) { for (const imported of metafile.outputs[current]?.imports ?? []) { if (imported.kind !== 'import-statement' || reached.has(imported.path)) { continue } reached.add(imported.path) - queue.push(imported.path) } } return reached diff --git a/config/scripts/build-orcad.mjs b/config/scripts/build-orcad.mjs index 2bd9933d33aa..66c8f535a5bd 100644 --- a/config/scripts/build-orcad.mjs +++ b/config/scripts/build-orcad.mjs @@ -23,10 +23,12 @@ import { tmpdir } from 'node:os' import { dirname, join, resolve } from 'node:path' import process from 'node:process' import { smokeProfileStateWorkers } from './profile-state-worker-smoke.mjs' +import { smokeForeignSqliteReaderWorker } from './foreign-sqlite-reader-worker-smoke.mjs' import { materializeWatcherPackage } from './orcad-watcher-package.mjs' import { stageOrcadWindowsProcessTree } from './orcad-windows-process-tree.mjs' import { ORCAD_EMOJI_SHORTCODE_DATASET, + ORCAD_FOREIGN_SQLITE_READER_ENTRY, ORCAD_NODE_PTY_DIR, ORCAD_NODE_PTY_JS_ARTIFACTS, ORCAD_NODE_RUNTIME_MARKER_FILENAME, @@ -56,6 +58,10 @@ const WATCHER_OUT_FILE = join(OUT_DIR, 'parcel-watcher-process-entry.js') // orcad restart would SIGKILL every running terminal. const DAEMON_ENTRY = join(ROOT, ORCAD_CHILD_ENTRY_POINTS.daemon) const DAEMON_OUT_FILE = join(OUT_DIR, 'daemon-entry.js') +// Why beside orcad.js: the hook server's OpenCode binder and the OpenCode history scanner +// start this worker from the module dir, since orcad has no Electron resources tree. +const FOREIGN_SQLITE_READER_ENTRY = join(ROOT, ORCAD_CHILD_ENTRY_POINTS.foreignSqliteReader) +const FOREIGN_SQLITE_READER_OUT_FILE = join(OUT_DIR, ORCAD_FOREIGN_SQLITE_READER_ENTRY) const OUT_FILE = join(OUT_DIR, 'orcad.js') const BUILD_TARGET = process.env.ORCAD_BUILD_TARGET if (!BUILD_TARGET) { @@ -205,6 +211,7 @@ function buildForkedChild(entryPoint, outfile) { const childResults = await Promise.all([ buildForkedChild(WATCHER_ENTRY, WATCHER_OUT_FILE), buildForkedChild(DAEMON_ENTRY, DAEMON_OUT_FILE), + buildForkedChild(FOREIGN_SQLITE_READER_ENTRY, FOREIGN_SQLITE_READER_OUT_FILE), ...['writer', 'backup'].map((role) => buildForkedChild( join(ROOT, ORCAD_CHILD_ENTRY_POINTS[role]), @@ -340,6 +347,16 @@ try { process.exitCode = 1 } +try { + smokeForeignSqliteReaderWorker(OUT_DIR) + if (nodeRuntimePath) { + smokeForeignSqliteReaderWorker(OUT_DIR, { runtimePath: nodeRuntimePath }) + } +} catch (error) { + console.error('[build-orcad] foreign SQLite reader worker check failed:', error) + process.exitCode = 1 +} + // Why a content hash and not ORCAD_VERSION alone: the remote install directory is keyed on // this string, so two different builds carrying one version would share a directory — and an // already-`.install-complete` dir is never re-uploaded. The deploy would silently run stale diff --git a/config/scripts/build-relay.mjs b/config/scripts/build-relay.mjs index 20dd1e0b17a3..83332aefa444 100644 --- a/config/scripts/build-relay.mjs +++ b/config/scripts/build-relay.mjs @@ -9,6 +9,7 @@ * gracefully degraded. */ import { build } from 'esbuild' +import { JSONC_PARSER_ESM_ALIAS } from '../build-plugins/jsonc-parser-esm.ts' import { createHash } from 'node:crypto' import { copyFileSync, @@ -60,7 +61,6 @@ const MANAGED_HOOK_RUNTIME_ENTRY = join( 'agent-hooks', 'managed-hook-runtime.ts' ) -const JSONC_PARSER_ESM_ENTRY = join(ROOT, 'node_modules', 'jsonc-parser', 'lib', 'esm', 'main.js') const NODE_PTY_CONSOLE_LIST_PATCH_FILENAME = 'node-pty-1.1.0-console-list-agent-patch.cjs' const NODE_PTY_CONSOLE_LIST_PATCH_SOURCE = join( ROOT, @@ -102,6 +102,7 @@ const RELAY_VERSION = '0.1.0' async function buildRelayBundles(outDir) { await build({ entryPoints: [RELAY_ENTRY], + alias: JSONC_PARSER_ESM_ALIAS, bundle: true, platform: 'node', target: 'node18', @@ -186,7 +187,7 @@ async function buildRelayBundles(outDir) { outfile: join(outDir, 'managed-hook-runtime.js'), // Why: jsonc-parser's default UMD build keeps relative dynamic requires // that break after bundling; its ESM entry is equivalent and self-contained. - alias: { 'jsonc-parser': JSONC_PARSER_ESM_ENTRY }, + alias: JSONC_PARSER_ESM_ALIAS, sourcemap: false, minify: true, define: { @@ -293,6 +294,7 @@ for (const platform of RELAY_BUILD_PLATFORMS) { mkdirSync(outDir, { recursive: true }) await build({ entryPoints: [wslHookEntry], + alias: JSONC_PARSER_ESM_ALIAS, bundle: true, platform: 'node', target: 'node18', diff --git a/config/scripts/build-windows-cli-launcher.mjs b/config/scripts/build-windows-cli-launcher.mjs index b93005600e9e..3d9d93aaed20 100644 --- a/config/scripts/build-windows-cli-launcher.mjs +++ b/config/scripts/build-windows-cli-launcher.mjs @@ -5,6 +5,7 @@ import { createHash } from 'node:crypto' import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { dirname, join, resolve } from 'node:path' import { pathToFileURL } from 'node:url' +import { findDynamicVcRuntimeImports, readPeImportedDllNames } from './windows-pe-imports.mjs' export function windowsCliLauncherFingerprint(inputPaths, version) { const hash = createHash('sha256').update(version) @@ -66,6 +67,7 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) join(crateRoot, 'build.rs'), manifestPath, join(crateRoot, 'app.manifest'), + join(crateRoot, '.cargo', 'config.toml'), iconPath, join(repoRoot, 'config/scripts/build-windows-cli-launcher.mjs') ], @@ -117,6 +119,16 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) process.exit(result.status ?? 1) } - copyFileSync(join(targetDirectory, 'release', 'orca.exe'), outputPath) + const builtPath = join(targetDirectory, 'release', 'orca.exe') + const vcRuntimeImports = findDynamicVcRuntimeImports( + readPeImportedDllNames(readFileSync(builtPath)) + ) + if (vcRuntimeImports.length > 0) { + // Why fatal: those DLLs ship with the Visual C++ Redistributable, so the CLI would fail to start on a clean Windows install. + throw new Error( + `orca.exe imports ${vcRuntimeImports.join(', ')}; the C runtime must be linked statically (native/windows-cli-launcher/.cargo/config.toml).` + ) + } + copyFileSync(builtPath, outputPath) writeFileSync(`${outputPath}.sha256`, fingerprint) } diff --git a/config/scripts/build-windows-cli-launcher.test.mjs b/config/scripts/build-windows-cli-launcher.test.mjs index e83b57f65e76..0e06cafd22eb 100644 --- a/config/scripts/build-windows-cli-launcher.test.mjs +++ b/config/scripts/build-windows-cli-launcher.test.mjs @@ -17,6 +17,7 @@ import { windowsCliLauncherFileVersion, windowsCliLauncherFingerprint } from './build-windows-cli-launcher.mjs' +import { findDynamicVcRuntimeImports, readPeImportedDllNames } from './windows-pe-imports.mjs' const itCrossHost = process.platform === 'win32' ? it.skip : it const projectRoot = resolve(import.meta.dirname, '../..') @@ -131,6 +132,9 @@ describe('Windows CLI launcher', () => { expect(info.ProductVersion).toBe(version) const binary = readFileSync(launcherPath) expect(binary.includes(Buffer.from('requestedExecutionLevel level="asInvoker"'))).toBe(true) + const imports = readPeImportedDllNames(binary) + expect(imports.map((name) => name.toLowerCase())).toContain('kernel32.dll') + expect(findDynamicVcRuntimeImports(imports)).toEqual([]) const icon = readFileSync(join(projectRoot, 'resources', 'build', 'icon.ico')) const imageSize = icon.readUInt32LE(14) const imageOffset = icon.readUInt32LE(18) diff --git a/config/scripts/check-changed-code-quality.mjs b/config/scripts/check-changed-code-quality.mjs index 71a512ad4070..9b213031aa7d 100644 --- a/config/scripts/check-changed-code-quality.mjs +++ b/config/scripts/check-changed-code-quality.mjs @@ -13,6 +13,9 @@ const CASTING_DISABLE_PATTERN = /\/[/*]\s*(?:oxlint|eslint)-disable(?:-next-line|-line)?\s[^\n]*typescript\/consistent-type-assertions/ const ANTI_SLOP_DISABLE_PATTERN = /\/[/*]\s*(?:oxlint|eslint)-disable(?:-next-line|-line)?\s[^\n]*\banti-slop\// +const REACT_DOCTOR_DISABLE_PATTERN = + /^\s*\/[/*]\s*(?:oxlint|eslint)-disable(?:-next-line|-line)?\s+react-doctor\/[\w-]+(?:\s*,\s*react-doctor\/[\w-]+)*\s*(?:--(?:(?!\*\/).)*)?(?:\*\/)?\s*$/ +const EXPLICIT_DISABLE_RULE_PATTERN = /(?:-disable(?:-next-line|-line)?\s+|^)[\w-]+(?:\/[\w-]+)?/ export const OXLINT_SCANS = [ { // Why: no --config, so Oxlint keeps discovering nested configs. Pinning the root @@ -41,7 +44,12 @@ export const OXLINT_SCANS = [ }, { label: 'React Doctor', - args: ['--config', 'config/oxlint-react-doctor.json'] + args: [ + '--config', + 'config/oxlint-react-doctor.json', + '--report-unused-disable-directives-severity', + 'warn' + ] }, { // Why changed-lines only: the renderer carries ~4.7k pre-existing restyle/raw-color @@ -250,6 +258,16 @@ export function collectBaseLineBlocks(root, comparisonBase, files = null) { } export function isMovedCode(highlightedLines, baseBlocks) { + return createMovedCodeMatcher(baseBlocks)(highlightedLines) +} + +export function createMovedCodeMatcher(baseBlocks) { + // Base-revision blocks stay fixed for the gate run; normalize each visited block once. + const normalizedBlocks = new Map() + return (highlightedLines) => matchMovedCode(highlightedLines, baseBlocks, normalizedBlocks) +} + +function matchMovedCode(highlightedLines, baseBlocks, normalizedBlocks) { const needle = highlightedLines.map(normalizeSourceLine).filter((line) => line !== '') if (needle.length === 0) { return false @@ -262,8 +280,12 @@ export function isMovedCode(highlightedLines, baseBlocks) { // and nearly all of it must be present. Genuinely new code shares neither the // anchor nor the ordering, so it stays reported. const MIN_COVERAGE = 0.9 - return baseBlocks.some((rawHaystack) => { - const haystack = rawHaystack.map(normalizeSourceLine).filter((line) => line !== '') + return baseBlocks.some((block) => { + let haystack = normalizedBlocks.get(block) + if (!haystack) { + haystack = block.map(normalizeSourceLine).filter((line) => line !== '') + normalizedBlocks.set(block, haystack) + } for (let start = 0; start < haystack.length; start += 1) { if (haystack[start] !== needle[0]) { continue @@ -301,7 +323,8 @@ export function diagnosticTouchesAddedLines( diagnostic, rangesByFile, root = process.cwd(), - baseBlocks = [] + baseBlocks = [], + movedCodeMatcher = isMovedCode ) { const file = normalizedDiagnosticPath(root, diagnostic.filename) const ranges = rangesByFile.get(file) @@ -313,7 +336,7 @@ export function diagnosticTouchesAddedLines( if (lineRange === null || !overlapsAddedLines(lineRange.start, lineRange.end, ranges)) { return false } - return !isMovedCode( + return !movedCodeMatcher( diagnosticHighlightedLines(root, diagnostic.filename, label.span), baseBlocks ) @@ -348,16 +371,34 @@ export function isCastingDirectiveUnusedWarning(diagnostic, root) { ) } -// Why: the anti-slop rules live in a JS plugin that only config/oxlint-anti-slop.json loads, so -// the root scan never sees those rule names and reports every anti-slop suppression as unused. -// `audit:anti-slop` is the scan that enforces them. -export function isAntiSlopDirectiveUnusedWarning(diagnostic, root) { +// Unloaded plugin directives are checked by their owning scan. +export function isUnloadedPluginDirectiveUnusedWarning(diagnostic, root, scanLabel) { if (!/^Unused (?:oxlint|eslint)-disable/.test(diagnostic.message ?? '')) { return false } + if (scanLabel === 'React Doctor') { + const labels = diagnostic.labels ?? [] + return ( + labels.length > 0 && + labels.every(({ span }) => { + if (span.offset === undefined || span.length === undefined) { + return false + } + const file = path.isAbsolute(diagnostic.filename) + ? diagnostic.filename + : path.join(root, diagnostic.filename) + // Oxlint spans use UTF-8 byte offsets, including before non-ASCII comments. + const directive = readFileSync(file) + .subarray(span.offset, span.offset + span.length) + .toString('utf8') + const rules = directive.split('--')[0] + return EXPLICIT_DISABLE_RULE_PATTERN.test(rules) && !/\breact-doctor\//.test(rules) + }) + ) + } return (diagnostic.labels ?? []).some((label) => - diagnosticHighlightedLines(root, diagnostic.filename, label.span).some((line) => - ANTI_SLOP_DISABLE_PATTERN.test(line) + diagnosticHighlightedLines(root, diagnostic.filename, label.span).some( + (line) => ANTI_SLOP_DISABLE_PATTERN.test(line) || REACT_DOCTOR_DISABLE_PATTERN.test(line) ) ) } @@ -429,6 +470,7 @@ export function main( } const baseBlocks = collectBaseLineBlocks(root, comparisonBase) + const movedCodeMatcher = createMovedCodeMatcher(baseBlocks) let failures = 0 for (const scan of OXLINT_SCANS) { @@ -436,8 +478,8 @@ export function main( (diagnostic) => !isSuppressedDiagnostic(diagnostic, root) && !isCastingDirectiveUnusedWarning(diagnostic, root) && - !isAntiSlopDirectiveUnusedWarning(diagnostic, root) && - diagnosticTouchesAddedLines(diagnostic, rangesByFile, root, baseBlocks) + !isUnloadedPluginDirectiveUnusedWarning(diagnostic, root, scan.label) && + diagnosticTouchesAddedLines(diagnostic, rangesByFile, root, baseBlocks, movedCodeMatcher) ) for (const diagnostic of diagnostics) { printDiagnostic(diagnostic, root) diff --git a/config/scripts/check-changed-code-quality.test.mjs b/config/scripts/check-changed-code-quality.test.mjs index e722acad6ef9..783ee140adcf 100644 --- a/config/scripts/check-changed-code-quality.test.mjs +++ b/config/scripts/check-changed-code-quality.test.mjs @@ -1,10 +1,12 @@ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' import path from 'node:path' import { describe, expect, it } from 'vitest' +import { runProcessSync } from '../../src/shared/child-process/run-process' +import { resolveOxlintInvocation } from './oxlint-cli-invocation.mjs' import { OXLINT_SCANS, diagnosticTouchesAddedLines, - isAntiSlopDirectiveUnusedWarning, + isUnloadedPluginDirectiveUnusedWarning, isMovedCode, isRootCodeQualityPath, overlapsAddedLines, @@ -124,7 +126,7 @@ describe('moved-code exemption', () => { }) }) -describe('anti-slop directive unused warning', () => { +describe('unloaded plugin directive unused warning', () => { const root = path.resolve(import.meta.dirname, '..', '..') // Assembled so no line here is itself a directive the gate would scan. const directive = (rule) => `/* oxlint-disable ${rule} -- reason */` @@ -146,21 +148,149 @@ describe('anti-slop directive unused warning', () => { it('exempts a suppression the root scan cannot resolve', () => { withFixture(directive('anti-slop/no-module-mocking'), (diagnostic) => { - expect(isAntiSlopDirectiveUnusedWarning(diagnostic, root)).toBe(true) + expect(isUnloadedPluginDirectiveUnusedWarning(diagnostic, root, 'code quality')).toBe(true) }) }) it('still reports an unused directive for a rule the root scan does load', () => { withFixture(directive('unicorn/no-array-reduce'), (diagnostic) => { - expect(isAntiSlopDirectiveUnusedWarning(diagnostic, root)).toBe(false) + expect(isUnloadedPluginDirectiveUnusedWarning(diagnostic, root, 'code quality')).toBe(false) }) }) it('ignores diagnostics that are not unused-directive warnings', () => { withFixture(directive('anti-slop/no-module-mocking'), (diagnostic) => { expect( - isAntiSlopDirectiveUnusedWarning({ ...diagnostic, message: 'Unexpected any.' }, root) + isUnloadedPluginDirectiveUnusedWarning( + { ...diagnostic, message: 'Unexpected any.' }, + root, + 'code quality' + ) ).toBe(false) }) }) + + function scanFixture(label, file) { + const scan = OXLINT_SCANS.find((candidate) => candidate.label === label) + if (!scan) { + throw new Error(`Missing ${label} scan`) + } + const { command, prefixArgs } = resolveOxlintInvocation(root) + const result = runProcessSync({ + program: command, + args: [...prefixArgs, ...scan.args, '--format', 'json', file], + cwd: root, + timeoutMs: 30_000, + maxOutputBytes: 4 * 1024 * 1024 + }) + return JSON.parse(result.stdout).diagnostics + } + + it('accepts a used Doctor directive only through its loaded scan', () => { + const source = [ + "import { useEffect, useState } from 'react'", + directive('react-doctor/no-derived-state-effect'), + 'export function Title({ title }: { title: string }) {', + " const [value, setValue] = useState('')", + ' useEffect(() => { setValue(title) }, [title])', + ' return value', + '}' + ].join('\n') + withFixture(source, ({ filename }) => { + const normal = scanFixture('code quality', filename) + const unused = normal.find((diagnostic) => diagnostic.message.startsWith('Unused ')) + expect(unused).toBeDefined() + expect(isUnloadedPluginDirectiveUnusedWarning(unused, root, 'code quality')).toBe(true) + expect(scanFixture('React Doctor', filename)).toEqual([]) + }) + }) + + it('keeps an unused Doctor directive failing in its loaded scan', () => { + withFixture(directive('react-doctor/no-derived-state-effect'), ({ filename }) => { + const diagnostics = scanFixture('React Doctor', filename) + expect(diagnostics).toHaveLength(1) + expect(diagnostics[0].message).toMatch(/^Unused /) + expect(isUnloadedPluginDirectiveUnusedWarning(diagnostics[0], root, 'React Doctor')).toBe( + false + ) + }) + }) + + it('does not hide unused native rules in a mixed directive', () => { + withFixture( + directive('react-doctor/no-derived-state-effect, unicorn/no-array-reduce'), + (diagnostic) => { + expect(isUnloadedPluginDirectiveUnusedWarning(diagnostic, root, 'code quality')).toBe(false) + } + ) + }) + + it('recognizes a standalone directive containing only Doctor rules', () => { + withFixture( + directive( + 'react-doctor/no-derived-state-effect, react-doctor/no-adjust-state-on-prop-change' + ), + (diagnostic) => { + expect(isUnloadedPluginDirectiveUnusedWarning(diagnostic, root, 'code quality')).toBe(true) + } + ) + }) + + it('keeps adjacent native directive warnings visible', () => { + const doctor = directive('react-doctor/no-derived-state-effect') + const native = directive('unicorn/no-array-reduce') + for (const source of [`${doctor} ${native}`, `${native} ${doctor}`]) { + withFixture(source, ({ filename }) => { + const diagnostic = scanFixture('code quality', filename).find((candidate) => + candidate.labels.some((label) => label.span.offset === source.indexOf(native)) + ) + expect(diagnostic).toBeDefined() + expect(diagnostic.message).toMatch(/^Unused /) + expect(isUnloadedPluginDirectiveUnusedWarning(diagnostic, root, 'code quality')).toBe(false) + }) + } + }) + + it('leaves used native directives to the scan that loads them', () => { + withFixture( + [ + 'export const banner = "λ"', + directive('typescript/no-explicit-any'), + 'export const answer: any = 42' + ].join('\n'), + ({ filename }) => { + expect(scanFixture('code quality', filename)).toEqual([]) + const diagnostics = scanFixture('React Doctor', filename) + expect(diagnostics).toHaveLength(1) + expect(isUnloadedPluginDirectiveUnusedWarning(diagnostics[0], root, 'React Doctor')).toBe( + true + ) + } + ) + }) + + it('does not exempt unused Doctor rules together with unloaded native rules', () => { + withFixture( + directive('react-doctor/no-derived-state-effect, typescript/no-explicit-any'), + ({ filename }) => { + const diagnostics = scanFixture('React Doctor', filename) + expect(diagnostics).toHaveLength(1) + expect(isUnloadedPluginDirectiveUnusedWarning(diagnostics[0], root, 'React Doctor')).toBe( + false + ) + } + ) + }) + + it('keeps blanket unused directives visible in the Doctor scan', () => { + for (const source of [directive(''), '// oxlint-disable-next-line -- reason']) { + withFixture(source, ({ filename }) => { + const diagnostics = scanFixture('React Doctor', filename) + expect(diagnostics).toHaveLength(1) + expect(isUnloadedPluginDirectiveUnusedWarning(diagnostics[0], root, 'React Doctor')).toBe( + false + ) + }) + } + }) }) diff --git a/config/scripts/check-react-doctor-changed.mjs b/config/scripts/check-react-doctor-changed.mjs index 743a64eee046..92a62b461f1b 100644 --- a/config/scripts/check-react-doctor-changed.mjs +++ b/config/scripts/check-react-doctor-changed.mjs @@ -26,7 +26,7 @@ const result = spawnSync( [ ...prefixArgs, 'dlx', - 'react-doctor@0.9.1', + 'react-doctor@0.9.14', '.', '--yes', '--scope', diff --git a/config/scripts/ci-background-step-barriers.test.mjs b/config/scripts/ci-background-step-barriers.test.mjs index d9dc6332a9f0..329e6c0bbcec 100644 --- a/config/scripts/ci-background-step-barriers.test.mjs +++ b/config/scripts/ci-background-step-barriers.test.mjs @@ -5,6 +5,7 @@ import { describe, expect, it } from 'vitest' const pr = parse(readFileSync('.github/workflows/pr.yml', 'utf8')) const mobile = parse(readFileSync('.github/workflows/mobile.yml', 'utf8')) const cloud = parse(readFileSync('.github/workflows/cloud-verify.yml', 'utf8')) +const headless = parse(readFileSync('.github/workflows/node-server-tests.yml', 'utf8')) function assertJoinedBefore(steps, id, consumer) { const start = steps.findIndex((step) => step.id === id) @@ -25,7 +26,8 @@ describe('CI background step barriers', () => { pr.jobs.package, pr.jobs.shell_contracts, mobile.jobs.verify, - cloud.jobs.security + cloud.jobs.security, + headless.jobs.persistence ]) { const pending = new Set() for (const step of job.steps) { @@ -56,6 +58,35 @@ describe('CI background step barriers', () => { ) }) + it('joins the Linux Bun build before requiring both headless runtime artifacts', () => { + const steps = headless.jobs.persistence.steps + const consumer = (step) => step.run?.startsWith('pnpm test:node-server --artifact ') + assertJoinedBefore(steps, 'bun-orcad', consumer) + const start = steps.findIndex((step) => step.id === 'bun-orcad') + const join = steps.findIndex((step) => step.wait === 'bun-orcad') + const install = steps.findIndex((step) => step.uses?.endsWith('/install-node-dependencies')) + const setup = steps.findIndex((step) => step.uses?.startsWith('oven-sh/setup-bun@')) + expect(install).toBeGreaterThanOrEqual(0) + expect(setup).toBeGreaterThanOrEqual(0) + expect(install).toBeLessThan(setup) + expect(setup).toBeLessThan(start) + expect(steps[setup].if).toBe("runner.os == 'Linux'") + expect(steps[start].if).toBeUndefined() + expect(steps[start].run).toContain('if [ "$RUNNER_OS" != Linux ]; then exit 0; fi') + for (const build of [ + steps.findIndex((step) => step.uses?.endsWith('/prepare-orcad-prebuilds')), + steps.findIndex((step) => step.run === 'pnpm build:orcad') + ]) { + expect(build).toBeGreaterThan(start) + expect(build).toBeLessThan(join) + expect(steps[build].background).toBeUndefined() + } + const test = steps.find(consumer) + expect(test.run).toContain("${{ runner.os == 'Linux' && '--cross-runtime' || '' }}") + expect(test.env.ORCA_BUN_ORCAD_SLOT).toBe('${{ steps.bun-orcad.outputs.slot }}') + expect(test.env.BUN_EXECUTABLE).toBe('${{ steps.bun-orcad.outputs.executable }}') + }) + it('finishes native import-cycle analysis before mobile installation changes resolution', () => { const steps = pr.jobs.static_analysis.steps assertJoinedBefore(steps, 'native-code-quality', (step) => @@ -66,13 +97,13 @@ describe('CI background step barriers', () => { expect(steps.findIndex((step) => step.id === 'changed-code-quality')).toBeGreaterThan(install) }) - it('finishes both mobile typechecks before allocating test workers', () => { + it('serializes mobile pnpm entrypoints before allocating test workers', () => { const steps = mobile.jobs.verify.steps assertJoinedBefore(steps, 'production-types', (step) => step.name === 'Test') const ratchet = steps.findIndex((step) => step.name === 'Typecheck tests (ratchet)') const join = steps.findIndex((step) => step.wait === 'production-types') expect(steps[ratchet].background).toBeUndefined() - expect(ratchet).toBeLessThan(join) + expect(ratchet).toBeGreaterThan(join) }) it('waits for WebKit and the bundle before any browser tests', () => { diff --git a/config/scripts/ci-cache-warmup-workflow.test.mjs b/config/scripts/ci-cache-warmup-workflow.test.mjs index 74b15e9b2673..469800edfacd 100644 --- a/config/scripts/ci-cache-warmup-workflow.test.mjs +++ b/config/scripts/ci-cache-warmup-workflow.test.mjs @@ -42,6 +42,7 @@ it('populates shared Electron archives on both Linux architectures without chang ) expect(install.with['native-runtime']).toBe('node') expect(install.with['cache-electron-package']).toBe('true') + expect(install.with['cache-pnpm-store-lookup-only']).toBe('true') const populate = steps.find((step) => step.name === 'Populate shared Electron archive') expect(populate.run).toBe('node config/scripts/install-electron-package-binary.mjs') expect(steps.indexOf(populate)).toBeGreaterThan(steps.indexOf(install)) @@ -72,12 +73,12 @@ it('bounds warming to the required platforms and validates changes without grant expect(workflow.permissions).toEqual({ contents: 'read' }) expect(workflow.on.push.branches).toEqual(['main']) expect(workflow.on.push.paths).toContain('.github/actions/prepare-native-runtime/**') - expect(workflow.on.schedule).toEqual([{ cron: '41 * * * *' }]) + expect(workflow.on.schedule).toEqual([{ cron: '41 */6 * * *' }]) expect(workflow.on.pull_request.paths).toContain('.github/workflows/ci-cache-warmup.yml') expect(steps[0].with['persist-credentials']).toBe(false) }) -it('lets hourly warmers wait while pushes, PR updates, and manual runs can replace active work', () => { +it('lets scheduled warmers wait while pushes, PR updates, and manual runs can replace active work', () => { expect(workflow.concurrency).toEqual({ group: 'ci-cache-warmup-${{ github.event.pull_request.number || github.ref }}', 'cancel-in-progress': "${{ github.event_name != 'schedule' }}" @@ -97,6 +98,9 @@ it('warms and probes both Windows images with the persistence job runtime', () = const install = job.steps.find( (step) => step.uses === './.github/actions/install-node-dependencies' ) - expect(install.with).toEqual({ 'native-runtime': 'node' }) + expect(install.with).toEqual({ + 'native-runtime': 'node', + 'cache-pnpm-store-lookup-only': 'true' + }) expect(job.steps.at(-1).run).toBe('node config/scripts/ensure-native-runtime.mjs --check-only') }) diff --git a/config/scripts/ci-dependency-download-cache.test.mjs b/config/scripts/ci-dependency-download-cache.test.mjs index 584c37f6740f..f04aff20b352 100644 --- a/config/scripts/ci-dependency-download-cache.test.mjs +++ b/config/scripts/ci-dependency-download-cache.test.mjs @@ -11,7 +11,9 @@ describe('CI dependency download caches', () => { it('scopes desktop stores to the root lockfile and lets mixed installs opt in', () => { expect(action.inputs['cache-dependency-path'].default).toBe('pnpm-lock.yaml') for (const step of action.runs.steps.filter((step) => step.uses === 'actions/setup-node@v6')) { - expect(step.with.cache).toBe("${{ github.event_name != 'pull_request' && 'pnpm' || '' }}") + expect(step.with.cache).toBe( + "${{ github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && steps.pnpm-store-mode.outputs.lookup-only != 'true' && 'pnpm' || '' }}" + ) expect(step.with['cache-dependency-path']).toBe('${{ inputs.cache-dependency-path }}') expect(step.with['package-manager-cache']).toBe(false) } @@ -30,15 +32,17 @@ describe('CI dependency download caches', () => { ]) }) - it('restores PR stores except measured Windows mixed installs, without a post-job save', () => { + it('restores PR stores except measured Windows, Linux and macOS installs, without a post-job save', () => { const resolve = action.runs.steps.find((step) => step.id === 'pnpm-store') const restore = action.runs.steps.find( (step) => step.name === 'Restore pnpm download store without saving' ) + expect(restore.if).toBe( + "github.event_name == 'pull_request' && inputs.cache-pnpm-store != 'false' && !((runner.os == 'Linux' || runner.os == 'macOS') && (runner.arch == 'X64' || runner.arch == 'ARM64') && inputs.cache-dependency-path == 'pnpm-lock.yaml') && (runner.os != 'Windows' || !(runner.arch == 'X64' && contains(inputs.cache-dependency-path, 'mobile/pnpm-lock.yaml')) && !((runner.arch == 'X64' || runner.arch == 'ARM64') && inputs.cache-dependency-path == 'pnpm-lock.yaml'))" + ) expect(resolve.if).toBe( - "github.event_name == 'pull_request' && (runner.os != 'Windows' || runner.arch != 'X64' || !contains(inputs.cache-dependency-path, 'mobile/pnpm-lock.yaml'))" + `${restore.if} || (github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && steps.pnpm-store-mode.outputs.lookup-only == 'true')` ) - expect(restore.if).toBe(resolve.if) expect(restore.uses).toBe('actions/cache/restore@v5') expect(restore.with.path).toBe('${{ steps.pnpm-store.outputs.path }}') expect(restore.with.key).toBe( @@ -65,39 +69,221 @@ describe('CI dependency download caches', () => { ]) }) + it('keeps producer lookup optional and compatible with the existing store archive', () => { + const lookup = action.runs.steps.find((step) => step.id === 'pnpm-store-lookup') + const restore = action.runs.steps.find((step) => step.id === 'pnpm-store-restore') + expect(action.inputs['cache-pnpm-store-lookup-only'].default).toBe('auto') + expect(lookup.uses).toBe('actions/cache@v5') + expect(lookup.if).toBe("steps.pnpm-store-mode.outputs.lookup-only == 'true'") + expect(lookup.with).toEqual({ + path: '${{ env.ORCA_PNPM_STORE_CACHE_PATH }}', + key: restore.with.key, + 'lookup-only': true + }) + expect(action.runs.steps.indexOf(lookup)).toBeLessThan( + action.runs.steps.findIndex((step) => step.name === 'Install dependencies') + ) + expect(action.outputs['pnpm-store-cache-hit'].value).toBe( + '${{ steps.pnpm-store-lookup.outputs.cache-hit || steps.pnpm-store-restore.outputs.cache-hit || steps.requested-node.outputs.cache-hit || steps.default-node.outputs.cache-hit }}' + ) + }) + it.each([ ['Windows x64 mixed PR', 'pull_request', 'Windows', 'X64', true, false, ''], ['Windows ARM64 mixed PR', 'pull_request', 'Windows', 'ARM64', true, true, ''], ['Windows x86 mixed PR', 'pull_request', 'Windows', 'X86', true, true, ''], - ['Windows x64 root-only PR', 'pull_request', 'Windows', 'X64', false, true, ''], + ['Windows x64 root-only PR', 'pull_request', 'Windows', 'X64', false, false, ''], + ['Windows ARM64 root-only PR', 'pull_request', 'Windows', 'ARM64', false, false, ''], + ['Windows x86 root-only PR', 'pull_request', 'Windows', 'X86', false, true, ''], + ['Windows x64 custom PR', 'pull_request', 'Windows', 'X64', 'cloud/pnpm-lock.yaml', true, ''], + [ + 'Windows ARM64 custom PR', + 'pull_request', + 'Windows', + 'ARM64', + 'cloud/pnpm-lock.yaml', + true, + '' + ], + ['Windows ARM64 root-only push', 'push', 'Windows', 'ARM64', false, false, 'pnpm'], + [ + 'Windows ARM64 root-only manual run', + 'workflow_dispatch', + 'Windows', + 'ARM64', + false, + false, + 'pnpm' + ], + ['Explicit Linux PR opt-out', 'pull_request', 'Linux', 'X64', false, false, '', 'false'], + ['Explicit Windows push opt-out', 'push', 'Windows', 'ARM64', false, false, '', 'false'], + [ + 'Explicit Windows manual opt-out', + 'workflow_dispatch', + 'Windows', + 'X64', + false, + false, + '', + 'false' + ], + [ + 'Explicit custom-store opt-out', + 'pull_request', + 'Windows', + 'X64', + 'cloud/pnpm-lock.yaml', + false, + '', + 'false' + ], + ['Windows x64 root-only push', 'push', 'Windows', 'X64', false, false, 'pnpm'], + ['Linux x64 root-only PR', 'pull_request', 'Linux', 'X64', false, false, ''], + ['Linux ARM64 root-only PR', 'pull_request', 'Linux', 'ARM64', false, false, ''], + ['Linux x86 root-only PR', 'pull_request', 'Linux', 'X86', false, true, ''], + ['Linux ARM root-only PR', 'pull_request', 'Linux', 'ARM', false, true, ''], + ['Linux x64 custom PR', 'pull_request', 'Linux', 'X64', 'cloud/pnpm-lock.yaml', true, ''], + ['Linux x64 root-only push', 'push', 'Linux', 'X64', false, false, 'pnpm'], + ['Linux ARM64 root-only manual', 'workflow_dispatch', 'Linux', 'ARM64', false, false, 'pnpm'], + ['macOS x64 root-only PR', 'pull_request', 'macOS', 'X64', false, false, ''], + ['macOS ARM64 root-only PR', 'pull_request', 'macOS', 'ARM64', false, false, ''], + ['macOS x86 root-only PR', 'pull_request', 'macOS', 'X86', false, true, ''], + ['macOS x64 mixed PR', 'pull_request', 'macOS', 'X64', true, true, ''], + ['macOS ARM64 mixed PR', 'pull_request', 'macOS', 'ARM64', true, true, ''], + ['macOS ARM64 custom PR', 'pull_request', 'macOS', 'ARM64', 'cloud/pnpm-lock.yaml', true, ''], + ['macOS ARM64 opted-out PR', 'pull_request', 'macOS', 'ARM64', true, false, '', 'false'], + ['macOS x64 root-only push', 'push', 'macOS', 'X64', false, false, 'pnpm'], + ['macOS ARM64 root-only manual', 'workflow_dispatch', 'macOS', 'ARM64', false, false, 'pnpm'], ['Linux x64 mixed PR', 'pull_request', 'Linux', 'X64', true, true, ''], ['Linux ARM64 mixed PR', 'pull_request', 'Linux', 'ARM64', true, true, ''], - ['macOS ARM64 mixed PR', 'pull_request', 'macOS', 'ARM64', true, true, ''], ['Windows x64 mixed push', 'push', 'Windows', 'X64', true, false, 'pnpm'], - ['Windows x64 mixed manual run', 'workflow_dispatch', 'Windows', 'X64', true, false, 'pnpm'] - ])('%s keeps its scoped store policy', (_name, event, os, arch, mixed, restore, cache) => { - const context = { - github: { event_name: event }, - runner: { os, arch }, - inputs: { - 'cache-dependency-path': mixed ? 'pnpm-lock.yaml\nmobile/pnpm-lock.yaml' : 'pnpm-lock.yaml' - }, - contains: (value, search) => value.toLowerCase().includes(search.toLowerCase()) - } - const evaluate = (expression) => - runInNewContext( - expression.replaceAll('inputs.cache-dependency-path', 'inputs["cache-dependency-path"]'), - context + ['Windows x64 mixed manual run', 'workflow_dispatch', 'Windows', 'X64', true, false, 'pnpm'], + ['Windows x64 lookup producer', 'push', 'Windows', 'X64', false, false, '', 'true', 'true'], + [ + 'Windows ARM64 lookup producer', + 'schedule', + 'Windows', + 'ARM64', + false, + false, + '', + 'true', + 'true' + ], + ['macOS ARM64 lookup producer', 'push', 'macOS', 'ARM64', false, false, '', 'true', 'true'], + [ + 'Linux x64 lookup producer', + 'workflow_dispatch', + 'Linux', + 'X64', + false, + false, + '', + 'true', + 'true' + ], + ['Opted-out lookup producer', 'push', 'Windows', 'ARM64', false, false, '', 'false', 'true'], + [ + 'macOS root PR lookup flag', + 'pull_request', + 'macOS', + 'ARM64', + false, + false, + '', + 'true', + 'true' + ], + ['macOS mixed PR lookup flag', 'pull_request', 'macOS', 'ARM64', true, true, '', 'true', 'true'] + ])( + '%s keeps its scoped store policy', + (_name, event, os, arch, mixed, restore, cache, storeCache = 'true', lookupOnly = 'false') => { + const context = { + github: { event_name: event }, + runner: { os, arch }, + steps: { + 'pnpm-store-mode': { + outputs: { + 'lookup-only': + event !== 'pull_request' && storeCache !== 'false' && lookupOnly === 'true' + ? 'true' + : '' + } + } + }, + inputs: { + 'cache-pnpm-store': storeCache, + 'cache-pnpm-store-lookup-only': lookupOnly, + 'cache-dependency-path': + typeof mixed === 'string' + ? mixed + : mixed + ? 'pnpm-lock.yaml\nmobile/pnpm-lock.yaml' + : 'pnpm-lock.yaml' + }, + contains: (value, search) => value.toLowerCase().includes(search.toLowerCase()) + } + const evaluate = (expression) => + runInNewContext( + expression + .replaceAll( + 'steps.pnpm-store-mode.outputs.lookup-only', + 'steps["pnpm-store-mode"].outputs["lookup-only"]' + ) + .replaceAll( + 'inputs.cache-pnpm-store-lookup-only', + 'inputs["cache-pnpm-store-lookup-only"]' + ) + .replaceAll('inputs.cache-dependency-path', 'inputs["cache-dependency-path"]') + .replaceAll('inputs.cache-pnpm-store', 'inputs["cache-pnpm-store"]'), + context + ) + for (const step of action.runs.steps.filter( + (step) => + step.id === 'pnpm-store' || step.name === 'Restore pnpm download store without saving' + )) { + expect(evaluate(step.if)).toBe( + restore || + (step.id === 'pnpm-store' && + event !== 'pull_request' && + storeCache !== 'false' && + lookupOnly === 'true') + ) + } + expect(evaluate(action.runs.steps.find((step) => step.id === 'pnpm-store-lookup').if)).toBe( + event !== 'pull_request' && storeCache !== 'false' && lookupOnly === 'true' ) - for (const step of action.runs.steps.filter( - (step) => - step.id === 'pnpm-store' || step.name === 'Restore pnpm download store without saving' - )) { - expect(evaluate(step.if)).toBe(restore) + for (const step of action.runs.steps.filter( + (step) => step.uses === 'actions/setup-node@v6' + )) { + expect(evaluate(step.with.cache.slice(3, -2))).toBe(cache) + expect(step.with['package-manager-cache']).toBe(false) + } } - for (const step of action.runs.steps.filter((step) => step.uses === 'actions/setup-node@v6')) { - expect(evaluate(step.with.cache.slice(3, -2))).toBe(cache) - expect(step.with['package-manager-cache']).toBe(false) + ) + + it('opts Windows server consumers out while preserving the main warmer store writer', () => { + const installer = './.github/actions/install-node-dependencies' + const persistence = workflow('node-server-tests').jobs.persistence.steps.find( + (step) => step.uses === installer + ) + const ssh = workflow('ssh-windows-hosts').jobs.hosts.steps.find( + (step) => step.uses === installer + ) + const warmer = workflow('ci-cache-warmup').jobs['warm-windows'].steps.find( + (step) => step.uses === installer + ) + expect(action.inputs['cache-pnpm-store'].default).toBe('true') + expect(persistence.with['cache-pnpm-store']).toBe("${{ runner.os != 'Windows' }}") + expect(ssh.with['cache-pnpm-store']).toBe('false') + expect(warmer.with['cache-pnpm-store']).toBeUndefined() + expect(warmer.with['cache-pnpm-store-lookup-only']).toBe('true') + expect(persistence.with['cache-pnpm-store-lookup-only']).toBe('true') + for (const name of ['warm', 'warm-linux-arm']) { + const install = workflow('ci-cache-warmup').jobs[name].steps.find((step) => + step.uses?.includes('install-node-dependencies') + ) + expect(install.with['cache-pnpm-store-lookup-only']).toBe('true') } }) diff --git a/config/scripts/ci-native-cache.test.mjs b/config/scripts/ci-native-cache.test.mjs index 6851ed62f9ba..045cc5de6d82 100644 --- a/config/scripts/ci-native-cache.test.mjs +++ b/config/scripts/ci-native-cache.test.mjs @@ -123,8 +123,8 @@ describe('CI native cache ownership', () => { installerPath, Buffer.from( readFileSync(installerPath, 'utf8').replace( - "inputs.cache-pnpm-verification == 'true'", - "inputs.cache-pnpm-verification == 'false'" + 'enabled: ${{ inputs.cache-pnpm-verification }}', + "enabled: 'false'" ) ) ) diff --git a/config/scripts/ci-pnpm-store-mode.test.mjs b/config/scripts/ci-pnpm-store-mode.test.mjs new file mode 100644 index 000000000000..d8402612b04e --- /dev/null +++ b/config/scripts/ci-pnpm-store-mode.test.mjs @@ -0,0 +1,150 @@ +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { runInNewContext } from 'node:vm' +import { parse } from 'yaml' +import { describe, expect, it } from 'vitest' +import { runProcessSync } from './script-child-process.mjs' + +const action = parse(readFileSync('.github/actions/install-node-dependencies/action.yml', 'utf8')) +const mode = action.runs.steps.find((step) => step.id === 'pnpm-store-mode') +const defaultContext = { + github: { event_name: 'push' }, + runner: { os: 'Linux', arch: 'X64', environment: 'github-hosted' }, + job: { container: { id: '' } }, + inputs: { + 'cache-pnpm-store': 'true', + 'cache-pnpm-store-lookup-only': 'auto', + 'cache-dependency-path': 'pnpm-lock.yaml', + 'node-version': '' + } +} +const expression = mode.if.replaceAll(/inputs\.([\w-]+)/g, 'inputs["$1"]') + +function eligible(changes) { + const context = structuredClone(defaultContext) + for (const [name, fields] of Object.entries(changes)) { + Object.assign(context[name], fields) + } + return runInNewContext(expression, context) +} + +function resolveMode(request, node, manager) { + const directory = mkdtempSync(join(tmpdir(), 'orca-store-mode-')) + const output = join(directory, 'output') + try { + writeFileSync( + join(directory, 'package.json'), + JSON.stringify({ engines: { node }, packageManager: manager }) + ) + const result = runProcessSync({ + program: 'bash', + args: ['-e', '-o', 'pipefail', '-c', mode.run], + cwd: directory, + env: { ...process.env, LOOKUP_REQUEST: request, GITHUB_OUTPUT: output } + }) + expect(result.code, result.stderr || result.stdout).toBe(0) + return readFileSync(output, 'utf8') + } finally { + rmSync(directory, { recursive: true, force: true }) + } +} + +describe('automatic pnpm store mode', () => { + it.each([ + ['auto', '24', 'pnpm@12.8.1', '', true], + ['auto', '25', 'pnpm@12.8.1', 'pnpm', false], + ['auto', '24', 'pnpm@13.0.0', 'pnpm', false], + ['true', '25', 'pnpm@13.0.0', '', true] + ])( + 'routes resolved %s mode for Node %s / %s into both cache steps', + (request, node, manager, cache, lookup) => { + const context = structuredClone(defaultContext) + context.inputs['cache-pnpm-store-lookup-only'] = request + const resolved = resolveMode(request, node, manager).split('=')[1].trim() + const evaluate = (value) => + runInNewContext( + value + .replaceAll(/inputs\.([\w-]+)/g, 'inputs["$1"]') + .replaceAll( + 'steps.pnpm-store-mode.outputs.lookup-only', + 'steps["pnpm-store-mode"].outputs["lookup-only"]' + ), + { ...context, steps: { 'pnpm-store-mode': { outputs: { 'lookup-only': resolved } } } } + ) + const nodeSetup = action.runs.steps.find((step) => step.id === 'default-node') + expect(evaluate(nodeSetup.with.cache.slice(3, -2))).toBe(cache) + expect(evaluate(action.runs.steps.find((step) => step.id === 'pnpm-store-lookup').if)).toBe( + lookup + ) + } + ) + + it.each( + ['Linux', 'Windows', 'macOS'].flatMap((os) => ['X64', 'ARM64'].map((arch) => [os, arch])) + )('qualifies the measured %s/%s hosted root context', (os, arch) => { + expect(eligible({ runner: { os, arch } })).toBe(true) + }) + + it.each([ + ['PR', { github: { event_name: 'pull_request' } }], + ['opted-out store', { inputs: { 'cache-pnpm-store': 'false' } }], + ['opted-out lookup', { inputs: { 'cache-pnpm-store-lookup-only': 'false' } }], + ['unknown request', { inputs: { 'cache-pnpm-store-lookup-only': 'other' } }], + [ + 'mixed lockfiles', + { inputs: { 'cache-dependency-path': 'pnpm-lock.yaml\nmobile/pnpm-lock.yaml' } } + ], + ['custom lockfile', { inputs: { 'cache-dependency-path': 'cloud/pnpm-lock.yaml' } }], + ['Node 25', { inputs: { 'node-version': '25' } }], + ['job container', { job: { container: { id: 'container-id' } } }], + ['self-hosted runner', { runner: { environment: 'self-hosted' } }], + ['unknown host kind', { runner: { environment: '' } }], + ['unmeasured architecture', { runner: { arch: 'X86' } }], + ['unmeasured OS', { runner: { os: 'other' } }] + ])('retains the legacy policy for %s', (_name, changes) => { + expect(eligible(changes)).toBe(false) + }) + + it('allows an explicit request to preserve the existing force-lookup contract', () => { + expect( + eligible({ + inputs: { + 'cache-pnpm-store-lookup-only': 'true', + 'node-version': '25', + 'cache-dependency-path': 'custom-lock.yaml' + }, + runner: { environment: 'self-hosted' }, + job: { container: { id: 'container-id' } } + }) + ).toBe(true) + expect( + eligible({ + github: { event_name: 'pull_request' }, + inputs: { 'cache-pnpm-store-lookup-only': 'true' } + }) + ).toBe(false) + }) + + it.each([ + ['24', 'pnpm@12.8.1', 'true'], + ['24', 'pnpm@12.8.1+sha512.fixture', 'true'], + ['25', 'pnpm@12.8.1', 'false'], + ['24.x', 'pnpm@12.8.1', 'false'], + ['24', 'pnpm@12.8.2', 'false'], + ['24', 'pnpm@12.8.10', 'false'], + ['24', undefined, 'false'], + [undefined, 'pnpm@12.8.1', 'false'], + ['24', 12, 'false'] + ])('checks manifest Node %s and manager %s before choosing lookup', (node, manager, expected) => { + expect(resolveMode('auto', node, manager)).toBe(`lookup-only=${expected}\n`) + }) + + it('checks uppercase auto requests consistently with GitHub expression comparisons', () => { + expect(resolveMode('AUTO', '25', 'pnpm@12.8.1')).toBe('lookup-only=false\n') + }) + + it('does not constrain an explicit request to the automatic manifest profile', () => { + expect(resolveMode('true', '25', 'pnpm@13.0.0')).toBe('lookup-only=true\n') + }) +}) diff --git a/config/scripts/ci-pnpm-verification-cache.test.mjs b/config/scripts/ci-pnpm-verification-cache.test.mjs index c64d019d06cb..b878b3ef0461 100644 --- a/config/scripts/ci-pnpm-verification-cache.test.mjs +++ b/config/scripts/ci-pnpm-verification-cache.test.mjs @@ -1,21 +1,116 @@ -import { readFileSync } from 'node:fs' +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' import { runInNewContext } from 'node:vm' import { describe, expect, it } from 'vitest' import { parse } from 'yaml' +import { classifyPrJobs, PR_CHECK_JOBS } from './pr-code-change-scope.mjs' +import { runProcessSync } from './script-child-process.mjs' const action = parse(readFileSync('.github/actions/install-node-dependencies/action.yml', 'utf8')) const steps = action.runs.steps -const resolve = steps.find((step) => step.id === 'verification-cache') -const restore = steps.find((step) => step.id === 'verification-cache-restore') +const cache = steps.find((step) => step.id === 'verification-cache') +const cacheAction = parse( + readFileSync('.github/actions/restore-pnpm-verification/action.yml', 'utf8') +) +const resolve = cacheAction.runs.steps.find((step) => step.id === 'verification-cache') +const restore = cacheAction.runs.steps.find((step) => step.id === 'verification-cache-restore') const install = steps.find((step) => step.name === 'Install dependencies') const save = steps.find((step) => step.name === 'Save pnpm verification record on main') -const evaluate = (expression, context) => - runInNewContext( - expression.replaceAll('inputs.cache-pnpm-verification', 'inputs["cache-pnpm-verification"]'), - context - ) +const evaluate = (expression, context) => runInNewContext(expression, context) + +function resolveRecord({ + container = false, + os = 'Linux', + packageManager = 'pnpm@12.8.1+sha512.test' +} = {}) { + const root = mkdtempSync(join(tmpdir(), 'orca-verification-record-')) + try { + const output = join(root, 'outputs') + const calls = join(root, 'pnpm-calls') + const cacheRoot = join(root, 'cache with spaces') + const cachePath = join(cacheRoot, 'pnpm') + writeFileSync(join(root, 'package.json'), JSON.stringify({ packageManager })) + const execution = runProcessSync({ + program: 'bash', + args: [ + '-e', + '-o', + 'pipefail', + '-c', + ` + pnpm() { + printf '%s\\n' "$*" >> "$TEST_CALLS" + case "$*" in + 'cache path') printf '%s\\n' "$TEST_CACHE_PATH" ;; + '--version') printf '%s\\n' '12.8.1' ;; + *) return 2 ;; + esac + } + ${resolve.run} + ` + ], + cwd: root, + env: { + ...process.env, + RUNNER_OS: os, + RUNNER_ARCH: 'ARM64', + POLICY_HASH: 'policy-digest', + GITHUB_OUTPUT: output, + TEST_CALLS: calls, + TEST_CACHE_PATH: cachePath, + CONTAINER_TOOLCHAIN: String(container), + XDG_CACHE_HOME: cacheRoot + } + }) + return { + code: execution.code, + output: existsSync(output) ? readFileSync(output, 'utf8').replaceAll(root, '') : '', + calls: existsSync(calls) ? readFileSync(calls, 'utf8') : '' + } + } finally { + rmSync(root, { recursive: true, force: true }) + } +} describe('pnpm-owned verification record', () => { + it('qualifies every shared-installer consumer when verification restoration changes', () => { + const result = classifyPrJobs(['.github/actions/restore-pnpm-verification/action.yml']) + for (const job of PR_CHECK_JOBS) { + expect(result[job], job).toBe(true) + } + }) + + it.skipIf(process.platform === 'win32')( + 'shares the exact key and archive path with containers without invoking host pnpm', + () => { + const host = resolveRecord() + const container = resolveRecord({ container: true }) + expect(host.code).toBe(0) + expect(container.code).toBe(0) + expect(container.output).toBe(host.output) + expect(container.output).toContain( + 'path=/cache with spaces/pnpm/lockfile-verified.jsonl' + ) + expect(container.output).toContain( + 'key=pnpm-verification-v1-Linux-ARM64-12.8.1-policy-digest' + ) + expect(host.calls).toBe('cache path\n--version\n') + expect(container.calls).toBe('') + } + ) + + it.skipIf(process.platform === 'win32').each([ + { container: true, os: 'Windows' }, + { container: true, os: 'macOS' }, + { container: true, packageManager: 'npm@12.8.1' } + ])('rejects an unsupported container identity: %j', (options) => { + const result = resolveRecord(options) + expect(result.code).not.toBe(0) + expect(result.output).toBe('') + expect(result.calls).toBe('') + }) + it.each([ ['Linux', 'X64', true], ['Linux', 'ARM64', true], @@ -31,7 +126,7 @@ describe('pnpm-owned verification record', () => { expect( evaluate(resolve.if, { runner: { os, arch }, - inputs: { 'cache-pnpm-verification': enabled } + inputs: { enabled } }) ).toBe(expected && enabled === 'true') } @@ -44,12 +139,15 @@ describe('pnpm-owned verification record', () => { expect(resolve.run).toContain('"$(pnpm cache path)"') expect(resolve.run).toContain('lockfile-verified.jsonl') expect(resolve.run).toContain('pnpm-verification-v1-%s-%s-%s-%s') - expect(resolve.run).toContain('"$RUNNER_OS" "$RUNNER_ARCH" "$(pnpm --version)" "$POLICY_HASH"') + expect(resolve.run).toContain('pnpm_version="$(pnpm --version)"') + expect(resolve.run).toContain('"$RUNNER_OS" "$RUNNER_ARCH" "$pnpm_version" "$POLICY_HASH"') + expect(cache.uses).toBe('./.github/actions/restore-pnpm-verification') + expect(cache.with.enabled).toBe('${{ inputs.cache-pnpm-verification }}') expect(restore.uses).toBe('actions/cache/restore@v5') expect(restore.with.path).toBe('${{ steps.verification-cache.outputs.path }}') expect(restore.with['restore-keys']).toBeUndefined() expect(restore['continue-on-error']).toBe(true) - expect(steps.indexOf(restore)).toBeLessThan(steps.indexOf(install)) + expect(steps.indexOf(cache)).toBeLessThan(steps.indexOf(install)) expect(install.if).toBeUndefined() expect(install.run).toContain('pnpm install --frozen-lockfile --ignore-scripts') }) @@ -64,21 +162,20 @@ describe('pnpm-owned verification record', () => { const context = { github: { event_name: event, ref }, steps: { - 'verification-cache': { outputs: { key: 'a-key' } }, - 'verification-cache-restore': { outputs: { 'cache-hit': 'false' } } + 'verification-cache': { outputs: { key: 'a-key', 'cache-hit': 'false' } } } } const expression = save.if .replaceAll( - 'steps.verification-cache-restore.outputs.cache-hit', - 'steps["verification-cache-restore"].outputs["cache-hit"]' + 'steps.verification-cache.outputs.cache-hit', + 'steps["verification-cache"].outputs["cache-hit"]' ) .replaceAll('steps.verification-cache.outputs.key', 'steps["verification-cache"].outputs.key') expect(evaluate(expression, context)).toBe(expected) context.steps['verification-cache'].outputs.key = '' expect(evaluate(expression, context)).toBe(false) context.steps['verification-cache'].outputs.key = 'a-key' - context.steps['verification-cache-restore'].outputs['cache-hit'] = 'true' + context.steps['verification-cache'].outputs['cache-hit'] = 'true' expect(evaluate(expression, context)).toBe(false) expect(save.uses).toBe('actions/cache/save@v5') expect(steps.indexOf(save)).toBeGreaterThan(steps.indexOf(install)) diff --git a/config/scripts/ci-shard-timings.json b/config/scripts/ci-shard-timings.json index 7a3a3710d4eb..73d0226943b8 100644 --- a/config/scripts/ci-shard-timings.json +++ b/config/scripts/ci-shard-timings.json @@ -626,7 +626,6 @@ "src/main/ai-vault/session-scanner-agent-root-overrides.test.ts": 1121, "src/main/ai-vault/session-scanner-antigravity-parser.test.ts": 72, "src/main/ai-vault/session-scanner-antigravity-source.test.ts": 330, - "src/main/ai-vault/session-scanner-background.test.ts": 354, "src/main/ai-vault/session-scanner-claude-cwd-drift.test.ts": 245, "src/main/ai-vault/session-scanner-claude-subagent-prune.test.ts": 170, "src/main/ai-vault/session-scanner-claude-subagents.test.ts": 137, @@ -706,7 +705,6 @@ "src/main/ai-vault/session-scanner-timeline.test.ts": 75, "src/main/ai-vault/session-scanner-unlimited-dedup.test.ts": 3562, "src/main/ai-vault/session-scanner-values.test.ts": 92, - "src/main/ai-vault/session-scanner-worker-client.test.ts": 48, "src/main/ai-vault/session-scanner.test.ts": 343, "src/main/ai-vault/session-sidecar-stat.test.ts": 25, "src/main/ai-vault/session-title-file-reader-wsl-stall.test.ts": 173, diff --git a/config/scripts/ci-unit-dependency-graph.mjs b/config/scripts/ci-unit-dependency-graph.mjs index f491c1749ded..7a3e035e45a1 100644 --- a/config/scripts/ci-unit-dependency-graph.mjs +++ b/config/scripts/ci-unit-dependency-graph.mjs @@ -42,9 +42,14 @@ export function buildUnitDependencyGraph(sources) { if (path === null) { continue } - const resolved = EXTENSIONS.map((extension) => path + extension).find((candidate) => - sources.has(candidate) - ) + let resolved + for (const extension of EXTENSIONS) { + const candidate = path + extension + if (sources.has(candidate)) { + resolved = candidate + break + } + } if (!resolved) { opaque.add(file) continue diff --git a/config/scripts/ci-unit-import-resolution-budget.test.mjs b/config/scripts/ci-unit-import-resolution-budget.test.mjs new file mode 100644 index 000000000000..bf1f04941b55 --- /dev/null +++ b/config/scripts/ci-unit-import-resolution-budget.test.mjs @@ -0,0 +1,59 @@ +import { describe, expect, it, vi } from 'vitest' +import { buildUnitDependencyGraph } from './ci-unit-dependency-graph.mjs' + +describe('unit graph import resolution', () => { + it('avoids allocating an extension-candidate array for every resolved import', () => { + const consumers = Array.from({ length: 1000 }, (_, index) => `src/consumer-${index}.ts`) + const sources = new Map([ + ['src/leaf', 'export const value = 1'], + ...consumers.map((file) => [file, "import './leaf'"]) + ]) + const originalMap = Array.prototype.map + let extensionArrays = 0 + const spy = vi.spyOn(Array.prototype, 'map').mockImplementation(function (...args) { + if (this.length === 10 && this[0] === '' && this[1] === '.ts' && this[9] === '/index.js') { + extensionArrays += 1 + } + return originalMap.apply(this, args) + }) + try { + const graph = buildUnitDependencyGraph(sources) + spy.mockRestore() + expect([...graph.reverse]).toEqual([['src/leaf', new Set(consumers)]]) + expect(graph.opaque).toEqual(new Set()) + expect(extensionArrays).toBe(0) + } finally { + spy.mockRestore() + } + }) + + it('keeps first-match precedence across literal paths, extensions and index files', () => { + const sources = new Map([ + ['src/leaf', ''], + ['src/leaf.ts', ''], + ['src/leaf.tsx', ''], + ['src/component.tsx', ''], + ['src/component.js', ''], + ['src/folder/index.ts', ''], + ['src/folder/index.tsx', ''], + ['src/config.json', '{}'], + ['src/renderer/src/view.tsx', ''], + ['src/use.ts', "import './leaf'; import './component'; import './folder'; import './config'"], + ['src/aliases.ts', "import '@renderer/view'; import '@/view'; import 'external-package'"], + ['src/missing.ts', "import './missing-file'"], + ['src/dynamic.ts', 'import(variablePath)'], + ['config/owner.mjs', "import '../src/leaf'"], + ['tests/owner.ts', "import '../src/leaf'"] + ]) + expect(buildUnitDependencyGraph(sources)).toEqual({ + reverse: new Map([ + ['src/leaf', new Set(['src/use.ts', 'config/owner.mjs', 'tests/owner.ts'])], + ['src/component.tsx', new Set(['src/use.ts'])], + ['src/folder/index.ts', new Set(['src/use.ts'])], + ['src/config.json', new Set(['src/use.ts'])], + ['src/renderer/src/view.tsx', new Set(['src/aliases.ts'])] + ]), + opaque: new Set(['src/missing.ts', 'src/dynamic.ts', 'config/owner.mjs', 'tests/owner.ts']) + }) + }) +}) diff --git a/config/scripts/ci-unit-sequencer.mjs b/config/scripts/ci-unit-sequencer.mjs index 6b580bdb0025..94dcd933f45a 100644 --- a/config/scripts/ci-unit-sequencer.mjs +++ b/config/scripts/ci-unit-sequencer.mjs @@ -16,13 +16,14 @@ export default class TimingSequencer extends BaseSequencer { 'utf8' ) ) + const discovered = new Set(plan.files) if ( plan.version !== 1 || !plan.sourceSha || plan.sourceSha !== process.env.ORCA_SHARD_SOURCE_SHA || JSON.stringify([...plan.files].sort()) !== JSON.stringify(specs.map(key).sort()) || !Array.isArray(plan.executionFiles) || - plan.executionFiles.some((file) => !plan.files.includes(file)) + plan.executionFiles.some((file) => !discovered.has(file)) ) { throw new Error('Selection provenance or discovery differs') } diff --git a/config/scripts/ci-unit-sequencer.test.mjs b/config/scripts/ci-unit-sequencer.test.mjs index 931a3ccd1582..a14ab60b4974 100644 --- a/config/scripts/ci-unit-sequencer.test.mjs +++ b/config/scripts/ci-unit-sequencer.test.mjs @@ -6,39 +6,86 @@ import TimingSequencer from './ci-unit-sequencer.mjs' let root afterEach(() => { + vi.restoreAllMocks() vi.unstubAllEnvs() if (root) { rmSync(root, { recursive: true, force: true }) } }) -it.each(['valid', 'stale', 'missing-file', 'missing-artifact'])( - 'preserves complete shard coverage with %s planning evidence', - async (kind) => { - root = mkdtempSync(join(tmpdir(), 'unit-sequencer-')) - const files = ['src/a.test.ts', 'src/b.test.ts', 'src/c.test.ts', 'src/d.test.ts'] - const plan = { - version: 1, - sourceSha: kind === 'stale' ? 'old' : 'current', - files: kind === 'missing-file' ? files.slice(1) : files, - executionFiles: files.slice(0, 2) - } - const planPath = join(root, 'selection.json') - if (kind !== 'missing-artifact') { - writeFileSync(planPath, JSON.stringify(plan)) - } - vi.stubEnv('ORCA_UNIT_SELECTION_PLAN', planPath) - vi.stubEnv('ORCA_SHARD_SOURCE_SHA', 'current') - vi.stubEnv('ORCA_SHARD_MANIFEST', join(root, 'assignment.json')) - const assigned = [] - for (const index of [1, 2]) { - const sequencer = new TimingSequencer({ config: { root, shard: { index, count: 2 } } }) - const specs = files.map((file) => ({ moduleId: join(root, file) })) - assigned.push(...(await sequencer.shard(specs)).map((spec) => spec.moduleId)) - } - expect(assigned.sort()).toEqual( - (kind === 'valid' ? files.slice(0, 2) : files).map((file) => join(root, file)).sort() - ) - expect(new Set(assigned).size).toBe(assigned.length) +it.each([ + 'valid', + 'stale', + 'missing-file', + 'missing-artifact', + 'outside-selection', + 'empty-selection' +])('preserves complete shard coverage with %s planning evidence', async (kind) => { + root = mkdtempSync(join(tmpdir(), 'unit-sequencer-')) + const files = ['src/a.test.ts', 'src/b.test.ts', 'src/c.test.ts', 'src/d.test.ts'] + const plan = { + version: 1, + sourceSha: kind === 'stale' ? 'old' : 'current', + files: kind === 'missing-file' ? files.slice(1) : files, + executionFiles: + kind === 'outside-selection' + ? ['src/unknown.test.ts'] + : kind === 'empty-selection' + ? [] + : files.slice(0, 2) } -) + const planPath = join(root, 'selection.json') + if (kind !== 'missing-artifact') { + writeFileSync(planPath, JSON.stringify(plan)) + } + vi.stubEnv('ORCA_UNIT_SELECTION_PLAN', planPath) + vi.stubEnv('ORCA_SHARD_SOURCE_SHA', 'current') + vi.stubEnv('ORCA_SHARD_MANIFEST', join(root, 'assignment.json')) + const assigned = [] + for (const index of [1, 2]) { + const sequencer = new TimingSequencer({ config: { root, shard: { index, count: 2 } } }) + const specs = files.map((file) => ({ moduleId: join(root, file) })) + assigned.push(...(await sequencer.shard(specs)).map((spec) => spec.moduleId)) + } + expect(assigned.sort()).toEqual( + (kind === 'valid' ? files.slice(0, 2) : files).map((file) => join(root, file)).sort() + ) + expect(new Set(assigned).size).toBe(assigned.length) +}) + +it('validates a large selection without scanning the discovered array for each file', async () => { + root = mkdtempSync(join(tmpdir(), 'unit-sequencer-scale-')) + const files = Array.from({ length: 1600 }, (_, index) => `src/scale-${index}.test.ts`) + const executionFiles = files.slice(800) + const planPath = join(root, 'selection.json') + writeFileSync( + planPath, + JSON.stringify({ version: 1, sourceSha: 'current', files, executionFiles }) + ) + vi.stubEnv('ORCA_UNIT_SELECTION_PLAN', planPath) + vi.stubEnv('ORCA_SHARD_SOURCE_SHA', 'current') + vi.stubEnv('ORCA_SHARD_MANIFEST', join(root, 'assignment.json')) + const sequencer = new TimingSequencer({ config: { root, shard: { index: 1, count: 1 } } }) + const specs = files.map((file) => ({ moduleId: join(root, file) })) + const includes = Array.prototype.includes + let discoveredArrayScans = 0 + const scan = vi + .spyOn(Array.prototype, 'includes') + .mockImplementation(function (value, fromIndex) { + if ( + this.length === files.length && + this[0] === files[0] && + typeof value === 'string' && + value.startsWith('src/scale-') + ) { + discoveredArrayScans += 1 + } + return includes.call(this, value, fromIndex) + }) + const selected = await sequencer.shard(specs) + scan.mockRestore() + expect(selected.map((spec) => spec.moduleId).sort()).toEqual( + executionFiles.map((file) => join(root, file)).sort() + ) + expect(discoveredArrayScans).toBe(0) +}) diff --git a/config/scripts/electron-builder-config.test.mjs b/config/scripts/electron-builder-config.test.mjs index fee07d5e299e..6e47f780a766 100644 --- a/config/scripts/electron-builder-config.test.mjs +++ b/config/scripts/electron-builder-config.test.mjs @@ -323,12 +323,14 @@ describe('electron-builder config', () => { // invisible to it and a packed worker entry fails closed — dropping every // OpenCode session in packaged builds while dev stays green. Three legs must // agree on the filename, so all three are read rather than hardcoded. - it('unpacks the OpenCode SQLite worker entry the scanner service forks', async () => { - const spawnSource = await readFile( - join(SRC_MAIN_DIR, 'ai-vault', 'session-scanner-opencode-sqlite-worker-spawn.ts'), + it('unpacks the foreign SQLite reader entry the scanner service runs OpenCode reads on', async () => { + const entryPathSource = await readFile( + join(SRC_MAIN_DIR, 'foreign-sqlite-readers', 'foreign-sqlite-reader-entry-path.ts'), 'utf8' ) - const entryFilename = spawnSource.match(/WORKER_ENTRY_FILENAME = '([^']+)'/)?.[1] + const entryFilename = entryPathSource.match( + /FOREIGN_SQLITE_READER_ENTRY_FILENAME = '([^']+)'/ + )?.[1] expect(entryFilename).toBeDefined() expect(electronBuilderConfig.asarUnpack).toContain(`out/main/${entryFilename}`) diff --git a/config/scripts/electron-builder-markdown-associations.test.mjs b/config/scripts/electron-builder-markdown-associations.test.mjs index 58f6f8d88655..654c6db246f3 100644 --- a/config/scripts/electron-builder-markdown-associations.test.mjs +++ b/config/scripts/electron-builder-markdown-associations.test.mjs @@ -8,6 +8,8 @@ const require = createRequire(import.meta.url) const electronBuilderConfig = require('../electron-builder.config.cjs') const MARKDOWN_EXTENSIONS = ['md', 'markdown', 'mdx'] +const TABULAR_EXTENSIONS = ['csv', 'tsv'] +const DOCUMENT_EXTENSIONS = [...MARKDOWN_EXTENSIONS, ...TABULAR_EXTENSIONS] // The exact shape app-builder-lib's APP_ASSOCIATE emits: a write to the DEFAULT ("") // value of Software\Classes\.. Additive `WriteRegNone ...\OpenWithProgids` must not @@ -23,23 +25,23 @@ const stripNsisCommentLines = (source) => const readInstallerHooks = () => readFile(electronBuilderConfig.nsis.include, 'utf8') -describe('electron-builder markdown file associations', () => { +describe('electron-builder document file associations', () => { // Why: any top-level (or `win.`) fileAssociations entry makes app-builder-lib's NSIS // packager emit `!insertmacro APP_ASSOCIATE`, whose first line writes that DEFAULT value // — silently taking .md from whichever editor owns it, for every existing user on their // next UPDATE, with APP_UNASSOCIATE never restoring it. `rank: 'Alternate'` cannot // prevent this; it is LSHandlerRank and applies to macOS only. So the mac block must // stay under `mac.` — hoisting it up "to share it with Windows" is what this test blocks. - it('never claims the Windows default markdown handler', () => { + it('never claims the Windows default document handler', () => { expect(electronBuilderConfig.fileAssociations).toBeUndefined() expect(electronBuilderConfig.win?.fileAssociations).toBeUndefined() }) - it('joins the macOS Open With list for every markdown extension without owning it', () => { + it('joins the macOS Open With list for every supported extension without owning it', () => { const associations = electronBuilderConfig.mac.fileAssociations // One entry per extension: an array `ext` would break the Linux packager's `*.${ext}` glob. expect([...associations].map((association) => association.ext).sort()).toEqual( - [...MARKDOWN_EXTENSIONS].sort() + [...DOCUMENT_EXTENSIONS].sort() ) for (const association of associations) { expect(association).toMatchObject({ role: 'Editor', rank: 'Alternate' }) @@ -54,6 +56,14 @@ describe('electron-builder markdown file associations', () => { expect(electronBuilderConfig.linux.fileAssociations).toBeUndefined() }) + it('adds CSV and TSV handlers to the Linux desktop entry', () => { + expect(electronBuilderConfig.linux.mimeTypes).toEqual([ + 'text/markdown', + 'text/csv', + 'text/tab-separated-values' + ]) + }) + it('points the single NSIS include at the installer hooks file on disk', () => { const includePath = electronBuilderConfig.nsis.include expect(existsSync(includePath)).toBe(true) @@ -84,7 +94,7 @@ describe('electron-builder markdown file associations', () => { expect(stripped).toMatch(DEFAULT_HANDLER_WRITE) }) - it('registers Windows markdown Open With additively, never as the default', async () => { + it('registers Windows document Open With additively, never as the default', async () => { const hooks = await readInstallerHooks() expect(stripNsisCommentLines(hooks)).not.toMatch(DEFAULT_HANDLER_WRITE) @@ -92,11 +102,18 @@ describe('electron-builder markdown file associations', () => { expect(hooks).toMatch( /WriteRegNone\s+SHELL_CONTEXT\s+"Software\\Classes\\\$\{EXT\}\\OpenWithProgids"/ ) - expect(hooks).toMatch(/!macro\s+ORCA_REGISTER_MARKDOWN_OPEN_WITH\s+EXT/) + expect(hooks).toMatch(/!macro\s+ORCA_REGISTER_DOCUMENT_OPEN_WITH\s+EXT\s+PROGID/) for (const ext of MARKDOWN_EXTENSIONS) { - expect(hooks).toContain(`ORCA_REGISTER_MARKDOWN_OPEN_WITH ".${ext}"`) - expect(hooks).toContain(`ORCA_UNREGISTER_MARKDOWN_OPEN_WITH ".${ext}"`) + expect(hooks).toContain(`ORCA_REGISTER_DOCUMENT_OPEN_WITH ".${ext}" "\${MARKDOWN_PROGID}"`) + expect(hooks).toContain(`ORCA_UNREGISTER_DOCUMENT_OPEN_WITH ".${ext}" "\${MARKDOWN_PROGID}"`) + } + for (const ext of TABULAR_EXTENSIONS) { + expect(hooks).toContain(`ORCA_REGISTER_DOCUMENT_OPEN_WITH ".${ext}" "\${TABULAR_PROGID}"`) + expect(hooks).toContain(`ORCA_UNREGISTER_DOCUMENT_OPEN_WITH ".${ext}" "\${TABULAR_PROGID}"`) } + expect(hooks).toContain('!define TABULAR_PROGID "Orca.Tabular"') + expect(hooks).toContain('ORCA_REGISTER_DOCUMENT_PROGID "${TABULAR_PROGID}" "Tabular Document"') + expect(hooks).toContain('DeleteRegKey SHELL_CONTEXT "Software\\Classes\\${TABULAR_PROGID}"') expect(hooks).toMatch(/!macro\s+customInstall\b/) expect(hooks).toMatch(/!macro\s+customUnInstall\b/) }) diff --git a/config/scripts/electron-vite-output-contract.test.ts b/config/scripts/electron-vite-output-contract.test.ts index da6d45c8a230..d5ddc5df43a1 100644 --- a/config/scripts/electron-vite-output-contract.test.ts +++ b/config/scripts/electron-vite-output-contract.test.ts @@ -99,13 +99,14 @@ describe('Electron Vite output contract', () => { expect(output.chunkFileNames).toBe('chunks/[name]-[hash].js') }) - it('keeps offline profile-state CLI imports unpacked at stable paths', () => { + it('keeps CLI main imports unpacked at stable paths', () => { const input = electronViteConfig.main?.build?.rollupOptions?.input if (!input || typeof input !== 'object' || Array.isArray(input)) { throw new Error('Expected named main-process inputs') } for (const name of [ + 'gitlab/project-ref-parser', 'orca-profiles/profile-index-store', 'persistence/profile-state/profile-state-access', 'persistence/profile-state/profile-state-active-location', @@ -121,6 +122,7 @@ describe('Electron Vite output contract', () => { ]) { expect(input).toHaveProperty(name) } + expect(electronBuilderConfig.asarUnpack).toContain('out/main/gitlab/project-ref-parser.js') expect(electronBuilderConfig.asarUnpack).toContain('out/main/persistence/profile-state/**') expect(electronBuilderConfig.asarUnpack).toContain( 'out/main/orca-profiles/profile-index-store.js' @@ -144,8 +146,11 @@ describe('Electron Vite output contract', () => { expect(external('@xterm/addon-serialize', undefined, false)).toBe(false) expect(external('tldts', undefined, false)).toBe(false) expect(external('zod', undefined, false)).toBe(false) + expect(external('smol-toml', undefined, false)).toBe(false) + expect(external('smol-toml/package.json', undefined, false)).toBe(false) expect(electronViteConfig.main?.build?.externalizeDeps?.exclude).toContain('tldts') expect(electronViteConfig.main?.build?.externalizeDeps?.exclude).toContain('zod') + expect(electronViteConfig.main?.build?.externalizeDeps?.exclude).toContain('smol-toml') }) it('bundles validation dependencies used by the sandboxed preload', () => { diff --git a/config/scripts/ensure-native-runtime.mjs b/config/scripts/ensure-native-runtime.mjs index c93b6a19d621..a91b4f528c4f 100644 --- a/config/scripts/ensure-native-runtime.mjs +++ b/config/scripts/ensure-native-runtime.mjs @@ -8,9 +8,13 @@ import { basename, dirname, resolve } from 'node:path' import { ensureWindowsProcessTreeCommandLinePatch, inspectWindowsProcessTreeAddon, + nodeGypRebuildInvocation, + nodeGypRebuildTimeoutMs, stageWindowsProcessTreeNodeAddonApiHeaders, windowsProcessTreeAddonPath } from './windows-process-tree-gyp-rebuild.mjs' +import { describeProcessFailure, runProcessSync } from './script-child-process.mjs' +import { disableMsbuildFileTrackingOnWindows } from './msbuild-file-tracking.mjs' const require = createRequire(import.meta.url) const { assertNodePtyJobOwnership, nodePtyAddonPath } = require('./node-pty-job-ownership.cjs') @@ -400,33 +404,39 @@ function rebuildNodeRuntimeModules(moduleNames) { moduleDir = realpathSync(moduleDir) } console.warn(`[native-runtime] Rebuilding ${moduleName} with node-gyp.`) - runPnpm(['exec', 'node-gyp', 'rebuild'], { cwd: moduleDir }) + // pnpm exec inside an installed addon cannot discover the root build tool. + runNodeGyp( + moduleName, + nodeGypRebuildInvocation( + process.arch, + moduleDir, + process.env.npm_config_node_gyp || undefined + ) + ) if (moduleName === 'node-pty' && process.platform === 'win32') { runNodeScript([resolve(moduleDir, 'scripts', 'post-install.js')]) } } } -function runPnpm(args, { cwd = projectDir } = {}) { - // cmd.exe resolves both Corepack's pnpm.cmd and pnpm 12's native pnpm.exe. - const command = 'pnpm' +function runNodeGyp(moduleName, { args, cwd }) { const env = - process.platform === 'linux' && args.includes('node-gyp') + process.platform === 'linux' ? { ...process.env, CXXFLAGS: `${process.env.CXXFLAGS ?? ''} -std=gnu++2a`.trim() } - : process.env - const result = spawnSync(command, args, { + : disableMsbuildFileTrackingOnWindows({ ...process.env }) + const result = runProcessSync({ + program: process.execPath, + args, cwd, + env, stdio: 'inherit', - shell: process.platform === 'win32', - env + timeoutMs: nodeGypRebuildTimeoutMs(moduleName) }) - - if (result.error || result.status !== 0) { - console.error(`[native-runtime] ${command} ${args.join(' ')} failed in ${cwd}.`) - if (result.error) { - console.error(formatError(result.error)) - } - process.exit(result.status ?? 1) + if (result.code !== 0) { + console.error( + `[native-runtime] node-gyp rebuild failed in ${cwd}: ${describeProcessFailure(result)}` + ) + process.exit(result.code ?? 1) } } diff --git a/config/scripts/ensure-native-runtime.test.mjs b/config/scripts/ensure-native-runtime.test.mjs index 47fe435edccb..68a10b5bb206 100644 --- a/config/scripts/ensure-native-runtime.test.mjs +++ b/config/scripts/ensure-native-runtime.test.mjs @@ -1,7 +1,7 @@ import { spawnSync } from 'node:child_process' import { - chmodSync, copyFileSync, + existsSync, mkdirSync, mkdtempSync, readFileSync, @@ -9,10 +9,21 @@ import { writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' -import { delimiter, join } from 'node:path' +import { isAbsolute, join, parse } from 'node:path' import { fileURLToPath } from 'node:url' import { describe, expect, it } from 'vitest' +import { + DEFAULT_MAX_OUTPUT_BYTES, + runProcessSync +} from '../../src/shared/child-process/run-process.ts' +import { resolveCliCommand } from '../../src/shared/node-cli-command-resolution.ts' +import { removeTreeSync } from '../../src/shared/windows-transient-lock-removal.ts' +import { resolvePnpmCliInvocation } from './pnpm-cli-invocation.mjs' import { copyScriptWithLocalModules } from './script-module-dependencies.mjs' +import { + nodeGypRebuildInvocation, + nodeGypRebuildTimeoutMs +} from './windows-process-tree-gyp-rebuild.mjs' const sourceScriptPath = fileURLToPath(new URL('./ensure-native-runtime.mjs', import.meta.url)) // The import walk sees `from './x.mjs'` only, so the createRequire'd CJS @@ -30,23 +41,30 @@ describe('ensure-native-runtime', () => { const scriptPath = join(projectDir, 'config', 'scripts', 'ensure-native-runtime.mjs') const logPath = join(projectDir, 'native-runtime.log') const markerPath = join(projectDir, 'rebuilt.marker') - const binDir = join(projectDir, 'bin') writeFakeNativeModules(projectDir) writeNodePtyPatchFile(projectDir) - writeFakePnpm(binDir) + writeFakeNodeGyp(projectDir) + const verboseOutputBytes = DEFAULT_MAX_OUTPUT_BYTES + 1024 * 1024 const result = spawnSync(process.execPath, [scriptPath, '--runtime=node'], { cwd: projectDir, encoding: 'utf8', - env: envWithPrependedPath(binDir, { + maxBuffer: DEFAULT_MAX_OUTPUT_BYTES * 4, + env: envForNativeFixture(projectDir, { ORCA_NATIVE_TEST_LOG: logPath, - ORCA_NATIVE_TEST_MARKER: markerPath + ORCA_NATIVE_TEST_MARKER: markerPath, + ORCA_NATIVE_TEST_VERBOSE_OUTPUT_BYTES: String(verboseOutputBytes) }) }) expect(result.status, result.stderr).toBe(0) + expect(result.stdout.indexOf('node-gyp stdout complete\n')).toBe(verboseOutputBytes) + expect(/^x+$/.test(result.stdout.slice(0, verboseOutputBytes))).toBe(true) + expect(result.stderr).toContain('node-gyp stderr complete\n') const log = readFileSync(logPath, 'utf8') - expect(log).toContain('pnpm exec node-gyp rebuild\n') + expect(log).toContain(`node-gyp rebuild --arch=${process.arch}\n`) + expect(log).toContain(`node-gyp timeout=${nodeGypRebuildTimeoutMs('node-pty')}\n`) + expect(log).toContain(`trackFileAccess=${process.platform === 'win32' ? 'false' : ''}\n`) expect(log).toContain(join('node_modules', 'node-pty')) if (process.platform === 'linux') { expect(log).toMatch(/^cxxflags=(?:.*\s)?-std=gnu\+\+2a$/m) @@ -60,24 +78,29 @@ describe('ensure-native-runtime', () => { } }) - it.skipIf(process.platform !== 'win32')( - 'rebuilds other failed Windows addons with patched node-pty', - () => { + it.skipIf(process.platform !== 'win32').each([ + { trackingEnv: {}, tracking: 'false' }, + { trackingEnv: { TrackFileAccess: 'true' }, tracking: 'true' }, + { trackingEnv: { trackfileaccess: 'true' }, tracking: 'true' }, + { trackingEnv: { tRaCkFiLeAcCeSs: 'false' }, tracking: 'false' } + ])( + 'rebuilds other failed Windows addons with patched node-pty and tracking=$tracking', + ({ trackingEnv, tracking }) => { const projectDir = mkTempProject() try { const scriptPath = join(projectDir, 'config', 'scripts', 'ensure-native-runtime.mjs') const logPath = join(projectDir, 'native-runtime.log') const markerPath = join(projectDir, 'rebuilt.marker') - const binDir = join(projectDir, 'bin') writeFakeNativeModules(projectDir, { windowsRegistryRequiresMarker: true }) writeNodePtyPatchFile(projectDir) - writeFakePnpm(binDir) + writeFakeNodeGyp(projectDir) const result = spawnSync(process.execPath, [scriptPath, '--runtime=node'], { cwd: projectDir, encoding: 'utf8', - env: envWithPrependedPath(binDir, { + env: envForNativeFixture(projectDir, { + ...trackingEnv, ORCA_NATIVE_TEST_LOG: logPath, ORCA_NATIVE_TEST_MARKER: markerPath }) @@ -85,7 +108,12 @@ describe('ensure-native-runtime', () => { expect(result.status, result.stderr).toBe(0) const log = readFileSync(logPath, 'utf8') - expect(log.match(/pnpm exec node-gyp rebuild\n/g)).toHaveLength(2) + expect( + log.split('\n').filter((line) => line === `node-gyp rebuild --arch=${process.arch}`) + ).toHaveLength(2) + expect( + log.split('\n').filter((line) => line === `trackFileAccess=${tracking}`) + ).toHaveLength(2) expect(log).toContain(join('node_modules', 'node-pty')) expect(log).toContain(join('node_modules', '@orca', 'windows-registry')) } finally { @@ -103,15 +131,14 @@ describe('ensure-native-runtime', () => { const scriptPath = join(projectDir, 'config', 'scripts', 'ensure-native-runtime.mjs') const logPath = join(projectDir, 'native-runtime.log') const markerPath = join(projectDir, 'rebuilt.marker') - const binDir = join(projectDir, 'bin') writeLoadableNativeModules(projectDir) writeNodePtyPatchFile(projectDir) - writeFakePnpm(binDir) + writeFakeNodeGyp(projectDir) const result = spawnSync(process.execPath, [scriptPath, '--runtime=node'], { cwd: projectDir, encoding: 'utf8', - env: envWithPrependedPath(binDir, { + env: envForNativeFixture(projectDir, { ORCA_NATIVE_TEST_LOG: logPath, ORCA_NATIVE_TEST_MARKER: markerPath }) @@ -121,7 +148,7 @@ describe('ensure-native-runtime', () => { expect(result.stderr).toContain( 'Patched node-pty build artifacts are missing; rebuilding native deps.' ) - expect(readFileSync(logPath, 'utf8')).toContain('pnpm exec node-gyp rebuild\n') + expect(readFileSync(logPath, 'utf8')).toContain(`node-gyp rebuild --arch=${process.arch}\n`) } finally { rmSync(projectDir, { recursive: true, force: true }) } @@ -137,16 +164,15 @@ describe('ensure-native-runtime', () => { const scriptPath = join(projectDir, 'config', 'scripts', 'ensure-native-runtime.mjs') const logPath = join(projectDir, 'native-runtime.log') const markerPath = join(projectDir, 'rebuilt.marker') - const binDir = join(projectDir, 'bin') writeLoadableNativeModules(projectDir) writeNodePtyPatchFile(projectDir) writePatchedNodePtyBuildArtifacts(projectDir) - writeFakePnpm(binDir) + writeFakeNodeGyp(projectDir) const result = spawnSync(process.execPath, [scriptPath, '--runtime=node'], { cwd: projectDir, encoding: 'utf8', - env: envWithPrependedPath(binDir, { + env: envForNativeFixture(projectDir, { ORCA_NATIVE_TEST_LOG: logPath, ORCA_NATIVE_TEST_MARKER: markerPath }) @@ -154,7 +180,7 @@ describe('ensure-native-runtime', () => { expect(result.status, result.stderr).toBe(0) expect(result.stderr).toContain("expected build/Release so Orca's node-pty patch is active") - expect(readFileSync(logPath, 'utf8')).toContain('pnpm exec node-gyp rebuild\n') + expect(readFileSync(logPath, 'utf8')).toContain(`node-gyp rebuild --arch=${process.arch}\n`) } finally { rmSync(projectDir, { recursive: true, force: true }) } @@ -170,16 +196,15 @@ describe('ensure-native-runtime', () => { const scriptPath = join(projectDir, 'config', 'scripts', 'ensure-native-runtime.mjs') const logPath = join(projectDir, 'native-runtime.log') const markerPath = join(projectDir, 'rebuilt.marker') - const binDir = join(projectDir, 'bin') writeLoadableNativeModules(projectDir, { nativeDir: '../build/Release/' }) writeNodePtyPatchFile(projectDir) writePatchedNodePtyBuildArtifacts(projectDir) - writeFakePnpm(binDir) + writeFakeNodeGyp(projectDir) const result = spawnSync(process.execPath, [scriptPath, '--runtime=node'], { cwd: projectDir, encoding: 'utf8', - env: envWithPrependedPath(binDir, { + env: envForNativeFixture(projectDir, { ORCA_NATIVE_TEST_LOG: logPath, ORCA_NATIVE_TEST_MARKER: markerPath }) @@ -187,12 +212,64 @@ describe('ensure-native-runtime', () => { expect(result.status, result.stderr).toBe(0) expect(result.stderr).not.toContain('Patched node-pty build artifacts are missing') - expect(readFileSync(logPath, 'utf8')).not.toContain('pnpm exec node-gyp rebuild') + expect(readFileSync(logPath, 'utf8')).not.toContain('node-gyp rebuild') } finally { rmSync(projectDir, { recursive: true, force: true }) } } ) + it('finds the installed node-gyp entry from an addon without build tools on PATH', () => { + const { command, prefixArgs } = resolvePnpmCliInvocation() + const program = isAbsolute(command) ? command : resolveCliCommand(parse(command).name) + expect(isAbsolute(program), 'pnpm must be installed for the native rebuild contract').toBe(true) + const projectDir = mkTempProject() + try { + writeFakeNativeModules(projectDir) + const addonDir = join(projectDir, 'node_modules', 'node-pty') + const env = { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' } + for (const key of Object.keys(env)) { + if (key.toLowerCase() === 'path') { + env[key] = '' + } + } + const oldInvocation = runProcessSync({ + program, + args: [ + ...prefixArgs, + '--config.verify-deps-before-run=false', + 'exec', + 'node-gyp', + '--version' + ], + cwd: addonDir, + env, + timeoutMs: 20_000 + }) + expect(oldInvocation.code).not.toBe(0) + expect(`${oldInvocation.stdout}\n${oldInvocation.stderr}`).toContain( + 'Command "node-gyp" not found' + ) + const { args, cwd } = nodeGypRebuildInvocation(process.arch, addonDir) + const installedInvocation = runProcessSync({ + program: process.execPath, + args: [args[0], '--version'], + cwd, + env, + timeoutMs: 20_000 + }) + expect( + installedInvocation.code, + `${installedInvocation.stdout}\n${installedInvocation.stderr}` + ).toBe(0) + const manifest = JSON.parse( + readFileSync(new URL('../../node_modules/node-gyp/package.json', import.meta.url), 'utf8') + ) + expect(installedInvocation.stdout.trim()).toBe(`v${manifest.version}`) + expect(existsSync(join(addonDir, 'pnpm-lock.yaml'))).toBe(false) + } finally { + removeTreeSync(projectDir) + } + }) }) function mkTempProject() { @@ -200,6 +277,17 @@ function mkTempProject() { // Walked, not listed: the script imports windows-process-tree-gyp-rebuild.mjs, and a fixture // missing it fails every case with a module-resolution error instead of the defect under test. copyScriptWithLocalModules(sourceScriptPath, join(projectDir, 'config', 'scripts')) + writeFileSync( + join(projectDir, 'config', 'scripts', 'script-child-process.mjs'), + `import { appendFileSync } from 'node:fs' +import { describeProcessFailure, runProcessSync as run } from ${JSON.stringify(new URL('./script-child-process.mjs', import.meta.url).href)} +export { describeProcessFailure } +export function runProcessSync(options) { + appendFileSync(process.env.ORCA_NATIVE_TEST_LOG, \`node-gyp timeout=\${options.timeoutMs}\\n\`) + return run(options) +} +` + ) for (const name of REQUIRED_CJS_SIBLINGS) { copyFileSync( fileURLToPath(new URL(`./${name}`, import.meta.url)), @@ -209,15 +297,15 @@ function mkTempProject() { return projectDir } -function envWithPrependedPath(binDir, extraEnv) { - const pathKey = - process.platform === 'win32' - ? (Object.keys(process.env).find((key) => key.toLowerCase() === 'path') ?? 'Path') - : 'PATH' +function envForNativeFixture(projectDir, extraEnv) { + // An inherited tracking preference would mask the Windows default under test. + const inherited = Object.fromEntries( + Object.entries(process.env).filter(([key]) => key.toLowerCase() !== 'trackfileaccess') + ) return { - ...process.env, + ...inherited, ...extraEnv, - [pathKey]: `${binDir}${delimiter}${process.env[pathKey] ?? ''}` + npm_config_node_gyp: join(projectDir, 'node_modules', 'node-gyp', 'bin', 'node-gyp.js') } } @@ -258,6 +346,11 @@ exports.loadNativeModule = function loadNativeModule(nativeName) { ` ) writeFakeWindowsRegistry(projectDir, { requiresMarker: windowsRegistryRequiresMarker }) + if (process.platform === 'win32') { + const buildDir = join(nodePtyDir, 'build', 'Release') + writePatchedNodePtyBuildArtifacts(projectDir) + writeFileSync(join(buildDir, 'conpty.node'), Buffer.from('msys-2.0.dll', 'utf16le')) + } } function writeLoadableNativeModules(projectDir, { nativeDir = null } = {}) { @@ -314,7 +407,10 @@ function writeFakeWindowsRegistry(projectDir, { requiresMarker = false } = {}) { ) const processTreeDir = join(projectDir, 'node_modules', '@vscode', 'windows-process-tree') mkdirSync(processTreeDir, { recursive: true }) - writeFileSync(join(processTreeDir, 'index.js'), 'module.exports = {}\n') + writeFileSync( + join(processTreeDir, 'index.js'), + 'exports.supportedProcessDataFlags = 4; exports.getProcessCreationTime = () => 1\n' + ) } function writeNodePtyPatchFile(projectDir) { @@ -338,33 +434,26 @@ function writePatchedNodePtyBuildArtifacts(projectDir) { } } -function writeFakePnpm(binDir) { - mkdirSync(binDir, { recursive: true }) - const shimPath = join(binDir, 'pnpm-shim.cjs') +function writeFakeNodeGyp(projectDir) { + const toolDir = join(projectDir, 'node_modules', 'node-gyp', 'bin') + mkdirSync(toolDir, { recursive: true }) writeFileSync( - shimPath, + join(toolDir, 'node-gyp.js'), ` -const { appendFileSync, writeFileSync } = require('node:fs') - -appendFileSync(process.env.ORCA_NATIVE_TEST_LOG, \`pnpm \${process.argv.slice(2).join(' ')}\\n\`) +const { appendFileSync, writeFileSync, writeSync } = require('node:fs') +appendFileSync(process.env.ORCA_NATIVE_TEST_LOG, \`node-gyp \${process.argv.slice(2).join(' ')}\\n\`) appendFileSync(process.env.ORCA_NATIVE_TEST_LOG, \`cwd=\${process.cwd()}\\n\`) -appendFileSync( - process.env.ORCA_NATIVE_TEST_LOG, - \`npm_config_build_from_source=\${process.env.npm_config_build_from_source || ''}\\n\` -) -appendFileSync( - process.env.ORCA_NATIVE_TEST_LOG, - \`cxxflags=\${process.env.CXXFLAGS || ''}\\n\` -) +appendFileSync(process.env.ORCA_NATIVE_TEST_LOG, \`cxxflags=\${process.env.CXXFLAGS || ''}\\n\`) +appendFileSync(process.env.ORCA_NATIVE_TEST_LOG, \`trackFileAccess=\${process.env.TrackFileAccess ?? ''}\\n\`) +if (process.env.ORCA_NATIVE_TEST_VERBOSE_OUTPUT_BYTES) { + const output = Buffer.alloc(Number(process.env.ORCA_NATIVE_TEST_VERBOSE_OUTPUT_BYTES), 'x') + for (let offset = 0; offset < output.length;) { + offset += writeSync(1, output.subarray(offset)) + } + writeSync(1, 'node-gyp stdout complete\\n') + writeSync(2, 'node-gyp stderr complete\\n') +} writeFileSync(process.env.ORCA_NATIVE_TEST_MARKER, 'rebuilt') ` ) - - const posixPnpmPath = join(binDir, 'pnpm') - writeFileSync(posixPnpmPath, `#!/usr/bin/env node\nrequire(${JSON.stringify(shimPath)})\n`) - chmodSync(posixPnpmPath, 0o755) - writeFileSync( - join(binDir, 'pnpm.cmd'), - `@echo off\r\n"${process.execPath}" "%~dp0\\pnpm-shim.cjs" %*\r\n` - ) } diff --git a/config/scripts/foreign-sqlite-reader-worker-smoke.mjs b/config/scripts/foreign-sqlite-reader-worker-smoke.mjs new file mode 100644 index 000000000000..b83c9c088afc --- /dev/null +++ b/config/scripts/foreign-sqlite-reader-worker-smoke.mjs @@ -0,0 +1,80 @@ +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' +import { describeProcessFailure, runProcessSync } from './script-child-process.mjs' +import { ORCAD_FOREIGN_SQLITE_READER_ENTRY } from '../../src/shared/orcad-artifacts.ts' + +// Why a child process: the read must run under the runtime orcad ships, which may not be +// the Node running the build. The verdict is the exit code, never matched output. +const PROBE = ` +const { Worker } = require('node:worker_threads') +const { DatabaseSync } = process.getBuiltinModule('node:sqlite') +const [entry, dbPath, missingPath] = process.argv.slice(2) +const db = new DatabaseSync(dbPath) +db.exec('CREATE TABLE session (id TEXT PRIMARY KEY, directory TEXT NOT NULL, time_created INTEGER NOT NULL, parent_id TEXT)') +db.prepare('INSERT INTO session VALUES (?, ?, ?, ?)').run('ses_smoke', '/smoke', 100, null) +db.close() +const steps = [ + { + request: { id: 1, kind: 'openCodeBinderSessions', dbPath, cursor: { ms: 0, id: '' } }, + expected: [{ id: 'ses_smoke', directory: '/smoke', createdAtMs: 100, parentId: null }] + }, + { request: { id: 2, kind: 'cursorProfile', dbPath: missingPath }, expected: { status: 'missing' } }, + // The OpenCode history scanner's kinds share this entry. + { request: { id: 3, kind: 'list', dbPaths: [], limit: null }, expected: { candidates: [], issues: [] } } +] +const worker = new Worker(entry, { execArgv: [] }) +const fail = (code, message) => { + console.error(message) + process.exit(code) +} +setTimeout(() => fail(3, 'foreign SQLite reader worker did not answer'), 20000).unref() +worker.on('error', (error) => fail(4, String(error && error.stack || error))) +worker.on('exit', (code) => fail(5, 'foreign SQLite reader worker exited with ' + code)) +let step = 0 +worker.on('message', (response) => { + const { request, expected } = steps[step] + if (!response || response.id !== request.id || response.ok !== true || + JSON.stringify(response.value) !== JSON.stringify(expected)) { + fail(6, request.kind + ' answered ' + JSON.stringify(response)) + } + step += 1 + if (step === steps.length) { + process.exit(0) + } + worker.postMessage(steps[step].request) +}) +worker.postMessage(steps[0].request) +` + +/** + * Load the built foreign SQLite reader entry and run real reads through it. + * @param outDir - orcad output directory holding the entry. + * @param options.runtimePath - Node to run under; the build's own Node when omitted. + */ +export function smokeForeignSqliteReaderWorker(outDir, { runtimePath, timeoutMs = 30_000 } = {}) { + const directory = mkdtempSync(join(tmpdir(), 'orca-foreign-sqlite-smoke-')) + try { + const probe = join(directory, 'probe.cjs') + writeFileSync(probe, PROBE) + const result = runProcessSync({ + program: runtimePath ?? process.execPath, + args: [ + probe, + resolve(outDir, ORCAD_FOREIGN_SQLITE_READER_ENTRY), + join(directory, 'opencode.db'), + join(directory, 'missing.vscdb') + ], + env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' }, + timeoutMs, + maxOutputBytes: 64 * 1024 + }) + if (result.code !== 0 || result.timedOut) { + throw new Error( + `Foreign SQLite reader worker smoke failed: ${describeProcessFailure(result)}` + ) + } + } finally { + rmSync(directory, { recursive: true, force: true }) + } +} diff --git a/config/scripts/foreign-sqlite-reader-worker-smoke.test.mjs b/config/scripts/foreign-sqlite-reader-worker-smoke.test.mjs new file mode 100644 index 000000000000..36121ad88d43 --- /dev/null +++ b/config/scripts/foreign-sqlite-reader-worker-smoke.test.mjs @@ -0,0 +1,65 @@ +import { build } from 'esbuild' +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import { smokeForeignSqliteReaderWorker } from './foreign-sqlite-reader-worker-smoke.mjs' +import { ORCAD_CHILD_ENTRY_POINTS } from './orcad-entry-build.mjs' + +const ENTRY = 'foreign-sqlite-reader-entry.js' +const directories = [] +let builtDirectory + +function fixtureDirectory() { + const directory = mkdtempSync(join(tmpdir(), 'orca-foreign-sqlite-smoke-test-')) + directories.push(directory) + return directory +} + +beforeAll(async () => { + builtDirectory = fixtureDirectory() + await build({ + entryPoints: [resolve(ORCAD_CHILD_ENTRY_POINTS.foreignSqliteReader)], + outfile: join(builtDirectory, ENTRY), + bundle: true, + platform: 'node', + target: 'node18', + format: 'cjs', + external: ['electron'], + logLevel: 'silent' + }) +}, 60_000) + +afterAll(() => { + for (const directory of directories) { + rmSync(directory, { recursive: true, force: true }) + } +}) + +describe('foreign SQLite reader build smoke', () => { + it('passes against the built entry', () => { + expect(() => smokeForeignSqliteReaderWorker(builtDirectory)).not.toThrow() + }) + + it('fails when the entry is missing', () => { + expect(() => smokeForeignSqliteReaderWorker(fixtureDirectory())).toThrow('smoke failed') + }) + + it('fails when the worker answers without reading', () => { + const directory = fixtureDirectory() + writeFileSync( + join(directory, ENTRY), + `const { parentPort } = require('node:worker_threads') + parentPort.on('message', ({ id }) => parentPort.postMessage({ id, ok: true, value: [] }))` + ) + expect(() => smokeForeignSqliteReaderWorker(directory)).toThrow('smoke failed') + }) + + it('runs in the orcad build under both runtimes', () => { + const source = readFileSync(resolve('config/scripts/build-orcad.mjs'), 'utf8') + expect(source).toContain('smokeForeignSqliteReaderWorker(OUT_DIR)') + expect(source).toContain( + 'smokeForeignSqliteReaderWorker(OUT_DIR, { runtimePath: nodeRuntimePath })' + ) + }) +}) diff --git a/config/scripts/generate-monaco-associations.mjs b/config/scripts/generate-monaco-associations.mjs index b5c2e99c00a6..ea81be3fa471 100644 --- a/config/scripts/generate-monaco-associations.mjs +++ b/config/scripts/generate-monaco-associations.mjs @@ -10,6 +10,12 @@ export const associationsPath = fileURLToPath( new URL('../../src/renderer/src/lib/monaco-language-associations.json', import.meta.url) ) +// Monaco omits common Ruby task, template and configuration files. +const rubyAssociations = { + extensions: ['.rake', '.ru', '.jbuilder', '.thor'], + filenames: ['Guardfile', 'Capfile', 'Podfile', 'Brewfile', 'Vagrantfile'] +} + // Read registration metadata without importing Monaco or executing its grammar loaders. export function readMonacoAssociations() { const entry = ts.createSourceFile( @@ -62,6 +68,12 @@ export function readMonacoAssociations() { if (!metadata.id) { throw new Error(`Missing language id in ${file}`) } + if (metadata.id === 'ruby') { + metadata.extensions = [ + ...new Set([...metadata.extensions, ...rubyAssociations.extensions]) + ] + metadata.filenames = [...new Set([...metadata.filenames, ...rubyAssociations.filenames])] + } registrations.push(metadata) } ts.forEachChild(node, visit) diff --git a/config/scripts/generate-monaco-associations.test.mjs b/config/scripts/generate-monaco-associations.test.mjs index 0e7563cd49ad..1076ea78c8a1 100644 --- a/config/scripts/generate-monaco-associations.test.mjs +++ b/config/scripts/generate-monaco-associations.test.mjs @@ -3,10 +3,36 @@ import { describe, expect, it } from 'vitest' import { associationsPath, readMonacoAssociations } from './generate-monaco-associations.mjs' describe('Monaco filename associations', () => { - it('matches every registration shipped by the installed editor entry point', () => { + it('matches the installed editor registrations and curated Orca associations', () => { expect( JSON.parse(readFileSync(associationsPath, 'utf8')), - 'Run node config/scripts/generate-monaco-associations.mjs after upgrading Monaco' + 'Run node config/scripts/generate-monaco-associations.mjs after changing associations or Monaco' ).toEqual(readMonacoAssociations()) }) + + it('keeps built-in Ruby aliases alongside the curated Ruby associations', () => { + expect(readMonacoAssociations().find((language) => language.id === 'ruby')).toEqual({ + id: 'ruby', + extensions: expect.arrayContaining([ + '.rb', + '.rbx', + '.rjs', + '.gemspec', + '.pp', + '.rake', + '.ru', + '.jbuilder', + '.thor' + ]), + filenames: expect.arrayContaining([ + 'rakefile', + 'Gemfile', + 'Guardfile', + 'Capfile', + 'Podfile', + 'Brewfile', + 'Vagrantfile' + ]) + }) + }) }) diff --git a/config/scripts/git-binary-compatibility-workflow.test.mjs b/config/scripts/git-binary-compatibility-workflow.test.mjs index c03c7973f6a8..f6e6a32b720c 100644 --- a/config/scripts/git-binary-compatibility-workflow.test.mjs +++ b/config/scripts/git-binary-compatibility-workflow.test.mjs @@ -16,10 +16,14 @@ describe('Git binary compatibility PR gate', () => { const run = stepNamed('Verify Git binary compatibility matrix')?.run expect(run).toContain('ORCA_GIT_COMPAT_BINARY="$HOME/.cache/orca-git-compat/git-2.25.5/git"') + expect(run).toContain('GIT_EXEC_PATH="$HOME/.cache/orca-git-compat/git-2.25.5"') expect(run).toContain('alpine/git:edge-2.38.1|2.38.1') expect(run).toContain('alpine/git:v2.49.1|2.49.1') expect(run).toContain('ORCA_GIT_COMPAT_IMAGE="$image"') expect(run).toContain('src/shared/git-binary-compatibility.test.ts') + expect(run).toContain('src/main/git/worktree-safety-real-git.test.ts') + expect(run).toContain('src/main/git/worktree-rebase-update-refs-real-git.test.ts') + expect(run).toContain('src/relay/git-review-draft-binary-compatibility.test.ts') expect(run).toContain('pids+=("$!")') expect(run).toContain('wait "$pid" || status=1') }) @@ -30,10 +34,17 @@ describe('Git binary compatibility PR gate', () => { expect(run).toContain('git-2.25.5.tar.gz') // Why asserted: the sha256 check only runs on the build path, so a cached binary // must come from a key that pins the same version the tarball line declares. - expect(run).toContain('if [ -x "$source/git" ]; then') + expect(run).toContain('[ -x "$source/git" ] && [ -x "$source/git-submodule" ]') + expect(run).toContain('[ -f "$source/git-sh-setup" ] && [ -f "$source/git-sh-i18n" ]') + expect(run).toContain( + '[ -f "$source/git-parse-remote" ] && [ -x "$source/git-sh-i18n--envsubst" ]' + ) expect(run).toContain('41662c52fc16fec4963bfc41075e71f8ead6b5e386797eb6f9a1111ff95a8ddf') expect(run).toContain('-j"$(nproc)"') - expect(run).toContain('NO_GETTEXT=YesPlease NO_TCLTK=YesPlease NO_PYTHON=YesPlease git') + expect(run).toContain('NO_GETTEXT=YesPlease NO_TCLTK=YesPlease NO_PYTHON=YesPlease') + expect(run).toContain( + 'git git-submodule git-sh-setup git-sh-i18n git-parse-remote git-sh-i18n--envsubst' + ) expect(run).toContain('sha256sum --check') expect(run).toContain('find "$source" -name \'*.o\' -delete') // The cached path and the build path must be the same directory or the guard @@ -61,7 +72,7 @@ describe('Git binary compatibility PR gate', () => { expect(steps[matrixIndex].run).not.toContain('make -C') expect(baselineSteps[cacheIndex].with.path).toBe(BASELINE_DIR) expect(baselineSteps[cacheIndex].with.key).toBe( - 'git-compat-baseline-${{ runner.os }}-${{ runner.arch }}-2.25.5' + 'git-compat-baseline-${{ runner.os }}-${{ runner.arch }}-2.25.5-submodule' ) }) diff --git a/config/scripts/hang-watchdog-process-metrics.mjs b/config/scripts/hang-watchdog-process-metrics.mjs index 7e5ee4de53f5..ddee2806e23f 100644 --- a/config/scripts/hang-watchdog-process-metrics.mjs +++ b/config/scripts/hang-watchdog-process-metrics.mjs @@ -90,9 +90,10 @@ export async function sampleProductionPerformance(boundary, options) { heartbeatCount += 1 boundary.sendHeartbeat() }, options.heartbeatIntervalMs) - const cpuBefore = combinedCpuTimeMs(boundary.pids) - loopDelay.enable() + let cpuBefore try { + cpuBefore = combinedCpuTimeMs(boundary.pids) + loopDelay.enable() await options.sleep(options.sampleMs) } finally { loopDelay.disable() diff --git a/config/scripts/hang-watchdog-process-metrics.test.mjs b/config/scripts/hang-watchdog-process-metrics.test.mjs index 2d54e691d007..54943322a01b 100644 --- a/config/scripts/hang-watchdog-process-metrics.test.mjs +++ b/config/scripts/hang-watchdog-process-metrics.test.mjs @@ -1,9 +1,15 @@ -import { describe, expect, it } from 'vitest' +import childProcess from 'node:child_process' +import { syncBuiltinESMExports } from 'node:module' +import perfHooks from 'node:perf_hooks' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { parsePhysicalFootprintBytes, - parseProcessCpuTimeMs + parseProcessCpuTimeMs, + sampleProductionPerformance } from './hang-watchdog-process-metrics.mjs' +const observations = { events: [], readCpu: null, histogram: null } + describe('hang watchdog process metrics', () => { it('uses the de-duplicated summary for multiple processes', () => { const output = ` @@ -43,3 +49,203 @@ Electron [101]: 64-bit Footprint: 5000000 B (16384 bytes per page) expect(parseProcessCpuTimeMs('-1:00')).toBeNull() }) }) + +function cpuObservation(pid) { + return ['cpu', 'ps', ['-o', 'time=', '-p', String(pid)], { encoding: 'utf8' }] +} + +describe('production watchdog sample lifetime', () => { + beforeEach(() => { + vi.useFakeTimers() + observations.events = [] + let enabled = false + observations.histogram = { + enable() { + observations.events.push(['enable']) + const changed = !enabled + enabled = true + return changed + }, + disable() { + observations.events.push(['disable']) + const changed = enabled + enabled = false + return changed + }, + percentile: (value) => { + observations.events.push(['percentile', value]) + return value === 95 ? 1_900_000 : 3_100_000 + }, + max: 5_000_000 + } + vi.spyOn(childProcess, 'execFileSync').mockImplementation((command, args, options) => { + observations.events.push(['cpu', command, args, options]) + return observations.readCpu() + }) + vi.spyOn(perfHooks, 'monitorEventLoopDelay').mockImplementation((options) => { + observations.events.push(['monitor', options]) + return observations.histogram + }) + syncBuiltinESMExports() + }) + afterEach(() => { + vi.clearAllTimers() + vi.restoreAllMocks() + syncBuiltinESMExports() + vi.useRealTimers() + }) + + it.each([1, 2])('clears the heartbeat when initial CPU observation %s throws', async (failAt) => { + const error = new Error('PID observation failed') + let reads = 0 + observations.readCpu = () => { + if (++reads === failAt) { + throw error + } + return '0:01.20' + } + const sendHeartbeat = vi.fn() + const sleep = vi.fn() + await expect( + sampleProductionPerformance( + { pids: [101, 102], sendHeartbeat }, + { heartbeatIntervalMs: 2_000, sampleMs: 30_000, sleep } + ) + ).rejects.toBe(error) + expect(reads).toBe(failAt) + expect(sleep).not.toHaveBeenCalled() + expect(observations.events.filter(([kind]) => kind !== 'disable')).toEqual([ + ['monitor', { resolution: 10 }], + ...[101, 102].slice(0, failAt).map(cpuObservation) + ]) + expect(vi.getTimerCount()).toBe(0) + await vi.advanceTimersByTimeAsync(4_000) + expect(sendHeartbeat).not.toHaveBeenCalled() + }) + + it('preserves an invalid CPU diagnostic while releasing the heartbeat', async () => { + observations.readCpu = () => 'invalid CPU time' + const sendHeartbeat = vi.fn() + const sleep = vi.fn() + await expect( + sampleProductionPerformance( + { pids: [101], sendHeartbeat }, + { heartbeatIntervalMs: 2_000, sampleMs: 30_000, sleep } + ) + ).rejects.toThrow('Could not read CPU time for PID 101') + expect(sleep).not.toHaveBeenCalled() + expect(observations.events.filter(([kind]) => kind !== 'disable')).toEqual([ + ['monitor', { resolution: 10 }], + cpuObservation(101) + ]) + expect(vi.getTimerCount()).toBe(0) + await vi.advanceTimersByTimeAsync(4_000) + expect(sendHeartbeat).not.toHaveBeenCalled() + }) + + it('repeated failed owners leave no timers or later sends', async () => { + const error = new Error('sample CPU failure') + observations.readCpu = () => { + throw error + } + const sendHeartbeat = vi.fn() + const sleep = vi.fn() + for (let index = 0; index < 64; index++) { + await expect( + sampleProductionPerformance( + { pids: [101], sendHeartbeat }, + { heartbeatIntervalMs: 2_000, sampleMs: 30_000, sleep } + ) + ).rejects.toBe(error) + } + expect(sleep).not.toHaveBeenCalled() + expect(observations.events.filter(([kind]) => kind !== 'disable')).toEqual( + Array.from({ length: 64 }, () => [ + ['monitor', { resolution: 10 }], + cpuObservation(101) + ]).flat() + ) + expect(vi.getTimerCount()).toBe(0) + await vi.advanceTimersByTimeAsync(4_000) + expect(sendHeartbeat).not.toHaveBeenCalled() + }) + + async function runSample({ values, sleepError } = {}) { + const cpuValues = values ?? ['0:01.20', '0:02.30', '0:01.25', '0:02.35'] + let index = 0 + observations.readCpu = () => { + const value = cpuValues[index++] + if (value instanceof Error) { + throw value + } + return value + } + const sendHeartbeat = vi.fn(() => observations.events.push(['heartbeat'])) + const sleep = async (ms) => { + observations.events.push(['sleep', ms]) + await vi.advanceTimersByTimeAsync(ms) + if (sleepError) { + throw sleepError + } + } + return sampleProductionPerformance( + { pids: [101, 102], sendHeartbeat }, + { heartbeatIntervalMs: 2_000, sampleMs: 6_000, sleep } + ) + } + + const completedSampleEvents = [ + ['monitor', { resolution: 10 }], + cpuObservation(101), + cpuObservation(102), + ['enable'], + ['sleep', 6_000], + ['heartbeat'], + ['heartbeat'], + ['heartbeat'], + ['disable'] + ] + + it('keeps complete live results, observation order and heartbeat pacing', async () => { + expect(await runSample()).toEqual({ + cpuMs: 100, + heartbeatCount: 3, + eventLoopDelayP95Ms: 1.9, + eventLoopDelayP99Ms: 3.1, + eventLoopDelayMaxMs: 5 + }) + expect(observations.events).toEqual([ + ...completedSampleEvents, + cpuObservation(101), + cpuObservation(102), + ['percentile', 95], + ['percentile', 99] + ]) + expect(vi.getTimerCount()).toBe(0) + }) + + it('keeps a sample sleep rejection and its existing cleanup', async () => { + const error = new Error('sample sleep rejected') + await expect(runSample({ sleepError: error })).rejects.toBe(error) + expect(observations.events).toEqual(completedSampleEvents) + expect(vi.getTimerCount()).toBe(0) + }) + + it('keeps a final CPU observation failure after cleanup', async () => { + const error = new Error('final CPU read failed') + await expect(runSample({ values: ['0:01.20', '0:02.30', error] })).rejects.toBe(error) + expect(observations.events).toEqual([...completedSampleEvents, cpuObservation(101)]) + expect(vi.getTimerCount()).toBe(0) + }) + + it('keeps the zero floor when the CPU total decreases', async () => { + expect(await runSample({ values: ['0:02.20', '0:03.30', '0:01.25', '0:02.35'] })).toEqual({ + cpuMs: 0, + heartbeatCount: 3, + eventLoopDelayP95Ms: 1.9, + eventLoopDelayP99Ms: 3.1, + eventLoopDelayMaxMs: 5 + }) + expect(vi.getTimerCount()).toBe(0) + }) +}) diff --git a/config/scripts/headless-detector-compiler-cache.mjs b/config/scripts/headless-detector-compiler-cache.mjs new file mode 100644 index 000000000000..9ef4d5cb6b5d --- /dev/null +++ b/config/scripts/headless-detector-compiler-cache.mjs @@ -0,0 +1,160 @@ +import { createHash } from 'node:crypto' +import { + appendFileSync, + cpSync, + existsSync, + lstatSync, + mkdirSync, + readFileSync, + readdirSync, + rmSync, + symlinkSync, + writeFileSync +} from 'node:fs' +import { createRequire } from 'node:module' +import { dirname, join, resolve } from 'node:path' +import { pathToFileURL } from 'node:url' + +const ROOT = resolve(import.meta.dirname, '../..') + +function inventory(directory, prefix = '') { + if (lstatSync(directory).isSymbolicLink()) { + throw new Error('Compiler cache directory is a symlink') + } + return readdirSync(directory) + .flatMap((name) => { + const path = join(directory, name) + const file = `${prefix}${name}` + const stat = lstatSync(path) + if (stat.isSymbolicLink()) { + throw new Error('Compiler cache contains a symlink') + } + if (stat.isDirectory()) { + return inventory(path, `${file}/`) + } + if (!stat.isFile()) { + throw new Error('Compiler cache contains a special file') + } + return [{ file, sha256: createHash('sha256').update(readFileSync(path)).digest('hex') }] + }) + .sort((a, b) => a.file.localeCompare(b.file)) +} + +export function compilerCacheIdentity({ + policyHash = process.env.COMPILER_POLICY_HASH, + cacheRoot = process.env.RUNNER_TEMP, + platform = process.platform, + arch = process.arch, + node = process.version +} = {}) { + if (!policyHash || !cacheRoot) { + throw new Error('Compiler cache requires policy hash and cache root') + } + return { + key: `headless-compiler-v1-${platform}-${arch}-${node}-${policyHash}`, + path: join(cacheRoot, 'headless-detector-compiler') + } +} + +export function packCompilerCache({ root = ROOT, identity = compilerCacheIdentity() } = {}) { + const require = createRequire(join(root, 'package.json')) + const esbuildDir = dirname(require.resolve('esbuild/package.json')) + const nativeName = `@esbuild/${process.platform}-${process.arch}` + const nativeDir = dirname(require.resolve(`${nativeName}/package.json`, { paths: [esbuildDir] })) + rmSync(identity.path, { recursive: true, force: true }) + mkdirSync(join(identity.path, 'node_modules', '@esbuild'), { recursive: true }) + cpSync(esbuildDir, join(identity.path, 'node_modules', 'esbuild'), { + recursive: true, + dereference: true + }) + cpSync(nativeDir, join(identity.path, 'node_modules', nativeName), { + recursive: true, + dereference: true + }) + writeFileSync( + join(identity.path, 'manifest.json'), + JSON.stringify({ + key: identity.key, + node: process.version, + version: require('esbuild').version, + files: inventory(identity.path) + }) + ) +} + +export async function activateCompilerCache({ + root = ROOT, + identity = compilerCacheIdentity() +} = {}) { + const dependencies = join(root, 'node_modules') + let created = false + try { + if (existsSync(dependencies)) { + throw new Error('Compiler activation requires an empty dependency tree') + } + const files = inventory(identity.path).filter((row) => row.file !== 'manifest.json') + const manifest = JSON.parse(readFileSync(join(identity.path, 'manifest.json'), 'utf8')) + if ( + manifest.key !== identity.key || + manifest.node !== process.version || + JSON.stringify(files) !== JSON.stringify(manifest.files) + ) { + throw new Error('Compiler cache identity or contents differ') + } + mkdirSync(join(dependencies, '@esbuild'), { recursive: true }) + created = true + for (const name of ['esbuild', `@esbuild/${process.platform}-${process.arch}`]) { + symlinkSync(join(identity.path, 'node_modules', name), join(dependencies, name), 'dir') + } + const require = createRequire(join(root, 'package.json')) + const esbuild = require('esbuild') + if (esbuild.version !== manifest.version) { + throw new Error('Compiler API version differs') + } + await esbuild.build({ + stdin: { contents: 'export const value = 1' }, + write: false, + logLevel: 'silent' + }) + return { available: true } + } catch (error) { + if (created) { + rmSync(dependencies, { recursive: true, force: true }) + } + return { available: false, reason: String(error) } + } +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + const phase = process.argv[2] + const identity = + phase === 'identity' + ? compilerCacheIdentity() + : { + key: process.env.COMPILER_CACHE_KEY, + path: process.env.COMPILER_CACHE_PATH + } + if (!identity.key || !identity.path) { + throw new Error('Compiler cache identity required') + } + const output = (values) => { + for (const [name, value] of Object.entries(values)) { + appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`) + } + } + if (phase === 'identity') { + output(identity) + } else if (phase === 'pack') { + packCompilerCache({ identity }) + } else if (phase === 'activate') { + const result = await activateCompilerCache({ identity }) + output({ available: result.available }) + console.log( + result.available + ? 'Validated headless compiler cache' + : `Use normal dependency install: ${result.reason}` + ) + } else { + throw new Error('Expected identity, pack, or activate') + } +} diff --git a/config/scripts/headless-detector-compiler-cache.test.mjs b/config/scripts/headless-detector-compiler-cache.test.mjs new file mode 100644 index 000000000000..484807159a75 --- /dev/null +++ b/config/scripts/headless-detector-compiler-cache.test.mjs @@ -0,0 +1,204 @@ +import { + appendFileSync, + cpSync, + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + readdirSync, + rmSync, + symlinkSync, + writeFileSync +} from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, expect, it } from 'vitest' +import { parse } from 'yaml' +import { + activateCompilerCache, + compilerCacheIdentity, + packCompilerCache +} from './headless-detector-compiler-cache.mjs' +import { collectNodeServerInputs } from './node-server-change-scope.mjs' +import { runProcessSync } from './script-child-process.mjs' + +const temporary = [] +afterEach(() => { + for (const dir of temporary.splice(0)) { + rmSync(dir, { recursive: true, force: true }) + } +}) +function fixture() { + const directory = mkdtempSync(join(tmpdir(), 'headless-compiler-cache-')) + temporary.push(directory) + const root = join(directory, 'checkout') + mkdirSync(root) + writeFileSync(join(root, 'package.json'), '{"type":"module"}') + const identity = compilerCacheIdentity({ policyHash: 'policy', cacheRoot: directory }) + packCompilerCache({ identity }) + return { root, identity } +} +function changeManifest(identity, update) { + const path = join(identity.path, 'manifest.json') + const manifest = JSON.parse(readFileSync(path, 'utf8')) + update(manifest) + writeFileSync(path, JSON.stringify(manifest)) +} + +it('separates policy, Node, platform and architecture identities with the same archive path', () => { + const options = { + policyHash: 'policy', + cacheRoot: '/cache', + platform: 'linux', + arch: 'x64', + node: 'v24.21.0' + } + const original = compilerCacheIdentity(options) + for (const override of [ + { policyHash: 'changed' }, + { node: 'v24.22.0' }, + { platform: 'darwin' }, + { arch: 'arm64' } + ]) { + const other = compilerCacheIdentity({ ...options, ...override }) + expect(other.key).not.toBe(original.key) + expect(other.path).toBe(original.path) + } +}) + +it('activates only the actual compiler packages and preserves import graph behavior', async () => { + const { root, identity } = fixture() + expect(await activateCompilerCache({ root, identity })).toEqual({ available: true }) + expect(readdirSync(join(root, 'node_modules')).sort()).toEqual(['@esbuild', 'esbuild']) + for (const name of [ + 'node-server-change-scope', + 'node-server-test-paths', + 'node-server-qualification', + 'orcad-entry-build' + ]) { + mkdirSync(join(root, 'config', 'scripts'), { recursive: true }) + cpSync( + new URL(`./${name}.mjs`, import.meta.url), + join(root, 'config', 'scripts', `${name}.mjs`) + ) + } + writeFileSync( + join(root, 'entry.ts'), + "import './first'; export * from './exports'; import('./dynamic'); require('./required'); import 'external-package'; import './native.node'" + ) + for (const name of ['first', 'exports', 'dynamic', 'required']) { + writeFileSync(join(root, `${name}.ts`), 'export const value = 1') + } + writeFileSync( + join(root, 'probe.mjs'), + "import { collectNodeServerInputs } from './config/scripts/node-server-change-scope.mjs'; console.log(JSON.stringify([...(await collectNodeServerInputs({ root: process.cwd(), entryPoints: ['entry.ts'] }))].sort()))" + ) + const baseline = [...(await collectNodeServerInputs({ root, entryPoints: ['entry.ts'] }))].sort() + const candidate = runProcessSync({ + program: process.execPath, + args: ['probe.mjs'], + cwd: root, + timeoutMs: 10_000 + }) + expect(candidate.code, candidate.stderr).toBe(0) + expect(JSON.parse(candidate.stdout.trim())).toEqual(baseline) +}, 20_000) + +it.each(['key', 'node', 'version', 'files'])( + 'falls back on invalid manifest %s and cleans partial activation', + async (field) => { + const { root, identity } = fixture() + changeManifest(identity, (manifest) => { + manifest[field] = 'wrong' + }) + expect((await activateCompilerCache({ root, identity })).available).toBe(false) + expect(existsSync(join(root, 'node_modules'))).toBe(false) + } +) + +it.each(['modified', 'missing', 'extra', 'symlink', 'malformed'])( + 'falls back on %s cache contents before loading code', + async (kind) => { + const { root, identity } = fixture() + const compiler = join(identity.path, 'node_modules', 'esbuild', 'lib', 'main.js') + if (kind === 'modified') { + appendFileSync(compiler, '\nthrow Error("must not load")') + } + if (kind === 'missing') { + rmSync(compiler) + } + if (kind === 'extra') { + writeFileSync(join(identity.path, 'unexpected'), 'extra') + } + if (kind === 'symlink') { + rmSync(compiler) + symlinkSync(join(root, 'package.json'), compiler) + } + if (kind === 'malformed') { + writeFileSync(join(identity.path, 'manifest.json'), '{') + } + expect((await activateCompilerCache({ root, identity })).available).toBe(false) + expect(existsSync(join(root, 'node_modules'))).toBe(false) + } +) + +it('leaves existing dependencies alone and falls back on an absent archive', async () => { + const { root, identity } = fixture() + rmSync(identity.path, { recursive: true }) + expect((await activateCompilerCache({ root, identity })).available).toBe(false) + mkdirSync(join(root, 'node_modules')) + writeFileSync(join(root, 'node_modules', 'retained'), 'retained') + expect((await activateCompilerCache({ root, identity })).available).toBe(false) + expect(readFileSync(join(root, 'node_modules', 'retained'), 'utf8')).toBe('retained') +}) + +it('uses exact optional restores, seeds only main and retains full dependency fallback', () => { + const action = parse(readFileSync('.github/actions/prepare-headless-compiler/action.yml', 'utf8')) + const steps = action.runs.steps + const restore = steps.find((step) => step.id === 'cache') + expect(restore.uses).toBe('actions/cache/restore@v5') + expect(restore['continue-on-error']).toBe(true) + expect(restore.with['restore-keys']).toBeUndefined() + const save = steps.find((step) => step.uses === 'actions/cache/save@v5') + expect(save.if).toContain("github.ref == 'refs/heads/main'") + expect(save.if).toContain("github.event_name != 'pull_request'") + expect(save['continue-on-error']).toBe(true) + expect(save.with).toEqual(restore.with) + const workflow = parse(readFileSync('.github/workflows/node-server-tests.yml', 'utf8')) + const detector = workflow.jobs.changes.steps + const cached = detector.find((step) => step.id === 'compiler') + expect(cached.if).toBe("steps.scope.outputs.graph_required == 'true'") + expect(cached['continue-on-error']).toBe(true) + expect( + detector.find((step) => step.uses === './.github/actions/install-node-dependencies').if + ).toBe( + "steps.scope.outputs.graph_required == 'true' && steps.compiler.outputs.available != 'true'" + ) + for (const event of ['push', 'pull_request']) { + expect(workflow.on[event].paths).toContain('.github/actions/prepare-headless-compiler/**') + } + const warmer = parse(readFileSync('.github/workflows/ci-cache-warmup.yml', 'utf8')) + const warmSteps = warmer.jobs.warm.steps + expect( + warmSteps.findIndex((step) => step.uses === './.github/actions/prepare-headless-compiler') + ).toBeGreaterThan( + warmSteps.findIndex((step) => step.uses === './.github/actions/install-node-dependencies') + ) + for (const event of ['push', 'pull_request']) { + expect(warmer.on[event].paths).toContain('.github/actions/prepare-headless-compiler/**') + } + const inputs = [ + ...steps.find((step) => step.id === 'identity').env.COMPILER_POLICY_HASH.matchAll(/'([^']+)'/g) + ].map((match) => match[1]) + for (const input of inputs) { + expect( + warmer.on.push.paths.some( + (pattern) => + pattern === input || + (pattern.endsWith('/**') && input.startsWith(pattern.slice(0, -2))) || + (pattern.endsWith('*') && input.startsWith(pattern.slice(0, -1))) + ), + input + ).toBe(true) + } +}) diff --git a/config/scripts/install-node-dependencies-action.test.mjs b/config/scripts/install-node-dependencies-action.test.mjs index 3006a0063da9..46b925ccf07a 100644 --- a/config/scripts/install-node-dependencies-action.test.mjs +++ b/config/scripts/install-node-dependencies-action.test.mjs @@ -1,4 +1,12 @@ -import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { + chmodSync, + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + writeFileSync +} from 'node:fs' import { tmpdir } from 'node:os' import { delimiter, join } from 'node:path' import { spawnSync } from 'node:child_process' @@ -56,28 +64,38 @@ function executeInstallScript(fixture) { } describe('install-node-dependencies action', () => { - it.each(['/home/runner/pnpm store/v11', 'C:\\Users\\runner\\pnpm store\\v11'])( - 'preserves setup-node store path %s and lowercase architecture', - (storePath) => { - const fixture = createFixture() - const output = join(fixture.root, 'github-output') - try { - const result = run('bash', ['-e', '-o', 'pipefail', '-c', storeScript], { - env: { - ...process.env, - GITHUB_OUTPUT: output, - LOCKFILE_HASH: 'lockfile-digest', - PNPM_TEST_STORE_PATH: storePath, - PATH: `${fixture.bin}${delimiter}${process.env.PATH}` - } - }) - expect(result.status, result.stderr || result.stdout).toBe(0) - expect(readFileSync(output, 'utf8')).toBe(`path=${storePath}\narch=${process.arch}\n`) - } finally { - rmSync(fixture.root, { recursive: true, force: true }) + it.each([ + ['/home/runner/pnpm store/v11', 'true'], + ['/home/runner/pnpm store/v11', 'false'], + ['C:\\Users\\runner\\pnpm store\\v11', 'true'], + ['C:\\Users\\runner\\pnpm store\\v11', 'false'] + ])('preserves setup-node store path %s with producer lookup %s', (storePath, lookupOnly) => { + const fixture = createFixture() + const output = join(fixture.root, 'github-output') + const environment = join(fixture.root, 'github-env') + try { + const result = run('bash', ['-e', '-o', 'pipefail', '-c', storeScript], { + env: { + ...process.env, + GITHUB_OUTPUT: output, + GITHUB_ENV: environment, + STORE_LOOKUP_ONLY: lookupOnly, + LOCKFILE_HASH: 'lockfile-digest', + PNPM_TEST_STORE_PATH: storePath, + PATH: `${fixture.bin}${delimiter}${process.env.PATH}` + } + }) + expect(result.status, result.stderr || result.stdout).toBe(0) + expect(readFileSync(output, 'utf8')).toBe(`path=${storePath}\narch=${process.arch}\n`) + if (lookupOnly === 'true') { + expect(readFileSync(environment, 'utf8')).toBe(`ORCA_PNPM_STORE_CACHE_PATH=${storePath}\n`) + } else { + expect(existsSync(environment)).toBe(false) } + } finally { + rmSync(fixture.root, { recursive: true, force: true }) } - ) + }) it.each([ ['', 'store'], diff --git a/config/scripts/linear-sdk-runtime.test.mjs b/config/scripts/linear-sdk-runtime.test.mjs new file mode 100644 index 000000000000..896b8c7b7d59 --- /dev/null +++ b/config/scripts/linear-sdk-runtime.test.mjs @@ -0,0 +1,78 @@ +import { mkdtemp, rm } from 'node:fs/promises' +import { createRequire } from 'node:module' +import { join, resolve } from 'node:path' +import { build } from 'vite' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import { electronViteConfig } from '../../electron.vite.config' +import { runProcess } from '../../src/shared/child-process/run-process' + +const projectDir = resolve(import.meta.dirname, '../..') +const require = createRequire(import.meta.url) +let outputDir + +const smokeScript = ` + const assert = require('node:assert/strict') + const { createRequire } = require('node:module') + const loaderPath = process.argv[1] + const requireFromLoader = createRequire(loaderPath) + const sdkEntry = requireFromLoader.resolve('@linear/sdk') + const { loadLinearSdk } = require(loaderPath) + assert.equal(require.cache[sdkEntry], undefined, 'SDK must remain lazy') + + const sdk = loadLinearSdk() + assert.equal(sdk, requireFromLoader('@linear/sdk')) + assert.equal(loadLinearSdk(), sdk, 'repeat loads must reuse the SDK') + const client = new sdk.LinearClient({ apiKey: 'orca-offline-smoke-test' }) + assert.equal(typeof client.issues, 'function') + assert.equal(typeof client.teams, 'function') + assert.ok(sdk.AuthenticationLinearError.prototype instanceof Error) + console.log('Linear SDK loaded') +` + +beforeAll(async () => { + // Keep normal dependency resolution while executing outside Vitest's module loader. + outputDir = await mkdtemp(join(projectDir, 'node_modules', 'orca-linear-sdk-runtime-')) + const mainBuild = electronViteConfig.main.build + await build({ + configFile: false, + publicDir: false, + logLevel: 'silent', + build: { + ssr: true, + minify: mainBuild.minify, + outDir: outputDir, + rollupOptions: { + external: mainBuild.rollupOptions.external, + input: join(projectDir, 'src/main/linear/linear-sdk.ts'), + output: { format: 'cjs', entryFileNames: 'linear-sdk.cjs' } + } + } + }) +}) + +afterAll(async () => { + if (outputDir) { + await rm(outputDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }) + } +}) + +describe('resolved Linear SDK through the production CommonJS loader', () => { + it.each([ + { name: 'Node', program: process.execPath }, + { name: 'Electron', program: require('electron') } + ])( + 'loads and constructs the real SDK under $name without network access', + async ({ program }) => { + const result = await runProcess({ + program, + args: ['-e', smokeScript, join(outputDir, 'linear-sdk.cjs')], + cwd: projectDir, + env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1', ELECTRON_RUN_AS_NODE: '1' }, + timeoutMs: 20000 + }) + expect(result.code, result.stderr).toBe(0) + expect(result.timedOut).toBe(false) + expect(result.stdout.trim()).toBe('Linear SDK loaded') + } + ) +}) diff --git a/config/scripts/locale-translation-policy.mjs b/config/scripts/locale-translation-policy.mjs index 75f50d15ff62..08b331918919 100644 --- a/config/scripts/locale-translation-policy.mjs +++ b/config/scripts/locale-translation-policy.mjs @@ -52,6 +52,7 @@ export const NEVER_TRANSLATE_VALUES = new Set([ 'Goose', 'Grok', 'Hermes', + 'Jcode', 'Jira', 'Kilocode', 'Kimi', @@ -79,6 +80,7 @@ export const NEVER_TRANSLATE_VALUES = new Set([ 'markdown', 'gh', 'idle', + 'jcode', 'anthropic', 'Discord', 'WSL', diff --git a/config/scripts/managed-data-account-runtime.test.ts b/config/scripts/managed-data-account-runtime.test.ts new file mode 100644 index 000000000000..452629732ac6 --- /dev/null +++ b/config/scripts/managed-data-account-runtime.test.ts @@ -0,0 +1,106 @@ +import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { createRequire } from 'node:module' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' +import { resolveConfig } from 'electron-vite' +import { build } from 'vite' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import { runProcess } from '../../src/shared/child-process/run-process' + +const projectDir = resolve(import.meta.dirname, '../..') +const require = createRequire(import.meta.url) +let outputDir: string + +beforeAll(async () => { + outputDir = mkdtempSync(join(tmpdir(), 'orca-account-runtime-')) + const resolved = await resolveConfig( + { configFile: join(projectDir, 'electron.vite.config.ts') }, + 'build', + 'production' + ) + const main = resolved.config?.main + if (!main?.build) { + throw new Error('Expected main-process build config') + } + await build({ + ...main, + logLevel: 'silent', + build: { + ...main.build, + outDir: join(outputDir, 'bundle'), + sourcemap: false, + rollupOptions: { + ...main.build.rollupOptions, + input: join(projectDir, 'src/main/managed-data-accounts/credential-capture.ts'), + output: { format: 'cjs', entryFileNames: 'credential-capture.cjs' } + } + } + }) + mkdirSync(join(outputDir, 'source', 'devin'), { recursive: true }) + writeFileSync( + join(outputDir, 'source', 'devin', 'credentials.toml'), + 'windsurf_api_key = "offline-account-runtime-fixture"\n' + ) +}) + +afterAll(() => { + if (outputDir) { + rmSync(outputDir, { recursive: true, force: true }) + } +}) + +describe('managed account credentials in the production main bundle', () => { + it.each([ + { name: 'Node', program: process.execPath }, + { name: 'Electron', program: require('electron') } + ])( + 'captures Devin credentials under $name outside the dependency install', + async ({ name, program }) => { + const environment: Record = { + ...process.env, + ORCA_BACKGROUND_LAUNCH: '1', + ELECTRON_RUN_AS_NODE: '1' + } + for (const key of [ + 'HOME', + 'XDG_CONFIG_HOME', + 'XDG_DATA_HOME', + 'XDG_STATE_HOME', + 'XDG_CACHE_HOME' + ]) { + const directory = join(outputDir, name, key) + mkdirSync(directory, { recursive: true }) + environment[key] = directory + } + const script = ` + const assert = require('node:assert/strict') + const { captureDataAccountCredentials } = require(process.argv[1]) + captureDataAccountCredentials('devin', process.argv[2], process.argv[3]) + .then((integrations) => { + assert.deepEqual(integrations, ['devin']) + console.log('Private credentials captured') + }).catch((error) => { console.error(error); process.exitCode = 1 }) + ` + const destination = join(outputDir, name, 'captured') + const result = await runProcess({ + program, + args: [ + '-e', + script, + join(outputDir, 'bundle', 'credential-capture.cjs'), + join(outputDir, 'source'), + destination + ], + cwd: outputDir, + env: environment, + timeoutMs: 20000 + }) + expect(result.code, result.stderr).toBe(0) + expect(result.timedOut).toBe(false) + expect(result.stdout.trim()).toBe('Private credentials captured') + expect(readFileSync(join(destination, 'devin', 'credentials.toml'), 'utf8')).toContain( + 'offline-account-runtime-fixture' + ) + } + ) +}) diff --git a/config/scripts/mobile-entry-static-closure-work-budget.test.mjs b/config/scripts/mobile-entry-static-closure-work-budget.test.mjs new file mode 100644 index 000000000000..4064afe4068c --- /dev/null +++ b/config/scripts/mobile-entry-static-closure-work-budget.test.mjs @@ -0,0 +1,120 @@ +import { expect, it } from 'vitest' +import { entryStaticClosure } from './build-mobile-web-app-bundle.mjs' + +it.each([1, 12, 128, 1000])( + 'keeps the complete %s-output closure without shifting a second queue', + (count) => { + const paths = Array.from({ length: count }, (_value, index) => `dist/chunk-${index}.js`) + const metafile = { + outputs: Object.fromEntries( + paths.map((path, index) => [ + path, + { + bytes: index + 1, + imports: + index === 0 + ? paths.slice(1).map((child) => ({ kind: 'import-statement', path: child })) + : [] + } + ]) + ) + } + const before = structuredClone(metafile) + const originalShift = Array.prototype.shift + let reached + let shifts = 0 + Array.prototype.shift = function () { + shifts++ + return originalShift.call(this) + } + try { + reached = entryStaticClosure(metafile, paths[0]) + } finally { + Array.prototype.shift = originalShift + } + expect([...reached]).toEqual(paths) + expect(metafile).toEqual(before) + expect([...reached].reduce((total, path) => total + metafile.outputs[path].bytes, 0)).toBe( + (count * (count + 1)) / 2 + ) + const fresh = entryStaticClosure(metafile, paths[0]) + expect(fresh).not.toBe(reached) + reached.add('mutated-result') + expect([...fresh]).toEqual(paths) + expect(shifts).toBe(0) + } +) + +it('keeps breadth-first order, duplicates, cycles, missing chunks and dynamic boundaries', () => { + const metafile = { + outputs: { + entry: { + imports: [ + { kind: 'import-statement', path: 'one' }, + { kind: 'dynamic-import', path: 'deferred' }, + { kind: 'import-statement', path: 'two' }, + { kind: 'import-statement', path: 'one' } + ] + }, + one: { imports: [{ kind: 'import-statement', path: 'three' }] }, + two: { + imports: [ + { kind: 'import-statement', path: 'entry' }, + { kind: 'import-statement', path: 'three' }, + { kind: 'import-statement', path: 'missing' } + ] + }, + three: { imports: [] }, + deferred: { imports: [{ kind: 'import-statement', path: 'deferred-child' }] } + } + } + expect([...entryStaticClosure(metafile, 'entry')]).toEqual([ + 'entry', + 'one', + 'two', + 'three', + 'missing' + ]) + metafile.outputs.one.imports.push({ kind: 'import-statement', path: 'fresh-😀' }) + expect([...entryStaticClosure(metafile, 'entry')]).toEqual([ + 'entry', + 'one', + 'two', + 'three', + 'fresh-😀', + 'missing' + ]) + expect([...entryStaticClosure({ outputs: {} }, 'unknown')]).toEqual(['unknown']) +}) + +it('keeps a later output error after earlier discoveries in the same order', () => { + const error = new Error('later-output') + const reads = [] + const outputs = { + get entry() { + reads.push('entry') + return { + imports: [ + { kind: 'import-statement', path: 'one' }, + { kind: 'import-statement', path: 'two' } + ] + } + }, + get one() { + reads.push('one') + return { imports: [{ kind: 'import-statement', path: 'three' }] } + }, + get two() { + reads.push('two') + throw error + } + } + let caught + try { + entryStaticClosure({ outputs }, 'entry') + } catch (failure) { + caught = failure + } + expect(caught).toBe(error) + expect(reads).toEqual(['entry', 'one', 'two']) +}) diff --git a/config/scripts/mobile-release-shell-switch-workflow.test.mjs b/config/scripts/mobile-release-shell-switch-workflow.test.mjs index f6f05c779ebf..5f45d6872ac4 100644 --- a/config/scripts/mobile-release-shell-switch-workflow.test.mjs +++ b/config/scripts/mobile-release-shell-switch-workflow.test.mjs @@ -120,9 +120,10 @@ const BUNDLER_CACHE_PATHS = ['metro-cache', '.expo', 'node_modules/.cache'] const REVIEWED_COMPUTED_PATHS = [ '${{ steps.electron-package-cache.outputs.cache-root }}', '${{ steps.pnpm-store.outputs.path }}', + '${{ env.ORCA_PNPM_STORE_CACHE_PATH }}', // Only pnpm's lockfile-verified.jsonl record, never Metro transforms. '${{ steps.verification-cache.outputs.path }}', - "${{ github.event_name != 'pull_request' && 'pnpm' || '' }} store" + "${{ github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && steps.pnpm-store-mode.outputs.lookup-only != 'true' && 'pnpm' || '' }} store" ] /** Every step a workflow runs, descending into the repository's own composite actions. */ diff --git a/config/scripts/mobile-typecheck-workflow.test.mjs b/config/scripts/mobile-typecheck-workflow.test.mjs new file mode 100644 index 000000000000..0ab57ad858f7 --- /dev/null +++ b/config/scripts/mobile-typecheck-workflow.test.mjs @@ -0,0 +1,42 @@ +import { readFileSync } from 'node:fs' +import { describe, expect, it } from 'vitest' +import { parse } from 'yaml' + +const workflow = parse(readFileSync('.github/workflows/mobile.yml', 'utf8')) +const packageJson = JSON.parse(readFileSync('mobile/package.json', 'utf8')) +const job = workflow.jobs.verify +const steps = job.steps + +describe('mobile verification command ownership', () => { + it('runs the declared checks through installed tools without a script-time install', () => { + const production = steps.find((step) => step.name === 'Typecheck') + const tests = steps.find((step) => step.name === 'Typecheck tests (ratchet)') + + expect(packageJson.scripts.typecheck).toMatch(/^tsc\b/) + expect(production.run).toBe(`node node_modules/typescript/bin/${packageJson.scripts.typecheck}`) + expect(tests.run).toBe(packageJson.scripts['check:tests-typecheck']) + expect(tests.run).toMatch(/^node\s/) + expect(job.defaults.run['working-directory']).toBe('mobile') + for (const name of ['typecheck', 'check:tests-typecheck']) { + expect(packageJson.scripts[`pre${name}`]).toBeUndefined() + expect(packageJson.scripts[`post${name}`]).toBeUndefined() + } + }) + + it('finishes installation and joins production types before checking test types', () => { + const installIndex = steps.findIndex((step) => step.name === 'Install dependencies') + const productionIndex = steps.findIndex((step) => step.name === 'Typecheck') + const ratchetIndex = steps.findIndex((step) => step.name === 'Typecheck tests (ratchet)') + const waitIndex = steps.findIndex((step) => step.wait === steps[productionIndex].id) + const testIndex = steps.findIndex((step) => step.name === 'Test') + + expect(steps[installIndex].run).toBe('pnpm install --frozen-lockfile') + expect(steps[installIndex].background ?? false).toBe(false) + expect(installIndex).toBeLessThan(productionIndex) + expect(steps[productionIndex].background).toBe(true) + expect(productionIndex).toBeLessThan(ratchetIndex) + expect(waitIndex).toBeGreaterThan(productionIndex) + expect(waitIndex).toBeLessThan(ratchetIndex) + expect(ratchetIndex).toBeLessThan(testIndex) + }) +}) diff --git a/config/scripts/mobile-web-app-html-preview-render.test.mjs b/config/scripts/mobile-web-app-html-preview-render.test.mjs index 48f974864876..9f78cc33a163 100644 --- a/config/scripts/mobile-web-app-html-preview-render.test.mjs +++ b/config/scripts/mobile-web-app-html-preview-render.test.mjs @@ -611,7 +611,13 @@ for (const engine of ['chromium', 'webkit']) { await frame?.click('#fraglink', { timeout: 2000 }) } - const shown = await open(browser(), { signal: ctx.signal, extra: tall, act: tapFragment }) + const shown = await open(browser(), { + signal: ctx.signal, + extra: tall, + act: tapFragment, + reportAfterAct: 'frame-src' + }) + expect(shown.actError).toBeNull() // The precondition the whole case rests on: the base URL is the embedder's, which is what // makes a fragment resolve off-document here. expect(shown.inside?.baseUri ?? shown.mountedSrcDoc).toBeTruthy() diff --git a/config/scripts/mobile-web-app-preview-arm-driver.mjs b/config/scripts/mobile-web-app-preview-arm-driver.mjs index c09eb00be65c..6ab780587558 100644 --- a/config/scripts/mobile-web-app-preview-arm-driver.mjs +++ b/config/scripts/mobile-web-app-preview-arm-driver.mjs @@ -4,6 +4,8 @@ import { recordRequestsTo } from './mobile-web-app-preview-request-log.mjs' import { watchImageEvidence } from './mobile-web-app-preview-image-evidence.mjs' import { artifact } from './mobile-web-app-preview-artifact-fixture.mjs' +import { pollReportsUntil } from './mobile-web-app-preview-csp-reports.mjs' +import { describePreviewFrame, untilAborted } from './mobile-web-app-preview-frame-diagnosis.mjs' import { previewFrame, settleAfterMount, @@ -42,6 +44,7 @@ export async function openPreviewArm( assets, doctype, reportReady = null, + reportAfterAct = null, /** What the shell told this page it may do. Defaults to the session route's own list, so an arm * that does not mention it measures the shipped screen (C8.1). */ grants = null, @@ -199,6 +202,15 @@ export async function openPreviewArm( // write, and the bounded wait says so rather than leaving a bare timeout. actError }) + // A refusal caused by the tap arrives after mount readiness. + if (reportAfterAct && !actError) { + await untilAborted( + pollReportsUntil(cspReports, nonce, reportAfterAct, signal), + signal, + async () => + `the policy reported no ${String(reportAfterAct)} refusal after the action: ${arm} | ${await describePreviewFrame(page, previewFrame(page), browserVersion)}` + ) + } const result = await readPreviewArm({ page, clip, diff --git a/config/scripts/moved-code-normalization-budget.test.mjs b/config/scripts/moved-code-normalization-budget.test.mjs new file mode 100644 index 000000000000..cd2e729608c4 --- /dev/null +++ b/config/scripts/moved-code-normalization-budget.test.mjs @@ -0,0 +1,164 @@ +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it, vi } from 'vitest' +import * as quality from './check-changed-code-quality.mjs' + +const processCalls = vi.hoisted(() => ({ execFileSync: vi.fn(), spawnSync: vi.fn() })) +// oxlint-disable-next-line anti-slop/no-module-mocking -- Test-only external process boundary; actual main executes. +vi.mock('node:child_process', () => processCalls) +// oxlint-disable-next-line anti-slop/no-module-mocking -- Resolve only the external executable used by this fixture. +vi.mock('./oxlint-cli-invocation.mjs', () => ({ + resolveOxlintInvocation: () => ({ command: 'fixture-oxlint', prefixArgs: [] }) +})) + +afterEach(() => { + vi.restoreAllMocks() + vi.unstubAllEnvs() +}) + +const createMatcher = (blocks) => + quality.createMovedCodeMatcher?.(blocks) ?? ((lines) => quality.isMovedCode(lines, blocks)) + +describe('moved-code base normalization budget', () => { + it('normalizes fixed base lines once across repeated changed-line diagnostics', () => { + const root = mkdtempSync(join(tmpdir(), 'orca-moved-code-budget-')) + const file = join(root, 'fixture.mjs') + const source = 'brandNewCall()\n' + writeFileSync(file, source) + const blocks = [Array.from({ length: 5000 }, (_, index) => ` base_line_${index}() `)] + const matcher = createMatcher(blocks) + const ranges = new Map([['fixture.mjs', [{ start: 1, end: 1 }]]]) + const diagnostic = { + filename: file, + labels: [{ span: { line: 1, offset: 0, length: source.length - 1 } }] + } + const original = String.prototype.replace + let normalizedBaseLines = 0 + const spy = vi.spyOn(String.prototype, 'replace').mockImplementation(function (...args) { + if (String(this).startsWith(' base_line_')) { + normalizedBaseLines += 1 + } + return original.apply(this, args) + }) + try { + const results = Array.from({ length: 100 }, () => + quality.diagnosticTouchesAddedLines(diagnostic, ranges, root, blocks, matcher) + ) + spy.mockRestore() + expect(results).toEqual(Array(100).fill(true)) + expect(normalizedBaseLines).toBe(5000) + } finally { + spy.mockRestore() + rmSync(root, { recursive: true, force: true }) + } + }) + + it('keeps the existing exemption decisions across mixed repeated highlights', () => { + const body = Array.from({ length: 20 }, (_, index) => `line${index}()`) + const blocks = [[' a() ', '', '\tb()'], body, ['first()', 'last()']] + const matcher = createMatcher(blocks) + const observations = [ + { lines: ['a()', ' ', 'b()'], moved: true }, + { lines: ['', ' '], moved: false }, + { lines: ['brandNewCall()', 'a()'], moved: false }, + { lines: ['first()', 'brandNewCall()'], moved: false }, + { lines: [...body.slice(0, 19), 'newDep,', body[19]], moved: true }, + { + lines: ['line0()', ...Array.from({ length: 18 }, (_, index) => `fresh${index}()`)], + moved: false + }, + { lines: ['b()', 'a()'], moved: false } + ] + for (let repeat = 0; repeat < 10; repeat += 1) { + for (const { lines, moved } of observations) { + expect(matcher(lines)).toBe(moved) + expect(quality.isMovedCode(lines, blocks)).toBe(moved) + } + } + }) + + it('does not normalize unvisited blocks and recomputes for the next invocation', () => { + const unused = [' base_line_unused() '] + const blocks = [['first()'], unused] + const matcher = createMatcher(blocks) + const original = String.prototype.replace + let unusedReads = 0 + const spy = vi.spyOn(String.prototype, 'replace').mockImplementation(function (...args) { + if (String(this).startsWith(' base_line_')) { + unusedReads += 1 + } + return original.apply(this, args) + }) + try { + expect(matcher([])).toBe(false) + expect(matcher(['first()'])).toBe(true) + expect(unusedReads).toBe(0) + } finally { + spy.mockRestore() + } + blocks[0][0] = 'changed()' + const nextMatcher = createMatcher(blocks) + expect(nextMatcher(['changed()'])).toBe(true) + expect(nextMatcher(['first()'])).toBe(false) + expect(quality.isMovedCode(['changed()'], blocks)).toBe(true) + }) + + it('shares the matcher across diagnostics and scans in the actual main entrypoint', () => { + const root = mkdtempSync(join(tmpdir(), 'orca-moved-code-main-')) + const file = join(root, 'fixture.mjs') + writeFileSync(file, 'brandNewCall()\n') + vi.stubEnv('GITHUB_EVENT_NAME', '') + processCalls.execFileSync.mockImplementation((_command, args) => { + if (args[0] === 'rev-list') { + return 'head parent\n' + } + if (args[0] === 'merge-base') { + return 'baseline\n' + } + if (args[0] === 'ls-files') { + return '' + } + if (args.includes('--name-only')) { + return 'fixture.mjs\0' + } + if (args.includes('--unified=0')) { + return '@@ -0,0 +1 @@\n+brandNewCall()\n' + } + throw new Error(`Unexpected Git arguments: ${args.join(' ')}`) + }) + const baseline = Array.from({ length: 1000 }, (_, index) => `base_line_${index}()`) + const diagnostics = Array.from({ length: 10 }, () => ({ + filename: file, + message: 'New code finding', + labels: [{ span: { line: 1, offset: 0, length: 14 } }] + })) + processCalls.spawnSync.mockImplementation((command, args) => { + if (command === 'git') { + return { status: 0, stdout: args[0] === 'show' ? baseline.join('\n') : '' } + } + return { status: 1, stdout: JSON.stringify({ diagnostics }), stderr: '' } + }) + const error = vi.spyOn(console, 'error').mockImplementation(() => {}) + vi.spyOn(console, 'log').mockImplementation(() => {}) + const replace = String.prototype.replace + let normalizedBaseLines = 0 + vi.spyOn(String.prototype, 'replace').mockImplementation(function (...args) { + if (String(this).startsWith('base_line_')) { + normalizedBaseLines += 1 + } + return replace.apply(this, args) + }) + try { + expect(quality.main(root, 'baseline')).toBe(1) + const scans = processCalls.spawnSync.mock.calls.filter(([command]) => command !== 'git') + expect(scans).toHaveLength(quality.OXLINT_SCANS.length) + expect(error.mock.calls.filter(([message]) => message.startsWith('::error '))).toHaveLength( + diagnostics.length * quality.OXLINT_SCANS.length + ) + expect(normalizedBaseLines).toBe(2000) + } finally { + rmSync(root, { recursive: true, force: true }) + } + }) +}) diff --git a/config/scripts/msbuild-file-tracking.mjs b/config/scripts/msbuild-file-tracking.mjs new file mode 100644 index 000000000000..0a2fac8b8043 --- /dev/null +++ b/config/scripts/msbuild-file-tracking.mjs @@ -0,0 +1,14 @@ +// FileTracker's long-path-unsafe .tlog files serve incremental builds; these rebuilds are forced. +export function disableMsbuildFileTrackingOnWindows( + env = process.env, + platform = process.platform +) { + // Windows environment keys are case-insensitive, including caller overrides in copied objects. + if ( + platform === 'win32' && + !Object.keys(env).some((key) => key.toLowerCase() === 'trackfileaccess') + ) { + env.TrackFileAccess = 'false' + } + return env +} diff --git a/config/scripts/msbuild-file-tracking.test.mjs b/config/scripts/msbuild-file-tracking.test.mjs new file mode 100644 index 000000000000..fdde02db25c4 --- /dev/null +++ b/config/scripts/msbuild-file-tracking.test.mjs @@ -0,0 +1,28 @@ +import { describe, expect, it } from 'vitest' +import { disableMsbuildFileTrackingOnWindows } from './msbuild-file-tracking.mjs' + +describe('disableMsbuildFileTrackingOnWindows', () => { + it('turns tracking off on Windows when the caller left it unset', () => { + expect(disableMsbuildFileTrackingOnWindows({ PATH: 'x' }, 'win32')).toEqual({ + PATH: 'x', + TrackFileAccess: 'false' + }) + }) + + it.each(['TrackFileAccess', 'trackfileaccess', 'TRACKFILEACCESS', 'tRaCkFiLeAcCeSs'])( + 'preserves explicit %s values without adding a duplicate key', + (key) => { + for (const value of ['true', 'false', '']) { + const env = { [key]: value } + expect(disableMsbuildFileTrackingOnWindows(env, 'win32')).toBe(env) + expect(env).toEqual({ [key]: value }) + } + } + ) + + it.each(['linux', 'darwin'])('leaves %s hosts alone', (platform) => { + const env = { PATH: 'x' } + expect(disableMsbuildFileTrackingOnWindows(env, platform)).toBe(env) + expect(env).toEqual({ PATH: 'x' }) + }) +}) diff --git a/config/scripts/node-server-change-scope.mjs b/config/scripts/node-server-change-scope.mjs index 7badba9fa982..71e788867e1b 100644 --- a/config/scripts/node-server-change-scope.mjs +++ b/config/scripts/node-server-change-scope.mjs @@ -1,12 +1,6 @@ -import { build } from 'esbuild' import { appendFileSync, globSync, readFileSync } from 'node:fs' import { resolve } from 'node:path' import { pathToFileURL } from 'node:url' -import { - externalNativeAddons, - ORCAD_CHILD_ENTRY_POINTS, - ORCAD_ENTRY_POINT -} from './orcad-entry-build.mjs' import { nodeServerTestPaths } from './node-server-test-paths.mjs' import { nodeServerQualification } from './node-server-qualification.mjs' @@ -37,11 +31,14 @@ const ALWAYS_FILES = new Set([ '.github/workflows/node-server-tests.yml', 'config/scripts/node-server-change-scope.mjs', 'config/scripts/node-server-change-scope.test.mjs', + 'config/scripts/headless-detector-compiler-cache.mjs', 'config/scripts/node-server-qualification.mjs', 'config/scripts/node-server-qualification.test.mjs' ]) const ALWAYS_PREFIXES = [ '.github/actions/install-node-dependencies/', + '.github/actions/restore-pnpm-verification/', + '.github/actions/prepare-headless-compiler/', '.github/actions/prepare-native-runtime/', '.github/actions/prepare-orcad-prebuilds/', // These areas also contain worker paths and fixtures opened without an import. @@ -68,6 +65,8 @@ export function discoverNodeServerTests(root = ROOT) { } export async function collectNodeServerInputs({ root = ROOT, entryPoints } = {}) { + const [{ build }, { externalNativeAddons, ORCAD_CHILD_ENTRY_POINTS, ORCAD_ENTRY_POINT }] = + await Promise.all([import('esbuild'), import('./orcad-entry-build.mjs')]) const entries = entryPoints ?? [ ORCAD_ENTRY_POINT, ...Object.values(ORCAD_CHILD_ENTRY_POINTS), @@ -99,7 +98,11 @@ export async function collectNodeServerInputs({ root = ROOT, entryPoints } = {}) ) } -export async function classifyNodeServerChanges(changedFiles, collect = collectNodeServerInputs) { +export async function classifyNodeServerChanges( + changedFiles, + collect = collectNodeServerInputs, + { deferGraph = false } = {} +) { if (changedFiles.length === 0) { return { shouldRun: true, reason: 'No complete changed-file evidence' } } @@ -113,6 +116,9 @@ export async function classifyNodeServerChanges(changedFiles, collect = collectN if (forced) { return { shouldRun: true, reason: `Build or CI input changed: ${forced}` } } + if (deferGraph) { + return { graphRequired: true, reason: 'Installed dependencies are needed to check imports' } + } try { const inputs = await collect() const matched = changedFiles.find((file) => inputs.has(file)) @@ -133,12 +139,16 @@ export async function classifyNodeServerChanges(changedFiles, collect = collectN if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { const changedFiles = readFileSync(process.argv[2], 'utf8').split('\0').filter(Boolean) - const result = await classifyNodeServerChanges(changedFiles) + const result = await classifyNodeServerChanges(changedFiles, collectNodeServerInputs, { + deferGraph: process.argv.includes('--defer-graph') + }) console.log(result.reason) const policy = nodeServerQualification(changedFiles, result, { fullQualification: process.argv.includes('--full-qualification') }) - const output = `should_run=${result.shouldRun}\nqualification=${policy.qualification}\nrunners=${JSON.stringify(policy.runners)}\n` + const output = result.graphRequired + ? 'graph_required=true\n' + : `should_run=${result.shouldRun}\nqualification=${policy.qualification}\nrunners=${JSON.stringify(policy.runners)}\n` if (process.env.GITHUB_OUTPUT) { appendFileSync(process.env.GITHUB_OUTPUT, output) } else { diff --git a/config/scripts/node-server-change-scope.test.mjs b/config/scripts/node-server-change-scope.test.mjs index 0ff118ed6783..4216beb25567 100644 --- a/config/scripts/node-server-change-scope.test.mjs +++ b/config/scripts/node-server-change-scope.test.mjs @@ -1,4 +1,4 @@ -import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { mkdtempSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs' import { join } from 'node:path' import { tmpdir } from 'node:os' import { afterEach, beforeAll, describe, expect, it } from 'vitest' @@ -11,6 +11,8 @@ import { import { nodeServerTestPaths } from './node-server-test-paths.mjs' import { ORCAD_CHILD_ENTRY_POINTS } from './orcad-entry-build.mjs' import { NODE_RUNTIME_PIN } from '../../src/shared/node-runtime-pin.ts' +import { runProcessSync } from './script-child-process.mjs' +import { NODE_SERVER_RUNNERS } from './node-server-qualification.mjs' const temporaryDirs = [] afterEach(() => { @@ -83,6 +85,9 @@ it.each([ 'config/patches/node-pty@1.1.0.patch', 'native/windows-registry/src/addon.cc', '.github/actions/install-node-dependencies/action.yml', + '.github/actions/restore-pnpm-verification/action.yml', + '.github/actions/prepare-headless-compiler/action.yml', + 'config/scripts/headless-detector-compiler-cache.mjs', '.github/actions/prepare-native-runtime/action.yml', '.github/actions/prepare-orcad-prebuilds/action.yml', '.github/workflows/node-server-tests.yml', @@ -91,15 +96,99 @@ it.each([ expect((await classifyNodeServerChanges([file], async () => new Set())).shouldRun).toBe(true) }) +it('defers uncertain paths without issuing a qualification verdict', async () => { + const result = await classifyNodeServerChanges( + ['src/renderer/src/example.ts'], + async () => { + throw new Error('graph must not run before installation') + }, + { deferGraph: true } + ) + expect(result.graphRequired).toBe(true) + expect(result.shouldRun).toBeUndefined() +}) + +it.each([ + { files: [], deferred: true, expected: 'should_run=true' }, + { files: ['package.json'], deferred: true, expected: 'should_run=true' }, + { + files: ['src/main/persistence/profile-state/profile-state-windows.ts'], + deferred: true, + fullQualification: false, + qualification: false, + runners: ['ubuntu-22.04', 'windows-2022', 'windows-11-arm'], + expected: 'should_run=true' + }, + { + files: ['src/main/persistence/profile-state/profile-state-windows.ts'], + deferred: true, + expected: 'should_run=true' + }, + { + files: ['src/main/providers/provider-windows.ts'], + deferred: true, + fullQualification: false, + expected: 'should_run=true' + }, + { files: ['src/renderer/src/example.ts'], deferred: true, expected: 'graph_required=true' }, + { files: ['src/renderer/src/example.ts'], deferred: false, expected: 'should_run=true' } +])('fails closed or requests dependencies in an uninstalled checkout: %j', (scenario) => { + const root = moduleTree( + Object.fromEntries( + ['node-server-change-scope', 'node-server-test-paths', 'node-server-qualification'].map( + (name) => [ + `config/scripts/${name}.mjs`, + readFileSync(new URL(`./${name}.mjs`, import.meta.url), 'utf8') + ] + ) + ) + ) + const changes = join(root, 'changes') + const stepOutput = join(root, 'step-output') + writeFileSync(changes, scenario.files.map((file) => `${file}\0`).join('')) + const result = runProcessSync({ + program: process.execPath, + args: [ + realpathSync(join(root, 'config/scripts/node-server-change-scope.mjs')), + changes, + ...(scenario.fullQualification === false ? [] : ['--full-qualification']), + ...(scenario.deferred ? ['--defer-graph'] : []) + ], + cwd: root, + env: { ...process.env, GITHUB_OUTPUT: stepOutput }, + timeoutMs: 5_000 + }) + expect(result.code).toBe(0) + const output = readFileSync(stepOutput, 'utf8') + expect(output).toContain(scenario.expected) + if (scenario.expected === 'graph_required=true') { + expect(output).not.toContain('should_run=') + expect(output).not.toContain('runners=') + } else { + expect(output).toContain(`qualification=${scenario.qualification !== false}`) + expect(output).toContain(`runners=${JSON.stringify(scenario.runners ?? NODE_SERVER_RUNNERS)}`) + } +}) + describe('the actual Bun build and profile-test dependency graph', () => { let inputs beforeAll(async () => { inputs = await collectNodeServerInputs() }, 60_000) + it('tracks the shared close probe without pulling in its mocked renderer adapter', () => { + expect(inputs.has('src/shared/pty-running-work-probe.ts')).toBe(true) + expect(inputs.has('src/shared/pty-running-work-probe.test.ts')).toBe(true) + expect(inputs.has('src/renderer/src/components/terminal/pty-running-work-probe.ts')).toBe(false) + expect(inputs.has('src/renderer/src/runtime/runtime-terminal-inspection.ts')).toBe(false) + expect([...inputs].some((file) => file.startsWith('src/renderer/'))).toBe(false) + }) + it.each([ 'config/scripts/ci-shard-timings.json', 'config/scripts/mobile-web-app-terminal-render.test.mjs', + 'src/renderer/src/components/terminal/pty-running-work-probe.ts', + 'src/renderer/src/runtime/runtime-terminal-inspection.ts', 'src/main/ssh/ssh-relay-upload-stage-commands.test.ts', 'src/main/menu/register-app-menu.ts' ])('skips unrelated work: %s', async (file) => { @@ -109,6 +198,8 @@ describe('the actual Bun build and profile-test dependency graph', () => { it.each([ ...Object.values(ORCAD_CHILD_ENTRY_POINTS), 'src/shared/keybindings/definitions-core-1.ts', + 'src/shared/pty-running-work-probe.ts', + 'src/shared/pty-running-work-probe.test.ts', 'src/main/runtime/orca-runtime.ts', 'src/main/windows/windows-process-table.ts', 'src/main/worker-thread-entry-path.ts', @@ -149,7 +240,7 @@ it('keeps every platform job and runs them when detection is skipped or fails', expect(detect.env.PUSH_BASE).toBe('${{ github.event.before }}') expect(detect.run).toContain('git fetch --no-tags --depth=1 origin "$PUSH_BASE"') expect(detect.run).toContain('git diff --name-only --no-renames -z "$PUSH_BASE" HEAD') - expect(detect.run).toContain('node-server-changes" --full-qualification') + expect(detect.run).toContain('node-server-changes" --defer-graph --full-qualification') expect(workflow.on.pull_request.types).toContain('ready_for_review') expect(workflow.on.schedule).toHaveLength(1) // A pull request may qualify one platform, so the merged commit must re-qualify all six. @@ -213,14 +304,29 @@ it('runs the Bun and Node cross-runtime tests on Linux against pinned inputs', ( expect(setupBun.with['bun-version']).toBe('1.4.2') const build = steps.find((step) => String(step.run).includes('build-orcad-bun.mjs')) expect(build.env.BUN_ORCAD_COMMIT).toMatch(/^[0-9a-f]{40}$/) - expect(build.run).toContain('ORCA_BUN_ORCAD_SLOT=') - expect(build.run).toContain('BUN_EXECUTABLE=') - for (const step of [setupBun, build]) { - expect(step.if).toBe("runner.os == 'Linux'") - } - expect(steps.map((step) => step.run).join('\n')).toContain( + expect(setupBun.if).toBe("runner.os == 'Linux'") + expect(build.id).toBe('bun-orcad') + expect(build.background).toBe(true) + expect(build.if).toBeUndefined() + expect(build['continue-on-error']).toBeUndefined() + expect(build.run).toMatch(/^if \[ "\$RUNNER_OS" != Linux \]; then exit 0; fi\n/) + expect(build.run).toContain('echo "slot=$RUNNER_TEMP/bun-orcad" >> "$GITHUB_OUTPUT"') + expect(build.run).toContain('echo "executable=$(command -v bun)" >> "$GITHUB_OUTPUT"') + expect(build.run).not.toContain('GITHUB_ENV') + const join = steps.findIndex((step) => step.wait === build.id) + expect(join).toBeGreaterThan(steps.indexOf(build)) + expect(steps[join].if).toBeUndefined() + expect(steps[join]['continue-on-error']).toBeUndefined() + const consumer = steps.find((step) => step.run?.startsWith('pnpm test:node-server --artifact ')) + expect(steps.indexOf(consumer)).toBeGreaterThan(join) + expect(consumer.run).toBe( "pnpm test:node-server --artifact ${{ runner.os == 'Linux' && '--cross-runtime' || '' }}" ) + expect(consumer.if).toBeUndefined() + expect(consumer.env).toEqual({ + ORCA_BUN_ORCAD_SLOT: '${{ steps.bun-orcad.outputs.slot }}', + BUN_EXECUTABLE: '${{ steps.bun-orcad.outputs.executable }}' + }) const alpine = workflow.jobs.linux_musl.steps.find((step) => String(step.run).includes('docker run') ) diff --git a/config/scripts/node-server-qualification.mjs b/config/scripts/node-server-qualification.mjs index 9901b609a9b0..777a2feb3146 100644 --- a/config/scripts/node-server-qualification.mjs +++ b/config/scripts/node-server-qualification.mjs @@ -1,7 +1,7 @@ export const NODE_SERVER_RUNNERS = [ 'ubuntu-22.04', 'ubuntu-24.04-arm', - 'macos-14', + 'macos-15', 'macos-15-intel', 'windows-2022', 'windows-11-arm' @@ -12,6 +12,7 @@ const BUILD_PREFIXES = [ 'native/', 'config/patches/', '.github/actions/install-node-dependencies/', + '.github/actions/restore-pnpm-verification/', '.github/actions/prepare-native-runtime/', '.github/actions/prepare-orcad-prebuilds/' ] diff --git a/config/scripts/node-server-qualification.test.mjs b/config/scripts/node-server-qualification.test.mjs index 6018fe6a7e4e..be4e0e4d9cd4 100644 --- a/config/scripts/node-server-qualification.test.mjs +++ b/config/scripts/node-server-qualification.test.mjs @@ -27,6 +27,7 @@ it.each([ 'native/windows-registry/src/addon.cc', 'config/patches/node-pty.patch', '.github/actions/install-node-dependencies/action.yml', + '.github/actions/restore-pnpm-verification/action.yml', '.github/actions/prepare-native-runtime/action.yml', '.github/actions/prepare-orcad-prebuilds/action.yml', 'src/main/ssh/ssh-provider.ts', @@ -74,11 +75,11 @@ it.each([ ['ubuntu-22.04', 'windows-2022', 'windows-11-arm'], false ], - ['src/main/daemon/darwin-process.ts', ['ubuntu-22.04', 'macos-14', 'macos-15-intel'], false], + ['src/main/daemon/darwin-process.ts', ['ubuntu-22.04', 'macos-15', 'macos-15-intel'], false], ['src/shared/linux-glibc.ts', ['ubuntu-22.04', 'ubuntu-24.04-arm'], true], [ 'src/main/daemon/posix-process.ts', - ['ubuntu-22.04', 'ubuntu-24.04-arm', 'macos-14', 'macos-15-intel'], + ['ubuntu-22.04', 'ubuntu-24.04-arm', 'macos-15', 'macos-15-intel'], true ] ])('selects both architectures and a Linux smoke for %s', (file, runners, qualification) => { @@ -92,7 +93,7 @@ it('combines platform families without adding Linux compatibility work', () => { scope ) ).toEqual({ - runners: ['ubuntu-22.04', 'macos-14', 'macos-15-intel', 'windows-2022', 'windows-11-arm'], + runners: ['ubuntu-22.04', 'macos-15', 'macos-15-intel', 'windows-2022', 'windows-11-arm'], qualification: false }) }) diff --git a/config/scripts/node-server-test-paths.mjs b/config/scripts/node-server-test-paths.mjs index 6938919bc7c1..a93c30869ca8 100644 --- a/config/scripts/node-server-test-paths.mjs +++ b/config/scripts/node-server-test-paths.mjs @@ -21,6 +21,7 @@ export function nodeServerTestPaths({ artifact = false, crossRuntime = false } = ...(artifact ? [ 'tests/e2e/daemon-running-work-probe.unit.test.ts', + 'src/shared/pty-running-work-probe.test.ts', 'src/main/orcad/orcad-packaged-node-pty.integration.test.ts', 'src/main/providers/agent-foreground-process-git-bash.win32.test.ts', 'src/main/orcad/orcad-node-launcher.integration.test.ts', diff --git a/config/scripts/orcad-entry-build.mjs b/config/scripts/orcad-entry-build.mjs index e168430fe0a7..8ee6b243b3de 100644 --- a/config/scripts/orcad-entry-build.mjs +++ b/config/scripts/orcad-entry-build.mjs @@ -8,7 +8,8 @@ export const ORCAD_CHILD_ENTRY_POINTS = { watcher: 'src/main/ipc/parcel-watcher-process-entry.ts', daemon: 'src/main/daemon/daemon-entry.ts', writer: 'src/main/persistence/profile-state/profile-state-writer-worker-entry.ts', - backup: 'src/main/persistence/profile-state/profile-state-backup-worker-entry.ts' + backup: 'src/main/persistence/profile-state/profile-state-backup-worker-entry.ts', + foreignSqliteReader: 'src/main/foreign-sqlite-readers/foreign-sqlite-reader-entry.ts' } export const ORCAD_EXTERNAL_MODULES = ['electron', 'node-pty', '@parcel/watcher', 'fsevents'] diff --git a/config/scripts/orcad-template-release-workflow.test.mjs b/config/scripts/orcad-template-release-workflow.test.mjs index eee8d451fc3c..311231ea4bf1 100644 --- a/config/scripts/orcad-template-release-workflow.test.mjs +++ b/config/scripts/orcad-template-release-workflow.test.mjs @@ -123,6 +123,65 @@ describe('orcad template release wiring (design D2)', () => { expect(releaseMac.permissions.actions).toBe('read') }) + it('skips the template only for a tag that predates it', () => { + const cutSteps = releaseCut.jobs.cut.steps + const push = stepIndex(cutSteps, (step) => step.name === 'Push tag') + const detect = stepIndex(cutSteps, (step) => step.id === 'orcad-template-support') + expect(detect).toBeGreaterThan(push) + expect(cutSteps[detect].run).toContain(':config/scripts/packaged-orcad-template.cjs"') + expect(releaseCut.jobs.cut.outputs.ships_orcad_template).toBe( + '${{ steps.orcad-template-support.outputs.ships }}' + ) + expect(releaseCut.jobs['orcad-template'].if).toContain( + "needs.cut.outputs.ships_orcad_template == 'true'" + ) + + for (const name of ['build', 'build-mac']) { + const condition = releaseCut.jobs[name].if + // Every other dependency still has to succeed, as under the implicit success(). + for (const need of releaseCut.jobs[name].needs.filter((need) => need !== 'orcad-template')) { + expect(condition).toContain(`needs.${need}.result == 'success'`) + } + expect(condition).toContain("needs.orcad-template.result == 'success'") + expect(condition).toContain( + "(needs.orcad-template.result == 'skipped' && needs.cut.outputs.ships_orcad_template == 'false')" + ) + } + + const buildSteps = releaseCut.jobs.build.steps + for (const step of [ + buildSteps.find((step) => step.name === 'Download the orcad deployment template'), + buildSteps.find((step) => step.id === 'reseal-orcad-template') + ]) { + expect(step.if).toContain("needs.cut.outputs.ships_orcad_template == 'true'") + } + const macDownload = releaseMac.jobs['build-mac'].steps.find( + (step) => step.with?.name === 'orcad-template' + ) + expect(macDownload.if).toBe("hashFiles('config/scripts/packaged-orcad-template.cjs') != ''") + }) + + it('keeps every job downstream of the template from inheriting its skip', () => { + const needsOf = (name) => [releaseCut.jobs[name].needs ?? []].flat() + const dependsOnTemplate = (name) => + needsOf(name).some((need) => need === 'orcad-template' || dependsOnTemplate(need)) + const downstream = Object.keys(releaseCut.jobs).filter(dependsOnTemplate) + expect(downstream).toEqual( + expect.arrayContaining(['build', 'build-mac', 'publish-release', 'homebrew-bump']) + ) + for (const name of downstream) { + // A skipped ancestor skips the job under the implicit success() that any `if` without a + // status function gets, so each one must override it and check its own needs instead. + const condition = releaseCut.jobs[name].if + expect(condition, name).toContain('!cancelled()') + for (const need of needsOf(name).filter( + (need) => need !== 'orcad-template' && need !== 'cut' + )) { + expect(condition, `${name} -> ${need}`).toContain(`needs.${need}.result == 'success'`) + } + } + }) + it('signs only Windows template binaries and reseals the manifest before the installer rebuild', () => { const steps = releaseCut.jobs.build.steps const stage = steps.find((step) => step.id === 'stage-inner') diff --git a/config/scripts/patched-dependencies-frozen-install.test.mjs b/config/scripts/patched-dependencies-frozen-install.test.mjs index 89f98ef074b0..53061c1dbd7d 100644 --- a/config/scripts/patched-dependencies-frozen-install.test.mjs +++ b/config/scripts/patched-dependencies-frozen-install.test.mjs @@ -61,6 +61,11 @@ describe('patched dependencies', () => { for (const file of ['package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml']) { copyFileSync(join(PROJECT_DIR, file), join(scratch, file)) } + mkdirSync(join(scratch, 'native', 'windows-registry'), { recursive: true }) + copyFileSync( + join(PROJECT_DIR, 'native', 'windows-registry', 'package.json'), + join(scratch, 'native', 'windows-registry', 'package.json') + ) mkdirSync(join(scratch, 'config'), { recursive: true }) cpSync(join(PROJECT_DIR, 'config', 'patches'), join(scratch, 'config', 'patches'), { recursive: true diff --git a/config/scripts/plain-node-entry-guard.test.ts b/config/scripts/plain-node-entry-guard.test.ts index 843d185b1016..588b32871bc2 100644 --- a/config/scripts/plain-node-entry-guard.test.ts +++ b/config/scripts/plain-node-entry-guard.test.ts @@ -231,8 +231,8 @@ describe('CLI and worker thread entry guard', () => { it('follows shared chunks out of a worker entry', () => { const plugin = createPlainNodeEntryGuardPlugin() const bundle: Rollup.OutputBundle = { - 'session-scanner-opencode-sqlite-worker-entry.js': entryChunk( - 'session-scanner-opencode-sqlite-worker-entry', + 'foreign-sqlite-reader-entry.js': entryChunk( + 'foreign-sqlite-reader-entry', 'require("./chunks/shared.js")', ['chunks/shared.js'] ), diff --git a/config/scripts/pr-code-change-scope.mjs b/config/scripts/pr-code-change-scope.mjs index 7f34a8304a97..800854c8008f 100644 --- a/config/scripts/pr-code-change-scope.mjs +++ b/config/scripts/pr-code-change-scope.mjs @@ -38,6 +38,7 @@ const ALWAYS_ON_CODE_JOBS = new Set(['static_analysis', 'typecheck', 'test']) const GLOBAL_FORCE_PREFIXES = [ '.github/workflows/pr.yml', '.github/actions/install-node-dependencies/', + '.github/actions/restore-pnpm-verification/', '.github/actions/prepare-native-runtime/', 'config/scripts/pr-code-change-scope' ] @@ -325,6 +326,7 @@ const WINDOWS_PACKAGE_TESTS = [ 'src/shared/child-process/windows-cmd-shim-resolution.test.ts', 'src/shared/child-process/windows-cmd-shim-resolution.win32.test.ts', 'src/main/agent-hooks/windows-hook-payload-delivery.test.ts', + 'src/main/jcode/hook-gate-script.test.ts', 'src/main/agent-hooks/windows-direct-cmd-hook-command.test.ts', 'src/main/codex/windows-hook-command.test.ts', 'src/main/codex/windows-hook-upgrade.test.ts', diff --git a/config/scripts/pr-e2e-gate-contract.test.mjs b/config/scripts/pr-e2e-gate-contract.test.mjs index 4b8831e357a1..4c1268cd7a23 100644 --- a/config/scripts/pr-e2e-gate-contract.test.mjs +++ b/config/scripts/pr-e2e-gate-contract.test.mjs @@ -1,4 +1,5 @@ import { DEDICATED_E2E_SPECS } from './ci-e2e-job-selection.mjs' +import { linuxInstallPackageList } from './pr-e2e-linux-packages.test-fixture.mjs' import { existsSync, readdirSync, readFileSync } from 'node:fs' import { join, resolve } from 'node:path' import { parse as parseJsonc } from 'jsonc-parser' @@ -20,6 +21,7 @@ import { const projectDir = resolve(import.meta.dirname, '../..') const prWorkflow = parseYaml(readFileSync(join(projectDir, '.github/workflows/pr.yml'), 'utf8')) const e2eWorkflow = parseYaml(readFileSync(join(projectDir, '.github/workflows/e2e.yml'), 'utf8')) + const reliabilityManifest = parseJsonc( readFileSync(join(projectDir, 'config/reliability-gates.jsonc'), 'utf8') ) @@ -218,7 +220,7 @@ describe('PR E2E gate contract', () => { const installStep = e2eWorkflow.jobs[jobName].steps.find((step) => step.name.startsWith('Install native build') ) - expect(installStep.run, jobName).toMatch(/\bzsh\b/) + expect(linuxInstallPackageList(installStep, jobName), jobName).toMatch(/(^|\s)zsh(\s|$)/) } }) @@ -305,10 +307,10 @@ describe('PR E2E gate contract', () => { // Why: this lane can now pay a Docker image build plus serial SSH specs. expect(e2eWorkflow.jobs['changed-e2e']['timeout-minutes']).toBeGreaterThanOrEqual(45) - const changedInstall = e2eWorkflow.jobs['changed-e2e'].steps.find((step) => + const install = e2eWorkflow.jobs['changed-e2e'].steps.find((step) => step.name.startsWith('Install native build') ) - expect(changedInstall.run).toContain('openssh-client') + expect(linuxInstallPackageList(install, 'changed-e2e')).toMatch(/(^|\s)openssh-client(\s|$)/) }) it('routes direct-SSH workspace and tab restore from its unnamed source seams', () => { diff --git a/config/scripts/pr-e2e-linux-packages.test-fixture.mjs b/config/scripts/pr-e2e-linux-packages.test-fixture.mjs new file mode 100644 index 000000000000..dfbb2d4b7981 --- /dev/null +++ b/config/scripts/pr-e2e-linux-packages.test-fixture.mjs @@ -0,0 +1,10 @@ +import { expect } from 'vitest' + +export function linuxInstallPackageList(step, jobName) { + const packages = step.env?.ORCA_E2E_APT_PACKAGES + if (packages !== undefined) { + expect(step.run, jobName).toContain('read -r -a packages <<< "$ORCA_E2E_APT_PACKAGES"') + expect(step.run, jobName).toContain('sudo apt-get install -y "${packages[@]}"') + } + return packages ?? step.run +} diff --git a/config/scripts/pr-workflow-parallelism.test.mjs b/config/scripts/pr-workflow-parallelism.test.mjs index 9f841ffb1bee..91156e59bf0b 100644 --- a/config/scripts/pr-workflow-parallelism.test.mjs +++ b/config/scripts/pr-workflow-parallelism.test.mjs @@ -1,4 +1,5 @@ import { existsSync, globSync, readFileSync } from 'node:fs' +import { runInNewContext } from 'node:vm' import { parse } from 'yaml' import { describe, expect, it } from 'vitest' import { UNIT_EXCLUDE } from './ci-unit-files.mjs' @@ -285,11 +286,12 @@ describe('PR workflow parallelism', () => { expect(pnpmIndex).toBeLessThan(nodeIndex) expect(pnpmIndex).toBeLessThan(requestedNodeIndex) const packageManagerVersion = /^pnpm@([^+]+)/.exec(packageJson.packageManager)?.[1] - expect(packageManagerVersion).toBe('12.0.0') + expect(packageManagerVersion).toBe('12.8.1') expect(steps[pnpmIndex].uses).toBe('pnpm/setup@v2') expect(steps[pnpmIndex].with.version).toBeUndefined() expect(steps[pnpmIndex].with.install).toBe(false) - const saveOutsidePrs = "${{ github.event_name != 'pull_request' && 'pnpm' || '' }}" + const saveOutsidePrs = + "${{ github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && steps.pnpm-store-mode.outputs.lookup-only != 'true' && 'pnpm' || '' }}" expect(steps[nodeIndex].with.cache).toBe(saveOutsidePrs) expect(steps[nodeIndex].if).toBe("inputs.node-version == ''") expect(steps[requestedNodeIndex].if).toBe("inputs.node-version != ''") @@ -304,7 +306,7 @@ describe('PR workflow parallelism', () => { ) expect(steps[restoreIndex].uses).toBe('actions/cache/restore@v5') expect(steps[restoreIndex].if).toBe( - "github.event_name == 'pull_request' && (runner.os != 'Windows' || runner.arch != 'X64' || !contains(inputs.cache-dependency-path, 'mobile/pnpm-lock.yaml'))" + "github.event_name == 'pull_request' && inputs.cache-pnpm-store != 'false' && !((runner.os == 'Linux' || runner.os == 'macOS') && (runner.arch == 'X64' || runner.arch == 'ARM64') && inputs.cache-dependency-path == 'pnpm-lock.yaml') && (runner.os != 'Windows' || !(runner.arch == 'X64' && contains(inputs.cache-dependency-path, 'mobile/pnpm-lock.yaml')) && !((runner.arch == 'X64' || runner.arch == 'ARM64') && inputs.cache-dependency-path == 'pnpm-lock.yaml'))" ) }) @@ -514,6 +516,21 @@ describe('PR workflow parallelism', () => { const evidence = workflow.jobs.unit_selection_evidence expect(evidence.uses).toBe('./.github/workflows/unit-selection-evidence.yml') expect(evidence.needs).toEqual(['test']) + for (const [result, cancelled, expected] of [ + ['success', false, true], + ['failure', false, true], + ['skipped', false, false], + ['cancelled', false, false], + ['success', true, false], + ['failure', true, false] + ]) { + expect( + runInNewContext(evidence.if.slice(3, -2), { + cancelled: () => cancelled, + needs: { test: { result } } + }) + ).toBe(expected) + } expect(workflow.jobs.verify.needs).not.toContain('unit_selection_evidence') expect(unitTestWorkflow.jobs.selection_evidence).toBeUndefined() const evidenceWorkflow = parse( diff --git a/config/scripts/readme-downloads-badge.test.mjs b/config/scripts/readme-downloads-badge.test.mjs new file mode 100644 index 000000000000..736ccbbd9b24 --- /dev/null +++ b/config/scripts/readme-downloads-badge.test.mjs @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest' +import { countReleaseDownloads } from '../../.github/scripts/render-readme-downloads-badge.mjs' + +const asset = (downloads) => ({ download_count: downloads }) + +describe('README downloads badge', () => { + it('counts published app releases and skips drafts and agent state rules releases', () => { + expect( + countReleaseDownloads([ + { tag_name: 'v1.4.1', draft: false, assets: [asset(10), asset(5)] }, + { tag_name: 'v1.4.2-rc.0', draft: false, assets: [asset(3)] }, + { tag_name: 'mobile-v0.0.1', draft: false, assets: [asset(2)] }, + { tag_name: 'v1.4.3', draft: true, assets: [asset(100)] }, + { tag_name: 'agent-state-rules-engine-1-next', draft: false, assets: [asset(9000)] }, + { tag_name: 'agent-state-rules-engine-1-stable', draft: false, assets: [asset(9000)] } + ]) + ).toBe(20) + }) +}) diff --git a/config/scripts/rebuild-native-deps-node-pty.test.mjs b/config/scripts/rebuild-native-deps-node-pty.test.mjs index a9ec246dd95b..d1153c5f20fd 100644 --- a/config/scripts/rebuild-native-deps-node-pty.test.mjs +++ b/config/scripts/rebuild-native-deps-node-pty.test.mjs @@ -22,6 +22,51 @@ import { } from './rebuild-native-deps-test-fixtures.mjs' describe('rebuild-native-deps patched node-pty rebuild', () => { + it.skipIf(process.platform !== 'win32')( + 'passes the Windows tracking default and explicit overrides to forced Electron rebuilds', + () => { + const projectDir = mkTempProject() + + try { + const rebuildLogPath = join(projectDir, 'electron-rebuild.log') + writeFakeUsableElectronPackage(projectDir, { platform: 'win32' }) + writeFakeElectronRebuild(projectDir, { logPathEnv: 'ORCA_REBUILD_TEST_LOG' }) + writeFakeLoadableNodePty(projectDir) + writeFakeWindowsProcessTree(projectDir) + writeFakeNodePtyConptyPayload(projectDir, process.arch) + + const env = { + ORCA_REBUILD_TEST_LOG: rebuildLogPath, + npm_config_platform: 'win32', + npm_config_arch: process.arch + } + for (const override of [ + {}, + { TrackFileAccess: 'true' }, + { trackfileaccess: 'true' }, + { tRaCkFiLeAcCeSs: 'false' } + ]) { + const result = runRebuildScript(projectDir, { ...env, ...override }) + expect(result.status, result.stderr).toBe(0) + } + + const calls = readFileSync(rebuildLogPath, 'utf8') + .trim() + .split('\n') + .map((line) => JSON.parse(line)) + expect(calls.map((call) => call.trackFileAccess)).toEqual([ + 'false', + 'true', + 'true', + 'false' + ]) + expect(calls.every((call) => call.force)).toBe(true) + } finally { + removeTreeSync(projectDir) + } + } + ) + it.skipIf(process.platform !== 'win32')( 'repairs a missing ConPTY runtime before probing without recompiling node-pty', () => { diff --git a/config/scripts/rebuild-native-deps-test-fixtures.mjs b/config/scripts/rebuild-native-deps-test-fixtures.mjs index 333c8f939ef5..83223b630425 100644 --- a/config/scripts/rebuild-native-deps-test-fixtures.mjs +++ b/config/scripts/rebuild-native-deps-test-fixtures.mjs @@ -110,7 +110,7 @@ export function writeWindowsProcessTreePatchFile(projectDir) { export function mkTempProject() { const projectDir = mkdtempSync(join(tmpdir(), 'orca-rebuild-native-deps-')) mkdirSync(join(projectDir, 'config', 'scripts'), { recursive: true }) - copyFileSync(sourceScriptPath, join(projectDir, 'config', 'scripts', 'rebuild-native-deps.mjs')) + copyScriptWithLocalModules(sourceScriptPath, join(projectDir, 'config', 'scripts')) copyScriptWithLocalModules(sourceInstallScriptPath, join(projectDir, 'config', 'scripts')) copyScriptWithLocalModules(sourceNodePtyJobOwnershipPath, join(projectDir, 'config', 'scripts')) copyFileSync( @@ -134,7 +134,8 @@ export function runRebuildScript(projectDir, extraEnv = {}, args = []) { for (const key of Object.keys(env)) { if ( key.toLowerCase() === 'orca_strict_electron_install' || - key.toLowerCase() === 'npm_lifecycle_event' + key.toLowerCase() === 'npm_lifecycle_event' || + key.toLowerCase() === 'trackfileaccess' ) { delete env[key] } @@ -286,6 +287,7 @@ export async function rebuild(options) {${emitAddon} electronVersion: options.electronVersion, force: options.force, ignoreModules: options.ignoreModules, + trackFileAccess: process.env.TrackFileAccess ?? null, onlyModules: options.onlyModules, platform: options.platform }) + '\\n' diff --git a/config/scripts/rebuild-native-deps.mjs b/config/scripts/rebuild-native-deps.mjs index 68de5d49e218..e565eeca97e0 100644 --- a/config/scripts/rebuild-native-deps.mjs +++ b/config/scripts/rebuild-native-deps.mjs @@ -26,6 +26,7 @@ import { stageWindowsProcessTreeNodeAddonApiHeaders, windowsProcessTreeAddonPath } from './windows-process-tree-gyp-rebuild.mjs' +import { disableMsbuildFileTrackingOnWindows } from './msbuild-file-tracking.mjs' import { copyFileSync, existsSync, @@ -162,6 +163,7 @@ try { console.warn('[rebuild] Repaired the un-applied windows-process-tree command-line patch.') } } + disableMsbuildFileTrackingOnWindows() await rebuild({ buildPath: projectDir, electronVersion, diff --git a/config/scripts/release-e2e-dispatch-contract.test.mjs b/config/scripts/release-e2e-dispatch-contract.test.mjs index 2277cd399a49..d939a6231ef9 100644 --- a/config/scripts/release-e2e-dispatch-contract.test.mjs +++ b/config/scripts/release-e2e-dispatch-contract.test.mjs @@ -28,7 +28,9 @@ describe('release E2E dispatch contract', () => { expect(releaseWorkflow.jobs.e2e).toBeUndefined() expect(dispatchJob.needs).toEqual(['cut', 'publish-release']) - expect(dispatchJob.if).toBe("${{ needs.cut.outputs.tag != '' }}") + expect(dispatchJob.if).toBe( + "${{ !cancelled() && needs.publish-release.result == 'success' && needs.cut.outputs.tag != '' }}" + ) expect(dispatchJob.permissions.actions).toBe('write') expect(dispatchStep.env.TAG).toBe('${{ needs.cut.outputs.tag }}') expect(dispatchStep.run).toContain('gh workflow run e2e.yml') diff --git a/config/scripts/release-policy.mjs b/config/scripts/release-policy.mjs index 4733e79f2b44..e9ef5d8264ec 100644 --- a/config/scripts/release-policy.mjs +++ b/config/scripts/release-policy.mjs @@ -1,15 +1,19 @@ // Enforced by .github/workflows/release-policy.yml on release events. +import { + DESKTOP_RC_TAG as RC_TAG, + DESKTOP_STABLE_TAG as STABLE_TAG, + MOBILE_TAG, + isAgentStateRulesTag +} from './release-tag-patterns.mjs' + const BOT_LOGIN = 'github-actions[bot]' const BOT_EMAIL = '41898282+github-actions[bot]@users.noreply.github.com' -const NUMBER = '(?:0|[1-9][0-9]*)' -const VERSION = `${NUMBER}\\.${NUMBER}\\.${NUMBER}` -const STABLE_TAG = new RegExp(`^v${VERSION}$`) -const RC_TAG = new RegExp(`^v${VERSION}-rc\\.${NUMBER}(?:\\.[0-9A-Za-z]+)?$`) -const MOBILE_TAG = new RegExp(`^mobile(?:-android)?-v${VERSION}$`) +// Why agent state rules here: their publish workflow is a bot author, and as a prerelease the +// release can never become Latest, which the app updater follows. export function isPrereleaseTag(tag) { - return RC_TAG.test(tag) || MOBILE_TAG.test(tag) + return RC_TAG.test(tag) || MOBILE_TAG.test(tag) || isAgentStateRulesTag(tag) } export function compareStableTags(left, right) { diff --git a/config/scripts/release-policy.test.mjs b/config/scripts/release-policy.test.mjs index 6478f6bb0c49..6d36717d7856 100644 --- a/config/scripts/release-policy.test.mjs +++ b/config/scripts/release-policy.test.mjs @@ -147,4 +147,41 @@ describe('release policy', () => { await expect(restoreLatestStable(github, repoRef)).resolves.toBe('v1.4.100') }) + + it('keeps a bot-published agent state rules release as a prerelease, off Latest', async () => { + const rules = release('agent-state-rules-engine-1-next', 'github-actions[bot]') + const stable = release('v1.4.214', 'github-actions[bot]') + const github = createGithub({ + releases: [rules, stable], + tags: { 'v1.4.214': {} } + }) + + await run(github, rules, 'published') + + expect(github.rest.repos.deleteRelease).not.toHaveBeenCalled() + expect(github.rest.git.deleteRef).not.toHaveBeenCalled() + expect(github.rest.repos.updateRelease).toHaveBeenCalledWith({ + ...repoRef, + release_id: rules.id, + prerelease: true, + make_latest: 'false' + }) + expect(github.rest.repos.updateRelease).toHaveBeenLastCalledWith({ + ...repoRef, + release_id: stable.id, + make_latest: 'true' + }) + }) + + it('deletes an agent state rules release a person published', async () => { + const rules = release('agent-state-rules-engine-1-stable', 'someone', { prerelease: true }) + const github = createGithub({ releases: [rules] }) + + await run(github, rules, 'published') + + expect(github.rest.repos.deleteRelease).toHaveBeenCalledWith({ + ...repoRef, + release_id: rules.id + }) + }) }) diff --git a/config/scripts/release-tag-pattern-census.test.mjs b/config/scripts/release-tag-pattern-census.test.mjs new file mode 100644 index 000000000000..b42a424a36f3 --- /dev/null +++ b/config/scripts/release-tag-pattern-census.test.mjs @@ -0,0 +1,184 @@ +// Census: every release-triggered workflow and every script that lists this repo's releases +// classifies tags through release-tag-patterns.mjs, so a new tag family such as the agent state +// rules cannot reach a desktop-only path through one that forgot it. +import { readdirSync, readFileSync } from 'node:fs' +import { join } from 'node:path' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { parse } from 'yaml' +import { + DESKTOP_RC_TAG, + DESKTOP_STABLE_TAG, + DESKTOP_STABLE_TAG_SHELL_PATTERN, + agentStateRulesTag, + isAgentStateRulesTag +} from './release-tag-patterns.mjs' + +const WORKFLOWS_DIR = '.github/workflows' +const SCRIPT_DIRS = ['config/scripts', '.github/scripts'] +const SHARED_IMPORT = /from '[^']*release-tag-patterns\.mjs'/ +const LISTS_RELEASES = /\/releases\?|listReleases|releases\.atom|'release',\s*'list'/ + +const RULES_TAGS = [agentStateRulesTag(1, 'next'), agentStateRulesTag(1, 'stable')] +const rulesRelease = (tag, extra = {}) => ({ + tag_name: tag, + name: tag, + draft: false, + prerelease: true, + author: { login: 'github-actions[bot]' }, + assets: [{ name: 'agent-state-rules.json', download_count: 5 }], + ...extra +}) + +function read(path) { + return readFileSync(path, 'utf8') +} + +function importsSharedPatterns(path) { + return SHARED_IMPORT.test(read(path)) +} + +function isReleaseTriggered(workflow) { + const on = workflow.on + if (typeof on === 'string') { + return on === 'release' + } + if (Array.isArray(on)) { + return on.includes('release') + } + return typeof on === 'object' && on !== null && 'release' in on +} + +function releaseTriggeredWorkflows() { + return readdirSync(WORKFLOWS_DIR) + .filter((name) => /\.ya?ml$/.test(name)) + .map((name) => join(WORKFLOWS_DIR, name)) + .filter((path) => isReleaseTriggered(parse(read(path)))) +} + +function scriptsNamedIn(text) { + return [...text.matchAll(/((?:config|\.github)\/scripts\/[\w.-]+\.mjs)/g)].map( + (match) => match[1] + ) +} + +function releaseListingScripts() { + return SCRIPT_DIRS.flatMap((dir) => + readdirSync(dir) + .filter((name) => name.endsWith('.mjs') && !name.includes('.test.')) + .map((name) => join(dir, name)) + ).filter((path) => LISTS_RELEASES.test(read(path))) +} + +/** + * Scripts that list releases yet admit only desktop tags by their own shape. Each entry proves, + * by calling the script, that a rules release never passes; a new listing script must import the + * shared patterns or add a proof here. + */ +const DESKTOP_ONLY_PROOFS = { + 'config/scripts/create-draft-release.mjs': async () => { + const { latestPreviousPublishedDesktopReleaseTag } = await import('./create-draft-release.mjs') + const releases = [ + ...RULES_TAGS.map((tag) => rulesRelease(tag)), + { tag_name: 'v1.4.1', draft: false } + ] + expect(latestPreviousPublishedDesktopReleaseTag(releases, 'v1.4.2')).toBe('v1.4.1') + }, + 'config/scripts/publish-complete-draft-releases.mjs': async () => { + const { isReleaseCutDraft } = await import('./publish-complete-draft-releases.mjs') + for (const tag of RULES_TAGS) { + expect(isReleaseCutDraft(rulesRelease(tag, { draft: true }))).toBe(false) + } + }, + 'config/scripts/latest-stable-release.mjs': async () => { + const { latestStableDesktopReleaseTag } = await import('./latest-stable-release.mjs') + const releases = [ + ...RULES_TAGS.map((tag) => rulesRelease(tag, { prerelease: false })), + { tag_name: 'v1.4.1' } + ] + expect(latestStableDesktopReleaseTag(releases)).toBe('v1.4.1') + }, + 'config/scripts/assert-github-release-is-draft.mjs': async () => { + const { matchingDesktopReleases } = await import('./assert-github-release-is-draft.mjs') + expect( + matchingDesktopReleases( + RULES_TAGS.map((tag) => rulesRelease(tag)), + 'v1.4.1' + ) + ).toEqual([]) + }, + 'config/scripts/verify-release-required-assets.mjs': async () => { + const { verifyRequiredReleaseAssets } = await import('./verify-release-required-assets.mjs') + vi.stubGlobal( + 'fetch', + vi.fn(async () => new Response(JSON.stringify(RULES_TAGS.map((tag) => rulesRelease(tag))))) + ) + await expect( + verifyRequiredReleaseAssets({ repo: 'stablyai/orca', tag: 'v1.4.1', token: '' }) + ).rejects.toThrow('was not found') + } +} + +afterEach(() => { + vi.unstubAllGlobals() +}) + +describe('release tag pattern census', () => { + it('finds the release-triggered workflows and release-listing scripts it guards', () => { + expect(releaseTriggeredWorkflows().length).toBeGreaterThan(0) + expect(releaseListingScripts().length).toBeGreaterThan(0) + }) + + it.each(releaseTriggeredWorkflows())('%s classifies tags through the shared patterns', (path) => { + const text = read(path) + const viaScript = scriptsNamedIn(text).some(importsSharedPatterns) + const viaShellPattern = text.includes(DESKTOP_STABLE_TAG_SHELL_PATTERN) + expect( + viaScript || viaShellPattern, + `${path} names no script importing release-tag-patterns.mjs and embeds no shared pattern` + ).toBe(true) + }) + + it.each(releaseTriggeredWorkflows())( + '%s checks out the shared patterns beside its script', + (path) => { + const workflow = parse(read(path)) + const sparse = Object.values(workflow.jobs) + .flatMap((job) => job.steps ?? []) + .map((step) => step.with?.['sparse-checkout']) + .filter((value) => typeof value === 'string') + for (const paths of sparse) { + if (scriptsNamedIn(paths).some(importsSharedPatterns)) { + expect(paths).toContain('config/scripts/release-tag-patterns.mjs') + } + } + } + ) + + it.each(releaseListingScripts())( + '%s imports the shared patterns or proves it admits only desktop tags', + async (path) => { + if (importsSharedPatterns(path)) { + return + } + const proof = DESKTOP_ONLY_PROOFS[path] + expect( + proof, + `${path} lists releases: import release-tag-patterns.mjs or add a proof` + ).toBeDefined() + await proof() + } + ) + + it('keeps no proof for a script that no longer lists releases', () => { + const listing = new Set(releaseListingScripts()) + expect(Object.keys(DESKTOP_ONLY_PROOFS).filter((path) => !listing.has(path))).toEqual([]) + }) + + it('never classifies an agent state rules tag as a desktop release', () => { + for (const tag of RULES_TAGS) { + expect(isAgentStateRulesTag(tag)).toBe(true) + expect(DESKTOP_STABLE_TAG.test(tag) || DESKTOP_RC_TAG.test(tag)).toBe(false) + } + expect(() => agentStateRulesTag(1, 'beta')).toThrow() + }) +}) diff --git a/config/scripts/release-tag-patterns.mjs b/config/scripts/release-tag-patterns.mjs new file mode 100644 index 000000000000..43bae852b5f7 --- /dev/null +++ b/config/scripts/release-tag-patterns.mjs @@ -0,0 +1,30 @@ +// The tag families published as GitHub releases of this repo. Every release-triggered workflow and +// every script that lists releases classifies tags through this file, so a new family (like the +// agent state rules) is excluded or admitted in one place; release-tag-pattern-census.test.mjs +// enforces that. + +const NUMBER = '(?:0|[1-9][0-9]*)' +const VERSION = `${NUMBER}\\.${NUMBER}\\.${NUMBER}` + +/** The stable desktop tag as a bash `[[ =~ ]]` pattern, for workflows that gate inline. */ +export const DESKTOP_STABLE_TAG_SHELL_PATTERN = + '^v(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$' + +export const DESKTOP_STABLE_TAG = new RegExp(DESKTOP_STABLE_TAG_SHELL_PATTERN) +export const DESKTOP_RC_TAG = new RegExp(`^v${VERSION}-rc\\.${NUMBER}(?:\\.[0-9A-Za-z]+)?$`) +export const MOBILE_TAG = new RegExp(`^mobile(?:-android)?-v${VERSION}$`) + +/** The agent state rules bundle, one release per rules engine and channel, updated in place. */ +const AGENT_STATE_RULES_TAG = /^agent-state-rules-engine-[1-9][0-9]*-(?:next|stable)$/ + +export function isAgentStateRulesTag(tag) { + return AGENT_STATE_RULES_TAG.test(tag) +} + +export function agentStateRulesTag(engineVersion, channel) { + const tag = `agent-state-rules-engine-${engineVersion}-${channel}` + if (!isAgentStateRulesTag(tag)) { + throw new Error(`not an agent state rules tag: ${tag}`) + } + return tag +} diff --git a/config/scripts/runtime-serve-terminal-smoke.mjs b/config/scripts/runtime-serve-terminal-smoke.mjs index c1d998717ffd..2b23115859ca 100644 --- a/config/scripts/runtime-serve-terminal-smoke.mjs +++ b/config/scripts/runtime-serve-terminal-smoke.mjs @@ -198,7 +198,7 @@ function resolveLaunch(userDataDir) { label: `electron (${serveEntry})`, command: override ?? 'npx', args: override ? serveArgs : ['electron', ...serveArgs], - env: {} + env: { ORCA_DEV_USER_DATA_PATH: userDataDir } } } diff --git a/config/scripts/ssh-hostile-hosts-workflow.test.mjs b/config/scripts/ssh-hostile-hosts-workflow.test.mjs index c79706ab4fdb..35c2cddecc25 100644 --- a/config/scripts/ssh-hostile-hosts-workflow.test.mjs +++ b/config/scripts/ssh-hostile-hosts-workflow.test.mjs @@ -71,7 +71,7 @@ describe('SSH hostile-host workflow', () => { const job = workflow.jobs.macos_hosts expect(job.if).toContain('github.event.pull_request.draft != true') expect(job.needs).toBeUndefined() - const runners = { 'darwin-arm64': 'macos-14', 'darwin-x64': 'macos-15-intel' } + const runners = { 'darwin-arm64': 'macos-15', 'darwin-x64': 'macos-15-intel' } const macCells = HOSTILE_HOST_CELLS.filter((cell) => cell.host === 'local-sshd') expect( job.strategy.matrix.include.map(({ os, target, cell }) => ({ os, target, cell })) diff --git a/config/scripts/ssh-windows-hosts-workflow.test.mjs b/config/scripts/ssh-windows-hosts-workflow.test.mjs index c137b268c0dc..3daef7cd4e2c 100644 --- a/config/scripts/ssh-windows-hosts-workflow.test.mjs +++ b/config/scripts/ssh-windows-hosts-workflow.test.mjs @@ -13,6 +13,14 @@ const workflow = parse( ) const job = workflow.jobs.hosts const runStep = job.steps.find((step) => step.name?.startsWith('Run the Windows host cells')) +const provisioning = readFileSync( + join(projectDir, 'config/ci/windows-ssh-provider/preview-ssh/prove-preview-openssh.ps1'), + 'utf8' +) +const capability = readFileSync( + join(projectDir, 'config/ci/windows-ssh-provider/preview-ssh/windows-ssh-capability.ps1'), + 'utf8' +) const manifest = (arch) => JSON.parse( readFileSync( @@ -47,6 +55,99 @@ describe('SSH Windows-host workflow', () => { 'x64/windows-2022/preview' ]) expect(job.env).toMatchObject({ ORCA_BACKGROUND_LAUNCH: '1', ORCA_ISOLATED_SSH_CI: '1' }) + expect(job.strategy['fail-fast']).toBe(false) + expect(job['timeout-minutes']).toBe(75) + expect(runStep['timeout-minutes']).toBe(50) + }) + + it('overlaps only guarded ARM inbox capability preparation with the existing builds', () => { + const selfTestIndex = job.steps.findIndex((step) => step.name?.startsWith('Self-test')) + const prepareIndex = job.steps.findIndex((step) => step.id === 'inbox-capability') + const installIndex = job.steps.findIndex( + (step) => step.uses === './.github/actions/install-node-dependencies' + ) + const buildIndex = job.steps.findIndex((step) => step.name?.startsWith('Build this runner')) + const prebuildIndex = job.steps.findIndex( + (step) => step.uses === './.github/actions/prepare-orcad-prebuilds' + ) + const templateIndex = job.steps.findIndex((step) => step.name?.startsWith('Build the win32')) + const waitIndex = job.steps.findIndex((step) => step.wait === 'inbox-capability') + const runIndex = job.steps.indexOf(runStep) + expect([ + selfTestIndex, + prepareIndex, + installIndex, + buildIndex, + prebuildIndex, + templateIndex, + waitIndex, + runIndex + ]).toEqual([1, 2, 3, 4, 5, 6, 7, 8]) + expect(job.steps[prepareIndex]).toMatchObject({ + background: true, + shell: 'pwsh' + }) + expect(job.steps[prepareIndex].if).toBeUndefined() + expect(job.steps[waitIndex].if).toBeUndefined() + expect(job.steps[prepareIndex].run.trim()).toMatch( + /^if\('\$\{\{ matrix\.server }}' -eq 'inbox' -and '\$\{\{ matrix\.arch }}' -eq 'arm64'\)\{[\s\S]+\}$/ + ) + expect(job.steps[prepareIndex].run).toContain('Initialize-WindowsInboxSshCapability') + expect(job.steps[prepareIndex].run).toContain('inbox-capability-preparation.json') + expect(runStep.run).toContain('-InboxPreparationReceipt $preparation') + expect(runStep.run).toContain( + "$preparation=Join-Path $receipts 'inbox-capability-preparation.json'" + ) + expect(runStep.run).toContain( + "if('${{ matrix.server }}' -eq 'inbox' -and '${{ matrix.arch }}' -eq 'arm64'){$preparation=" + ) + expect(runStep.background).toBeUndefined() + expect(job.steps.at(-1)).toMatchObject({ if: 'always()', uses: 'actions/upload-artifact@v7' }) + }) + + it('shares one capability installer without bypassing native verification or private cleanup', () => { + expect(capability.match(/Add-WindowsCapability -Online/g)).toHaveLength(1) + expect(provisioning).not.toContain('Add-WindowsCapability') + expect(provisioning).toContain(". (Join-Path $PSScriptRoot 'windows-ssh-capability.ps1')") + expect(provisioning).toContain('Install-WindowsInboxSshCapability $Arch $report') + const install = capability.slice( + capability.indexOf('function Install-WindowsInboxSshCapability'), + capability.indexOf('function Initialize-WindowsInboxSshCapability') + ) + const mutation = install.indexOf('Add-WindowsCapability') + expect(install.indexOf('Assert-IsolatedWindowsSshCi')).toBeLessThan(mutation) + expect(install.indexOf('Assert-WindowsSshGlobalServerDormant')).toBeLessThan(mutation) + expect(install.lastIndexOf('Assert-WindowsSshGlobalServerDormant')).toBeGreaterThan(mutation) + expect(install.indexOf('Assert-WindowsSshStockShell')).toBeLessThan(mutation) + expect(install.lastIndexOf('Assert-WindowsSshStockShell')).toBeGreaterThan(mutation) + for (const check of [ + '(Machine $path) -ne $target.machine', + 'Get-AuthenticodeSignature -LiteralPath $path', + 'Inbox native input Microsoft signature invalid', + "Write-Stage 'host-cell-probe-start'", + "Write-Stage 'cleanup-default-shell-start'", + "Write-Stage 'cleanup-service-stop-delete-start'" + ]) { + expect(provisioning).toContain(check) + } + }) + + it('keeps preparation provenance separate from the oracle current capability state', () => { + for (const [field, environment] of [ + ['sourceSha', 'GITHUB_SHA'], + ['runId', 'GITHUB_RUN_ID'], + ['runAttempt', 'GITHUB_RUN_ATTEMPT'], + ['runnerName', 'RUNNER_NAME'], + ['imageVersion', 'ImageVersion'] + ]) { + expect(capability).toContain(`${field}=$env:${environment}`) + expect(provisioning).toContain(`$preparation.${field} -ne $env:${environment}`) + } + expect(provisioning).toContain("$preparation.status -ne 'passed'") + expect(provisioning).toContain('$preparation.arch -ne $Arch') + expect(provisioning).toContain('$report.inboxCapabilityPreparation=$preparation') + expect(capability).toContain('$Report.inboxCapabilityInitialState=[string]$capability.State') + expect(capability).toContain("$report.status='failed';$report.error=$_.Exception.Message;throw") }) it('fetches the preview release its hash manifests pin', () => { diff --git a/config/scripts/telemetry-bundle-constant-patterns.mjs b/config/scripts/telemetry-bundle-constant-patterns.mjs index 87c2ae43cf77..f0e9d53ed1c0 100644 --- a/config/scripts/telemetry-bundle-constant-patterns.mjs +++ b/config/scripts/telemetry-bundle-constant-patterns.mjs @@ -2,5 +2,6 @@ // them, but the injected identity and key remain adjacent in the declaration. export const BUILD_IDENTITY_RE = /\b(?:const|let|var)\s+BUILD_IDENTITY\s*=\s*["`](rc|stable)["`]/ export const WRITE_KEY_RE = /\b(?:const|let|var)\s+WRITE_KEY\s*=\s*["`](phc_[A-Za-z0-9_-]+)["`]/ +// Why the `,` alternative: the identity can be a later declarator in a shared `var` (`var a=!0,b=\`stable\``). export const MINIFIED_TELEMETRY_RE = - /\b(?:const|let|var)\s+[$\w]+\s*=\s*["'`](rc|stable)["'`][\s\S]{0,200}?[,$]\s*[$\w]+\s*=\s*["'`](phc_[A-Za-z0-9_-]+)["'`]/ + /(?:\b(?:const|let|var)\s+|,\s*)[$\w]+\s*=\s*["'`](rc|stable)["'`][\s\S]{0,200}?[,$]\s*[$\w]+\s*=\s*["'`](phc_[A-Za-z0-9_-]+)["'`]/ diff --git a/config/scripts/telemetry-bundle-constant-patterns.test.mjs b/config/scripts/telemetry-bundle-constant-patterns.test.mjs index ca87e3ee971b..57f8e0c72179 100644 --- a/config/scripts/telemetry-bundle-constant-patterns.test.mjs +++ b/config/scripts/telemetry-bundle-constant-patterns.test.mjs @@ -22,4 +22,16 @@ describe('telemetry bundle constant patterns', () => { const bundle = 'var dde=`stable`,fde=`phc_example-key_123`,pde=(dde===`stable`)' expect(bundle).toMatch(MINIFIED_TELEMETRY_RE) }) + + it('accepts the identity as a later declarator in a shared declaration', () => { + const bundle = 'var wpe=!0,Tpe=`stable`,Epe=`phc_example-key_123`,Dpe=(Tpe===`stable`)' + expect(MINIFIED_TELEMETRY_RE.exec(bundle)?.slice(1, 3)).toEqual([ + 'stable', + 'phc_example-key_123' + ]) + }) + + it('rejects a minified identity with no adjacent write key', () => { + expect('var a=!0,b=`stable`,c=null').not.toMatch(MINIFIED_TELEMETRY_RE) + }) }) diff --git a/config/scripts/terminal-perf-dependency-preparation.test.mjs b/config/scripts/terminal-perf-dependency-preparation.test.mjs new file mode 100644 index 000000000000..3f4c5955f7a2 --- /dev/null +++ b/config/scripts/terminal-perf-dependency-preparation.test.mjs @@ -0,0 +1,116 @@ +import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' +import { runInNewContext } from 'node:vm' +import { parse } from 'yaml' +import { expect, it } from 'vitest' +import { runProcessSync } from './script-child-process.mjs' + +const workflow = parse(readFileSync('.github/workflows/terminal-perf.yml', 'utf8')) +const steps = workflow.jobs['terminal-perf'].steps +const selector = steps.find((step) => step.id === 'install-mode') +const script = selector.run.trim().match(/^node <<'NODE'\n([\s\S]*)\nNODE$/)[1] +const supportedAction = readFileSync('.github/actions/install-node-dependencies/action.yml', 'utf8') +const supportedManifest = { + engines: { node: '24' }, + packageManager: 'pnpm@12.8.1', + scripts: { postinstall: 'node config/scripts/rebuild-native-deps.mjs' } +} + +function select(options = {}) { + const directory = mkdtempSync(join(tmpdir(), 'orca-terminal-preparation-')) + const output = join(directory, 'output') + try { + writeFileSync( + join(directory, 'package.json'), + JSON.stringify(options.manifest ?? supportedManifest) + ) + for (const [file, content] of [ + ['.github/actions/install-node-dependencies/action.yml', options.action ?? supportedAction], + ['.github/actions/prepare-native-runtime/action.yml', 'runs: {}'], + ['config/scripts/ensure-native-runtime.mjs', ''] + ]) { + if (options.missing === file) { + continue + } + const path = join(directory, file) + mkdirSync(dirname(path), { recursive: true }) + writeFileSync(path, content) + } + const result = runProcessSync({ + program: process.execPath, + args: ['-e', script], + cwd: directory, + env: { + ...process.env, + GITHUB_OUTPUT: output, + RUNNER_KIND: options.kind ?? 'github-hosted', + JOB_CONTAINER: options.container ?? '', + RUNNER_OS: options.os ?? 'Linux', + RUNNER_ARCH: options.arch ?? 'X64' + } + }) + expect(result.code, result.stderr || result.stdout).toBe(0) + return readFileSync(output, 'utf8').trim() + } finally { + rmSync(directory, { recursive: true, force: true }) + } +} + +it('selects the measured current root profile with the actual installer metadata', () => { + expect(select()).toBe('shared=true') + expect( + select({ manifest: { ...supportedManifest, packageManager: 'pnpm@12.8.1+sha512.fixture' } }) + ).toBe('shared=true') +}) + +it.each([ + ['historical Node', { manifest: { ...supportedManifest, engines: { node: '22' } } }], + ['historical pnpm', { manifest: { ...supportedManifest, packageManager: 'pnpm@10.0.0' } }], + ['unmeasured pnpm', { manifest: { ...supportedManifest, packageManager: 'pnpm@12.8.10' } }], + ['missing toolchain', { manifest: {} }], + [ + 'extra lifecycle work', + { manifest: { ...supportedManifest, scripts: { postinstall: 'generate' } } } + ], + ['self-hosted runner', { kind: 'self-hosted' }], + ['job container', { container: 'container-id' }], + ['another OS', { os: 'Windows' }], + ['another architecture', { arch: 'ARM64' }], + ['missing installer', { missing: '.github/actions/install-node-dependencies/action.yml' }], + ['missing native action', { missing: '.github/actions/prepare-native-runtime/action.yml' }], + ['missing runtime script', { missing: 'config/scripts/ensure-native-runtime.mjs' }], + ['old installer interface', { action: 'inputs:\n native-runtime: {}\nruns: {}\n' }], + [ + 'output-only names', + { action: 'outputs:\n native-runtime: {}\n cache-pnpm-store-lookup-only: {}\n' } + ] +])('retains the original install for %s', (_name, options) => { + expect(select(options)).toBe('shared=false') +}) + +it.each(['true', 'false', ''])('routes mode %s to one complete preparation path', (shared) => { + const enabled = (step) => + runInNewContext(step.if.replaceAll('steps.install-mode.outputs.shared', 'shared'), { shared }) + const current = steps.find((step) => step.name === 'Prepare current dependencies') + const legacy = steps.filter((step) => + [ + 'Setup pnpm', + 'Setup Node.js', + "Use external node-gyp to avoid pnpm's bundled copy", + 'Install dependencies' + ].includes(step.name) + ) + expect(legacy).toHaveLength(4) + expect(enabled(current)).toBe(shared === 'true') + expect(legacy.every((step) => enabled(step) === (shared !== 'true'))).toBe(true) + expect(current.with).toEqual({ + 'native-runtime': 'electron', + 'cache-electron-package': 'true', + 'cache-pnpm-store-lookup-only': 'true' + }) + expect(legacy.at(-1).run).toBe('pnpm install --frozen-lockfile') + expect(steps.find((step) => step.name === 'Run terminal scale perf report gate').run).toContain( + 'pnpm run test:e2e:terminal-perf:scale:report' + ) +}) diff --git a/config/scripts/windows-cmd-shim-spawn-boundary.test.mjs b/config/scripts/windows-cmd-shim-spawn-boundary.test.mjs index 80263ec4b65c..a4a267b45cee 100644 --- a/config/scripts/windows-cmd-shim-spawn-boundary.test.mjs +++ b/config/scripts/windows-cmd-shim-spawn-boundary.test.mjs @@ -29,7 +29,6 @@ const WINDOWS_SHIM_SPAWN_ALLOWLIST = [ 'config/scripts/dev-cli-terminal-wrapper.mjs', 'config/scripts/dev-cli-terminal-wrapper.test.mjs', 'config/scripts/electron-builder-config.test.mjs', - 'config/scripts/ensure-native-runtime.test.mjs', 'config/scripts/live-remote-freeze-rpc.mjs', 'config/scripts/pty-transcript-secret-scan.test.mjs', 'config/scripts/remote-agent-session-authority-repro.mjs', diff --git a/config/scripts/windows-pe-imports.mjs b/config/scripts/windows-pe-imports.mjs new file mode 100644 index 000000000000..340bc08a2aee --- /dev/null +++ b/config/scripts/windows-pe-imports.mjs @@ -0,0 +1,54 @@ +// Why a hand-rolled reader: the release guard only needs the import table's DLL +// names, and a parser dependency would be a new supply-chain input for one check. +const IMPORT_DIRECTORY_INDEX = 1 + +export function readPeImportedDllNames(buffer) { + const peOffset = buffer.readUInt32LE(0x3c) + if (buffer.toString('latin1', peOffset, peOffset + 4) !== 'PE\0\0') { + throw new Error('Not a PE image') + } + const sectionCount = buffer.readUInt16LE(peOffset + 6) + const optionalHeaderSize = buffer.readUInt16LE(peOffset + 20) + const optionalHeader = peOffset + 24 + const isPe32Plus = buffer.readUInt16LE(optionalHeader) === 0x20b + const dataDirectories = optionalHeader + (isPe32Plus ? 112 : 96) + const importRva = buffer.readUInt32LE(dataDirectories + IMPORT_DIRECTORY_INDEX * 8) + if (importRva === 0) { + return [] + } + const sections = [] + for (let index = 0; index < sectionCount; index += 1) { + const header = optionalHeader + optionalHeaderSize + index * 40 + sections.push({ + virtualAddress: buffer.readUInt32LE(header + 12), + size: Math.max(buffer.readUInt32LE(header + 8), buffer.readUInt32LE(header + 16)), + rawOffset: buffer.readUInt32LE(header + 20) + }) + } + const fileOffset = (rva) => { + const section = sections.find( + (candidate) => + rva >= candidate.virtualAddress && rva < candidate.virtualAddress + candidate.size + ) + if (!section) { + throw new Error(`RVA 0x${rva.toString(16)} is outside every section`) + } + return rva - section.virtualAddress + section.rawOffset + } + const names = [] + for (let descriptor = fileOffset(importRva); ; descriptor += 20) { + const nameRva = buffer.readUInt32LE(descriptor + 12) + if (nameRva === 0) { + return names + } + const start = fileOffset(nameRva) + names.push(buffer.toString('latin1', start, buffer.indexOf(0, start))) + } +} + +// The Visual C++ runtime DLLs ship with the Redistributable, not with Windows. +const DYNAMIC_VC_RUNTIME_RE = /^(?:vcruntime|msvcp|msvcr)\d+(?:_\d+)?\.dll$/i + +export function findDynamicVcRuntimeImports(dllNames) { + return dllNames.filter((name) => DYNAMIC_VC_RUNTIME_RE.test(name)) +} diff --git a/config/scripts/windows-pe-imports.test.mjs b/config/scripts/windows-pe-imports.test.mjs new file mode 100644 index 000000000000..11cfbbfc78e5 --- /dev/null +++ b/config/scripts/windows-pe-imports.test.mjs @@ -0,0 +1,66 @@ +import { describe, expect, it } from 'vitest' +import { findDynamicVcRuntimeImports, readPeImportedDllNames } from './windows-pe-imports.mjs' + +// Smallest PE32+ image with one section holding an import directory for `dllNames`. +function peWithImports(dllNames) { + const peOffset = 0x40 + const optionalHeaderSize = 240 + const sectionHeader = peOffset + 24 + optionalHeaderSize + const rawOffset = 0x200 + const virtualAddress = 0x1000 + const descriptorsSize = (dllNames.length + 1) * 20 + const strings = dllNames.map((name) => Buffer.from(`${name}\0`, 'latin1')) + const sectionSize = descriptorsSize + strings.reduce((sum, item) => sum + item.length, 0) + const buffer = Buffer.alloc(rawOffset + sectionSize) + buffer.write('MZ', 0, 'latin1') + buffer.writeUInt32LE(peOffset, 0x3c) + buffer.write('PE\0\0', peOffset, 'latin1') + buffer.writeUInt16LE(0x8664, peOffset + 4) + buffer.writeUInt16LE(1, peOffset + 6) + buffer.writeUInt16LE(optionalHeaderSize, peOffset + 20) + buffer.writeUInt16LE(0x20b, peOffset + 24) + buffer.writeUInt32LE(virtualAddress, peOffset + 24 + 112 + 8) + buffer.writeUInt32LE(sectionSize, sectionHeader + 8) + buffer.writeUInt32LE(virtualAddress, sectionHeader + 12) + buffer.writeUInt32LE(sectionSize, sectionHeader + 16) + buffer.writeUInt32LE(rawOffset, sectionHeader + 20) + let stringRva = virtualAddress + descriptorsSize + strings.forEach((item, index) => { + buffer.writeUInt32LE(stringRva, rawOffset + index * 20 + 12) + item.copy(buffer, rawOffset + (stringRva - virtualAddress)) + stringRva += item.length + }) + return buffer +} + +describe('windows PE imports', () => { + it('reads every imported DLL name in order', () => { + const names = ['KERNEL32.dll', 'VCRUNTIME140.dll', 'api-ms-win-crt-runtime-l1-1-0.dll'] + expect(readPeImportedDllNames(peWithImports(names))).toEqual(names) + }) + + it('reads an image with no imports', () => { + expect(readPeImportedDllNames(peWithImports([]))).toEqual([]) + }) + + it('rejects a file that is not a PE image', () => { + const buffer = Buffer.alloc(0x80) + buffer.writeUInt32LE(0x40, 0x3c) + expect(() => readPeImportedDllNames(buffer)).toThrow('Not a PE image') + }) + + it('flags only the Visual C++ Redistributable DLLs', () => { + expect( + findDynamicVcRuntimeImports([ + 'KERNEL32.dll', + 'ntdll.dll', + 'VCRUNTIME140.dll', + 'vcruntime140_1.dll', + 'MSVCP140.dll', + 'msvcr120.dll', + 'api-ms-win-crt-heap-l1-1-0.dll', + 'ucrtbase.dll' + ]) + ).toEqual(['VCRUNTIME140.dll', 'vcruntime140_1.dll', 'MSVCP140.dll', 'msvcr120.dll']) + }) +}) diff --git a/config/scripts/windows-process-tree-gyp-rebuild.mjs b/config/scripts/windows-process-tree-gyp-rebuild.mjs index ba3c699a7a6b..f782334628d0 100644 --- a/config/scripts/windows-process-tree-gyp-rebuild.mjs +++ b/config/scripts/windows-process-tree-gyp-rebuild.mjs @@ -54,17 +54,27 @@ export const WINDOWS_PROCESS_TREE_NODE_ADDON_API_HEADERS = [ 'napi-inl.deprecated.h' ] -export function nodeGypRebuildInvocation(arch, packageDir = WINDOWS_PROCESS_TREE_PACKAGE_DIR) { +export function nodeGypRebuildInvocation( + arch, + packageDir = WINDOWS_PROCESS_TREE_PACKAGE_DIR, + nodeGypEntry = join(ROOT, 'node_modules', 'node-gyp', 'bin', 'node-gyp.js') +) { return { - args: [ - join(ROOT, 'node_modules', 'node-gyp', 'bin', 'node-gyp.js'), - 'rebuild', - `--arch=${arch}` - ], + args: [nodeGypEntry, 'rebuild', `--arch=${arch}`], cwd: realpathSync(packageDir) } } +export function nodeGypRebuildTimeoutMs( + moduleName, + { platform = process.platform, arch = process.arch, ci = process.env.CI } = {} +) { + // Cold headers and toolchain discovery consumed over four minutes on Windows ARM CI. + return moduleName === 'node-pty' && platform === 'win32' && arch === 'arm64' && ci === 'true' + ? 600_000 + : 300_000 +} + /** The binary the addon actually loads. */ export function windowsProcessTreeAddonPath(packageDir = WINDOWS_PROCESS_TREE_PACKAGE_DIR) { return join(packageDir, 'build', 'Release', 'windows_process_tree.node') diff --git a/config/scripts/windows-process-tree-gyp-rebuild.test.mjs b/config/scripts/windows-process-tree-gyp-rebuild.test.mjs index a33506116f1d..620deba58c4e 100644 --- a/config/scripts/windows-process-tree-gyp-rebuild.test.mjs +++ b/config/scripts/windows-process-tree-gyp-rebuild.test.mjs @@ -16,6 +16,7 @@ import { assertWindowsProcessTreeRuntimeCreationTime, inspectWindowsProcessTreeAddon, nodeGypRebuildInvocation, + nodeGypRebuildTimeoutMs, stageWindowsProcessTreeNodeAddonApiHeaders, WINDOWS_PROCESS_TREE_NODE_ADDON_API_HEADERS, WINDOWS_PROCESS_TREE_PACKAGE_DIR @@ -46,6 +47,41 @@ describe('windows-process-tree node-gyp rebuild', () => { expect(args).toContain('--arch=arm64') }) + it('preserves an external node-gyp entry and the physical addon cwd', () => { + const entry = join(tmpdir(), 'external-node-gyp', 'bin', 'node-gyp.js') + expect(nodeGypRebuildInvocation('arm64', import.meta.dirname, entry)).toEqual({ + args: [entry, 'rebuild', '--arch=arm64'], + cwd: realpathSync(import.meta.dirname) + }) + }) + + it('allows cold setup and compilation only for node-pty on a Windows ARM CI host', () => { + expect( + nodeGypRebuildTimeoutMs('node-pty', { platform: 'win32', arch: 'arm64', ci: 'true' }) + ).toBe(600_000) + }) + + it.each([ + { moduleName: 'node-pty', platform: 'win32', arch: 'x64', ci: 'true' }, + { moduleName: 'node-pty', platform: 'linux', arch: 'arm64', ci: 'true' }, + { moduleName: 'node-pty', platform: 'darwin', arch: 'arm64', ci: 'true' }, + { moduleName: 'node-pty', platform: 'win32', arch: 'arm64', ci: '' }, + { moduleName: 'node-pty', platform: 'win32', arch: 'arm64', ci: 'false' }, + { moduleName: 'node-pty', platform: 'win32', arch: 'arm64', ci: '1' }, + { moduleName: '@orca/windows-registry', platform: 'win32', arch: 'arm64', ci: 'true' }, + { moduleName: '@vscode/windows-process-tree', platform: 'win32', arch: 'arm64', ci: 'true' } + ])('keeps the five-minute bound for $moduleName on $platform/$arch with CI=$ci', (host) => { + expect(nodeGypRebuildTimeoutMs(host.moduleName, host)).toBe(300_000) + }) + + it('uses the execution host rather than an ARM cross-compilation target', () => { + const { args } = nodeGypRebuildInvocation('arm64', import.meta.dirname) + expect(args).toContain('--arch=arm64') + expect( + nodeGypRebuildTimeoutMs('node-pty', { platform: 'win32', arch: 'x64', ci: 'true' }) + ).toBe(300_000) + }) + it('copies node-addon-api headers into the patched include dir', () => { const packageDir = mkdtempSync(join(tmpdir(), 'orca-windows-process-tree-headers-')) try { diff --git a/config/ts-nocheck-baseline.txt b/config/ts-nocheck-baseline.txt index 7b102bdf419f..787e9ffe89a4 100644 --- a/config/ts-nocheck-baseline.txt +++ b/config/ts-nocheck-baseline.txt @@ -163,7 +163,6 @@ src/main/runtime/runtime-file-commands-revoke-terminal-file-grants-for-client.ts src/main/runtime/runtime-file-commands-search-local-runtime-files.ts src/main/runtime/runtime-file-commands-search-remote-quick-open-file-paths.ts src/main/runtime/runtime-file-commands-search-runtime-files.ts -src/main/runtime/runtime-file-commands-ssh-file-watcher-rearm.ts src/main/runtime/runtime-file-commands-terminal-artifact-access.ts src/main/runtime/runtime-file-commands-terminal-file-paths.ts src/main/runtime/runtime-file-commands-write-file-explorer-file.ts diff --git a/config/tsconfig.cli.json b/config/tsconfig.cli.json index 903562b67ede..44edcfd90b7e 100644 --- a/config/tsconfig.cli.json +++ b/config/tsconfig.cli.json @@ -4,11 +4,13 @@ "../src/cli/**/*", "../src/shared/**/*", "../src/main/agent-state-file-reader.ts", + "../src/main/gitlab/project-ref-parser.ts", "../src/main/agent-hooks/grok-replay-guard.ts", "../src/main/claude/hook-script.ts", "../src/main/claude/claude-hook-event-versions.ts", "../src/main/claude/claude-managed-hook-events.ts", "../src/main/qoder/hook-service.ts", + "../src/main/qwen-code/hook-service.ts", "../src/main/codebuddy/hook-service.ts", "../src/main/agent-hooks/hook-stdin-contract.ts", "../src/main/agent-hooks/hook-post-command.ts", @@ -103,6 +105,7 @@ "../src/main/amp/managed-plugin-install-status.ts", "../src/main/antigravity/hook-events.ts", "../src/main/antigravity/hook-script.ts", + "../src/main/antigravity/windows-hook-json-post.ts", "../src/main/antigravity/hook-service.ts", "../src/main/antigravity/hooks-json-bundle.ts", "../src/main/claude/hook-settings.ts", @@ -221,6 +224,9 @@ "../src/main/hermes/hook-service.ts", "../src/main/git-bash.ts", "../src/main/in-flight-run-dedupe.ts", + "../src/main/jcode/hook-settings.ts", + "../src/main/jcode/hook-config.ts", + "../src/main/jcode/hook-service.ts", "../src/main/kimi/hook-service.ts", "../src/main/kimi/kimi-hook-config-toml.ts", "../src/main/dsh/dsh-home-patch.ts", @@ -235,6 +241,7 @@ "../src/main/openclaude/hook-service.ts", "../src/main/rolling-file-backup.ts", "../src/main/startup/hydrate-shell-path.ts", + "../src/main/startup/shell-path-probe.ts", "../src/main/startup/windows-shell-path-ownership.ts", // Why: serve-electron-flag-parity.test.ts checks the Electron-side serve argv rewrite against this // project's serve spec; the module has no imports, so listing it pulls in nothing else. diff --git a/docs/assets/readme-downloads.svg b/docs/assets/readme-downloads.svg index fb18038c92fa..557299f72590 100644 --- a/docs/assets/readme-downloads.svg +++ b/docs/assets/readme-downloads.svg @@ -1,5 +1,5 @@ - - downloads: 87m + + downloads: 89m @@ -15,7 +15,7 @@ downloads downloads - 87m - 87m + 89m + 89m diff --git a/docs/reference/agent-status-store.md b/docs/reference/agent-status-store.md index e60e683c7389..5dae5fafafbd 100644 --- a/docs/reference/agent-status-store.md +++ b/docs/reference/agent-status-store.md @@ -446,8 +446,8 @@ call it. `terminal wait --for tui-idle` is a reader too. Before STA-9100 hook state reached it only through the ` ready` titles the window writes, so a headless `orca serve` never saw it (#16095). Now an agent whose rule file says -`profile.hooks: "authoritative"` (OpenCode, OpenCode 2, Pi, OMP) has its -fresh row read straight from the store, through the same +`profile.hooks: "authoritative"` (OpenCode, OpenCode 2, Pi, OMP) or +`"turn-end"` (Codex) has its fresh row read straight from the store, through the same `selectFreshExplicitAgentStatusRow` join prompt-receipt verification uses (`src/main/runtime/tui-idle-hook-lane.ts`): @@ -469,11 +469,14 @@ fresh row read straight from the store, through the same OSC 133 zones itself; - every other agent stays `identity-only`: Claude sends no event when an approval is denied or Esc stops a tool, so its row can sit at `waiting` or - `working` forever, and the rules keep deciding. Codex is identity-only too: - before its `Interrupt` hook an Esc mid-turn leaves the row `working`, and an - older TUI can hand its hooks to a newer shared app server, so no version - check tells which Codex posts it. Current Codex settles fast anyway, since - `Interrupt` drives its `Codex ready` title. + `working` forever, and the rules keep deciding; +- Codex is `turn-end`: only a `done` decides (it settles the wait), and a + `working` or permission row leaves the decision to the rules. Before its + `Interrupt` hook an Esc mid-turn can leave the row `working`, and an older + TUI can hand its hooks to a newer shared app server, so no version check + tells which Codex posts it. A `done` is a real turn end on every version, so + trusting only that one keeps the headless gain (no quiet window after the + turn) without letting a missing cancel hang the wait. The titles stay for display; remote clients read them. diff --git a/docs/reference/antigravity-native-accounts.md b/docs/reference/antigravity-native-accounts.md new file mode 100644 index 000000000000..29fe943afded --- /dev/null +++ b/docs/reference/antigravity-native-accounts.md @@ -0,0 +1,90 @@ +# Native Antigravity Accounts + +Accounts reads the credential authority on the runtime that owns execution. A client chooses +an owning Orca runtime and a host/distro target before sending an operation; it never replaces +the client's Mac Keychain item for another host. The RPC capability is +`accounts.antigravity-native.v1`. Older paired hosts are refused before account mutations. +The RPC returns account summaries only, never credential JSON, access tokens or refresh tokens. +Displayed quota is tied to the subject and authentication method observed during its refresh; +an external identity change hides the previous account's quota without an automatic fetch. + +## Supported authority + +Normal macOS agy uses service `gemini`, account `antigravity`. Its go-keyring values use the +base64 or legacy hex wrapper. Orca passes writes through `security -i` stdin, validates bounded +output and reads the entire native value back. The command buffer limit is checked before +writing. A missing native item falls back to the CLI-specific +`~/.gemini/antigravity-cli/antigravity-oauth-token` file. The distinct legacy jetski fallback +is not imported. + +The compiled CLI bypasses keyring storage when SSH/WSL environment detectors or WSL kernel +identity apply. A runtime running under that evidenced bypass reads/writes its own CLI file; +it does not contact the client keychain. The file must be private and regular. A macOS +`cache/antigravity-keyring-unavailable` marker makes authority uncertain: Orca refuses instead +of assuming that the keychain or file wins. + +Native Windows Credential Manager, native Linux Secret Service, and operations directed from +Windows Orca to a selected WSL distro are explicitly unsupported pending verified adapters. +Windows file bypass is also refused until private ACL protection is verified. +Windows' `gemini:antigravity` raw blob and 2560-byte limit are different from the Mac wrapper; +Linux uses the login collection with `service=gemini`, `username=antigravity`. No dependency, +PowerShell compilation, credential-home flag, or cross-host fallback is invented here. +A separate SSH relay has no Accounts RPC; use a paired owning runtime that implements it. + +## Identity and snapshots + +A Google ID token supplies the normalized Google issuer and stable subject. The authentication +method also scopes identity. The label uses a verified email when available; email is never the +identity key. Account record IDs are random and survive token, expiry, refresh-token and email +rotation. Profiles without a stable subject can be displayed but cannot be saved for switching. + +Snapshots preserve the exact native JSON, including fields that Orca does not interpret. The +host's vault under `userData/antigravity-accounts/vault` requires meaningful OS encryption and +private permissions. Weak or unavailable encryption is refused. Unreadable/corrupt ciphertext +is preserved; it is never treated as an empty vault. This does not migrate the experimental +candidate's incompatible array vault or token-hash IDs. + +One host service serializes Add, Select, Remove, launch checks and refresh reconciliation. +It re-reads the vault after asynchronous native reads and captures external CLI refreshes into +the same stable account. Selection reconciles the outgoing snapshot, checks the expected native +bytes before writing, and checks native readback before publishing the selected ID. It avoids +writing an old snapshot over an already-active account. The current or selected account cannot +be removed; deletion checks the latest native value again before committing. + +A selected account is checked before new Orca PTY launches, including desktop daemon and +headless runtime paths. An externally changed native identity blocks the launch and asks the +user to select again. Existing sessions can retain their original credentials in memory. +Shell commands typed manually into a running terminal are outside the Orca launch guard. + +## Sign-in and concurrency limits + +Sign-in uses the supported ordinary agy browser/code flow. Users run agy on the owning host; +to add a different account they use its `/logout` command, complete the next sign-in, then save +the actual resulting account in Orca. This implementation does not advertise an Orca-managed +login or invent an agy `login`/`--login` flag. Browser completion and a second real Google +account remain user-driven; tests do not sign out or change the developer's real native item. + +Native keyring does not expose compare-and-swap. Orca's queue serializes its own calls, and +bounded before/after checks detect observed conflicts; another independently running agy or +Orca process can still write between the final check and the write or launch. A failed +verification may mean the native item changed but selection was not persisted. Refresh and +explicit selection resolve that state; automatic rollback could destroy a newer CLI refresh +and is deliberately avoided. The file backend has the same external-writer limit. + +## Evidence and contributor credit + +The foundation adapts the reviewed codec/macOS adapter from #21784 and account-service concepts +from #21797 (nwparker), with fresh identity, persistence, serialization and conflict handling. +The signed-in Accounts card and quota-error visibility acknowledge #19588 by @artile; quota +transport is reused from current main rather than its obsolete extraction code. Targeted +multi-account UI/target concepts acknowledge #23761 by @Tai-DT, replacing its placeholder login +and unused settings selection. The Accounts legacy-Gemini clarification acknowledges #21682 +and the original relevant migration contribution by @siddqamar, as requested in #17345. +No stale development stack was cherry-picked. + +Live proof uses a disposable Mac service/account item, a fully isolated hidden Electron home, +and synthetic accounts. A private task-only copy was also selected through the real service; +installed agy 1.2.14 consumed that verified file credential under its SSH bypass and returned +`command.name=usage`, `num_turns=0`, no conversation. The real native item remained unchanged. +This proves the Mac adapter mechanics and actual CLI file authority, not a second-account +native-keychain switch, native Windows/Linux switching, or WSL/SSH relay deployment. diff --git a/docs/reference/ci-demand-rollout.md b/docs/reference/ci-demand-rollout.md index c1be4219d6b2..d8092ef412f8 100644 --- a/docs/reference/ci-demand-rollout.md +++ b/docs/reference/ci-demand-rollout.md @@ -10,24 +10,34 @@ or a guaranteed forecast of savings. ## What runs now -| Work | Ordinary draft update | Ready PR / final checks | Main reference | -| -------------------------------------- | ------------------------------------------------------------------------------ | --------------------------------------------- | --------------------------------------- | -| Static analysis and types | Immediately | Immediately | Existing workflows | -| Unit suite | Full, with shadow selection evidence | Full | Existing daily Node 24/26 x86 suite | -| Packages | After successful static analysis and types | Same | Existing release workflows | -| Headless Node persistence | Linux x64 for ordinary runtime changes; all six platforms for sensitive inputs | All six platforms when server inputs changed | Full nightly qualification at 11:30 UTC | -| Headless Node glibc/musl qualification | Sensitive inputs only, after persistence succeeds | Both architectures after persistence succeeds | Both architectures | -| E2E | Existing targeted routing | Existing targeted routing | One complete run at 17:00 UTC | - -Headless-runtime-sensitive inputs include root/toolchain files, configuration, native code, -resources, platform-specific paths, persistence, SQLite, orcad, providers, -daemon, SSH, relay, and child-process code. Dependency discovery failure, a -missing event or an incomplete diff retains full qualification. An unrelated -change still skips these lanes through the existing dependency classifier. No native -artifact is shared across platforms or ABIs. The routine draft reduction is an -explicit coverage-placement change; it does not assert identical per-update -coverage. Every non-draft synchronize event and the ready-for-review event -restores full qualification. +| Work | Ordinary draft update | Ready PR / final checks | Main reference | +| -------------------------------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Static analysis and types | Immediately | Immediately | Existing workflows | +| Unit suite | Full, with shadow selection evidence | Full | Existing daily Node 24/26 x86 suite | +| Packages | After successful static analysis and types | Same | Existing release workflows | +| Headless Node persistence | Deferred until ready | Linux x64 for ordinary runtime changes; explicit platform families or all six for sensitive inputs | All six platforms for relevant main pushes; full nightly qualification at 11:30 UTC | +| Headless Node glibc/musl qualification | Deferred until ready | Linux-specific or full qualification, after persistence succeeds | Both architectures for relevant main pushes and nightly qualification | +| E2E | Existing targeted routing | Existing targeted routing | One complete run at 17:00 UTC | + +Headless draft updates carry no verdict; readiness starts the checks. Relevant +ready PRs retain Linux x64 smoke coverage. Explicit Windows or macOS paths add both +architectures in that family, while Linux paths add Linux ARM and the glibc/musl +lanes. Root/toolchain inputs, native build inputs, shared execution/storage paths, +SSH, providers and relay changes retain every platform. Missing or incomplete +change evidence and failed import analysis also retain full qualification. +Unrelated changes skip through the dependency classifier. Relevant main pushes +qualify all six platforms and both Linux compatibility architectures; detection +uses the whole push before qualification can supersede an older relevant run. + +The detector checks known build inputs with the pinned Node toolchain first. +Other paths install dependencies for import analysis; an uncertain result never +skips qualification. This changes setup cost, not the qualification policy. + +Windows server prebuilds are cached separately by architecture and pinned build +inputs, including the runner image. Only successful qualification on main publishes +them. Consumers validate the payload and still run the pinned-Node load/spawn smoke; +a cache miss or invalid payload builds fresh. Nightly, manual and release builds +remain fresh. No native artifact is shared across platforms or ABIs. Expensive PR jobs wait for static/type success. This reduces fan-out for failed or rapidly superseded commits without sleeping on a runner. Successful isolated @@ -35,6 +45,15 @@ PRs pay the extra stage latency. Existing per-PR cancellation remains in place. Package assertions, native boundaries, SSH/folder coverage, cache warming and slow-test assertions are retained. +The daemon running-work test imports the shared probe directly, with the daemon's +process inspector supplied as its callback. The renderer keeps its existing +adapter and forwarding tests. This removes a mocked renderer dependency from the +headless graph without changing the probe algorithm or skipping backend tests. +At validation, the graph fell from 6,018 inputs (1,070 renderer inputs) to 4,879 +inputs (no renderer inputs), including nine added shared-probe cases. Renderer +adapter changes no longer qualify the headless matrix; shared probe and daemon +test changes still do. Future actual renderer imports remain discoverable. + ## Unit selection rollout PR planning runs alongside typechecking after their shared dependency setup; an diff --git a/docs/reference/ci-runner-efficiency.md b/docs/reference/ci-runner-efficiency.md index 6c7f0d059cf3..8130b770dd94 100644 --- a/docs/reference/ci-runner-efficiency.md +++ b/docs/reference/ci-runner-efficiency.md @@ -46,6 +46,40 @@ used 42 aggregate runner-minutes across 11 test jobs. The estimates 34.9 headless runner-hours, including 23.4 in cancelled runs. These are baseline observations; post-merge savings have not yet been measured. +## October 2 headless detector compiler cache + +The deferred detector already avoids dependency setup for known build inputs. +For changes that need import analysis, the collector marks package imports external; +only esbuild and its platform binary are needed. A small compiler archive can replace +root dependency setup for this analysis, while qualification jobs still install normally. + +The existing Linux x64 warmer packs these two packages after its frozen, +script-free, policy-checked install. Only main publishes. Readers use an exact key +covering Node/platform/architecture, manifests, install policy, patches and the cache +implementation. The producer and reader use the same archive path. File hashes, +identity and a compiler smoke are checked before availability is reported; missing, +invalid or failed restores use the original full installer. Graph analysis retains +its existing conservative full-qualification verdict on errors. + +A [three-pair hosted comparison](https://github.com/stablyai/orca/actions/runs/37071724200) +passed on Ubuntu x64 with Node 24.21.0 and esbuild 0.28.2. Every pair produced the +same 6,018 source inputs. Sample 2 ran the compiler-only treatment first; samples 1 +and 3 ran the existing installer first. Each used a fresh dependency tree, and the +compiler treatment required a real cache hit and validated its bytes and smoke. + +| Sample | Full installer + graph | Compiler restore + graph | Paired saving | +| ------ | ---------------------- | ------------------------ | ------------- | +| 1 | 11.730s | 2.805s | 8.925s | +| 2 | 14.702s | 4.706s | 9.996s | +| 3 | 14.666s | 3.750s | 10.916s | + +The median paired saving is 9.996 seconds. Inter-step overhead, archive transfer, +validation and the real graph are included. Checkout, initial Node setup, dependency +resets, seed work, post-job cache saves, tests and queues are excluded. These are +warm detector measurements, not whole-workflow or billing savings. The trial uses +the same package layout and validation as the production helper; production also +resolves its policy fingerprint. Cold or changed identities still install fully. + ## SSH Windows slot reuse The SSH Windows host workflow uses the same server-slot preparation action as @@ -76,6 +110,171 @@ request reuse only following an exact prepared native-cache hit; manual SSH and all release builders retain fresh compilation. Subsequent staging checks still run. Hosted validation and the reuse interval remain to be measured. +## Windows root download stores: registry installs finish sooner + +Three paired samples on each Windows architecture compared the existing exact +main download-store restore with a fresh registry install. Each treatment used a +fresh dependency tree, store and pnpm metadata, with registry-first ordering in +sample 2. Both restored the same policy-checked verification record before timing. +All six pairs used Node 24.21.0 and pnpm 12.8.1; manifest digests and installed +lockfile digests matched, and both retained frozen, script-free installation. + +| Runner | Cached totals (seconds) | Registry totals (seconds) | Paired median saving | +| ------------- | --------------------------- | --------------------------- | -------------------- | +| Windows x64 | 26.820 / 28.885 / 27.751 | 13.644 / 14.126 / 12.908 | 14.759 seconds | +| Windows ARM64 | 216.540 / 288.492 / 189.342 | 119.856 / 238.562 / 115.611 | 73.731 seconds | + +The x64 samples are the three successful Windows 2022 jobs in +[run 37064549378](https://github.com/stablyai/orca/actions/runs/37064549378). +Its ARM cleanup guard rejected pnpm's setup-owned store path before measurement; +those incomplete ARM jobs are excluded. The corrected +[ARM-only run](https://github.com/stablyai/orca/actions/runs/37065220916) passed all +three samples. Earlier rejected measurements also stopped before installation +because an optional config file was absent; none count toward these timings. + +Intervals include actual store lookup/restore, inter-step overhead and root +installation. Checkout, toolchain setup, tree/store reset, verification-record +restoration and native preparation are excluded. ARM variation is substantial; +these samples do not measure whole-workflow, queue or billing savings. + +Root-only Windows x64/ARM64 PR installs now skip the download-store restore. +The existing x64 mixed-install exception remains. An explicit store opt-out also +lets Windows headless persistence and SSH jobs avoid the archive on main or +manual runs. Frozen installs, verification records, native caches and every +qualification check remain. Other lockfile sets and platforms keep their +existing policy. Default non-PR writers, including the warmer, still seed stores +for direct setup-node consumers and workflows that run package scripts. + +## October 2 Linux root store comparison + +A [six-job hosted comparison](https://github.com/stablyai/orca/actions/runs/37073978443) +measured the actual main root-store archive against direct registry installation, +with three fresh-runner pairs on each Linux architecture. All six jobs passed. +The middle sample on each architecture reversed treatment order. Between treatments, +the driver removed the dependency tree, store and pnpm metadata, then restored the +same policy-checked verification record before timing. Frozen, script-free installs +preserved policy files and produced identical installed lockfile digests in each pair. + +| Architecture/sample | Store restore + install | Direct registry install | Paired saving | +| ------------------- | ----------------------- | ----------------------- | ------------- | +| x64 / 1 | 6.516s | 5.372s | 1.144s | +| x64 / 2 | 6.743s | 3.950s | 2.793s | +| x64 / 3 | 6.600s | 3.985s | 2.615s | +| ARM64 / 1 | 7.632s | 3.346s | 4.286s | +| ARM64 / 2 | 5.504s | 3.575s | 1.929s | +| ARM64 / 3 | 5.450s | 3.364s | 2.086s | + +Median paired savings are 2.615 seconds on x64 and 2.086 seconds on ARM64; means +are 2.184 and 2.767 seconds. Both used Node 24.21.0 and pnpm 12.8.1. Actual store +lookup/transfer/restore, inter-step overhead and installation are timed. Checkout, +initial toolchain/dependency setup, preparing the existing process wrapper, +dependency resets, verification-record restores, native work, tests, post-job cache +saves and queues are excluded. Package services have already been used by initial +setup. These are warm-policy setup measurements, not workflow or billing savings. + +The shared installer consequently skips root-only Linux x64/ARM64 store restores +on PRs. It still installs and checks every package through pnpm. Mixed mobile and +custom lockfile sets, other architectures, verification/native caches, +main store writers and release installation policies keep their existing behavior. +The measured Windows exceptions remain. Mac restores were retained at this stage; +the following comparison supersedes that policy. No periodic job or cache is added. + +## October 2 macOS root store comparison + +A [six-job hosted comparison](https://github.com/stablyai/orca/actions/runs/37078232553) +used the same paired method on macOS 15 Intel and Apple Silicon. All six jobs +passed with actual main store cache hits. The middle sample reversed treatment +order. Each treatment started with a reset dependency tree, store and pnpm +metadata, followed by the same verification-record restore. Policy files and +installed lockfile digests matched within every pair. + +| Architecture/sample | Store restore + install | Direct registry install | Paired saving | +| ------------------- | ----------------------- | ----------------------- | ------------- | +| x64 / 1 | 87.270s | 60.584s | 26.686s | +| x64 / 2 | 103.280s | 52.640s | 50.640s | +| x64 / 3 | 61.564s | 40.768s | 20.796s | +| ARM64 / 1 | 26.024s | 14.375s | 11.649s | +| ARM64 / 2 | 37.000s | 19.726s | 17.274s | +| ARM64 / 3 | 34.616s | 14.888s | 19.728s | + +Median paired savings are 26.686 seconds on x64 and 17.274 seconds on ARM64; +means are 32.707 and 16.217 seconds. Node matched within each pair: 24.19.0 on +Intel and 24.20.0 on Apple Silicon, as resolved by the existing installer. Both +used pnpm 12.8.1. Timing includes actual cache lookup/transfer/restore, inter-step +overhead and installation. Checkout, initial setup, process-wrapper preparation, +resets, verification restores, native work, tests, cache saves and queues are +excluded. Initial setup has already used package services. These measurements +do not establish whole-workflow or billing savings. + +The existing root-only PR exception now also covers macOS x64/ARM64. Frozen, +script-free installs and pnpm policy checks still run. Mixed/custom lockfile sets, +other architectures, verification/native caches, main/manual store writers and +release installation policies retain their existing behavior. No periodic job +or cache is added. + +## October 2 store producers: keep caches without downloading hits + +The optional `cache-pnpm-store-lookup-only` installer input uses +[`actions/cache` lookup-only](https://github.com/actions/cache#inputs) on non-PR +runs. An exact hit refreshes cache access without extracting the archive; a miss +still installs from the registry and publishes the populated store at successful +job completion. The default remains the existing `setup-node` cache behavior. +The four Linux/Windows dependency warmers and Linux/macOS persistence producers +opt in. Windows persistence retains its existing store opt-out, and PR restore +policies are unchanged. This adds no recurring job or extra cache family. + +A [tiny framework control](https://github.com/stablyai/orca/actions/runs/37082688033) +proved that lookup left the payload absent, refreshed the existing cache's access +time, and published a miss that a fresh job restored. A +[nested composite control](https://github.com/stablyai/orca/actions/runs/37084946789) +then saved and restored a fresh payload using the actual environment-path pattern. +The installer exports its resolved store path through `GITHUB_ENV`: twice-nested composite post-job saves +cannot resolve their internal step outputs. The primary key is captured +by the cache action before cleanup. Paths, architecture and lockfile keys match +`setup-node`, so existing default-branch archives remain reusable. + +The [six-platform installer screen](https://github.com/stablyai/orca/actions/runs/37084946789), +[Linux repeats](https://github.com/stablyai/orca/actions/runs/37085164277), and +[corrected Windows repeats](https://github.com/stablyai/orca/actions/runs/37085248976) +compared the complete shared installer, including toolchain setup, cache actions, +policy verification, frozen installation and native probes where requested. +Each treatment reset dependencies, the store, pnpm metadata and the Windows +registry build directory. Treatment order reversed across architectures and +repeats. Every qualified pair required real main store cache hits, matching +policy/installed-lockfile digests and Node/pnpm versions, plus exact native-cache +hits on Linux and Windows. The initial Windows x64 screen stopped before timing +because its benchmark guard rejected the standard `D:\.pnpm-store` path; that +unqualified job is excluded. + +| Platform / sample | Restore + installer | Lookup + installer | Paired saving | +| ------------------------ | ------------------: | -----------------: | ------------: | +| macOS ARM64 | 37.785s | 20.024s | 17.761s | +| macOS x64 | 75.610s | 46.638s | 28.972s | +| Linux ARM64 / 1 | 10.005s | 7.242s | 2.763s | +| Linux ARM64 / 2 | 8.817s | 6.672s | 2.145s | +| Linux ARM64 / 3 | 8.800s | 6.581s | 2.219s | +| Linux x64 / 1 | 12.309s | 9.735s | 2.574s | +| Linux x64 / 2 | 10.131s | 8.690s | 1.441s | +| Linux x64 / 3 | 13.741s | 9.485s | 4.256s | +| Windows ARM64 / repeat 1 | 145.818s | 78.729s | 67.089s | +| Windows ARM64 / repeat 2 | 152.730s | 106.475s | 46.255s | +| Windows ARM64 / screen | 294.092s | 193.957s | 100.135s | +| Windows x64 / 1 | 30.936s | 20.256s | 10.680s | +| Windows x64 / 2 | 31.021s | 20.098s | 10.923s | + +All 13 qualified pairs improved. Median paired savings were 2.574 seconds on +Linux x64, 2.219 on Linux ARM64, 10.802 on Windows x64 and 67.089 on Windows +ARM64. Each macOS architecture had one pair; its 28.972 / 17.761 second savings +are a screen, supported by the earlier three-pair root-store comparisons. + +All pairs used pnpm 12.8.1. Node was 24.21.0 on Linux and Windows, 24.19.0 on +macOS Intel and 24.20.0 on macOS ARM. Source dependency policies were frozen for +this screen; later main dependency changes do not extend these measurements. +Timing excludes checkout, initial service/bootstrap use, wrapper compilation, +resets, result validation, post-job saves and queues. These are installation +measurements, not whole-workflow or billing savings. Cold publication is verified +separately by the small controls; no large synthetic store cache was uploaded. + ## October 1 Windows and dependency cache follow-up [PR #24355](https://github.com/stablyai/orca/pull/24355) merged at `197ea3a3`. @@ -679,8 +878,8 @@ coverage, and release behavior: Shard 4 spent 535 worker-seconds importing and 357 executing tests; a uniform per-file import estimate misses that cost. See [timing refresh](../../config/scripts/ci-shard-timings.md). - Seed Node 24 native modules, the pinned Git compatibility binary, and TypeScript - state on the default branch, hourly - and when dependency/toolchain inputs change. One ten-minute-bounded hosted job + state on the default branch when dependency/toolchain inputs change, with + scheduled recovery (originally hourly; now every six hours). One ten-minute-bounded hosted job reuses existing cache keys and skips typechecking an already-cached commit. New PRs can restore default-branch caches, while caches saved by another PR are inaccessible. The audit found 80 entries totaling 10.67 GiB, including @@ -1216,7 +1415,7 @@ These are single cold/warm observations, not paired medians or a measured whole-workflow saving. They demonstrate usable exact-key reuse after publication; future savings depend on cache availability and unchanged native inputs. The trial seeds belong to this PR's merge ref. Other PRs require a main-branch seed -after merging this new namespace; the existing main push and hourly warming +after merging this new namespace; the existing main-push and scheduled warming jobs provide that seed. ## Separate mobile install verification: retain the current policy @@ -1269,7 +1468,7 @@ This measures the three-file oracle cohort. Whole-shard timings include other test bodies, imports and transforms, so a whole-suite saving needs separate measurement. -## Cache warming: let hourly ticks wait for active work +## Cache warming: let scheduled ticks wait for active work The hourly warmer previously cancelled an active warmer, even when both used the same source. On October 2, the [merge-triggered run](https://github.com/stablyai/orca/actions/runs/36965832780) @@ -1289,6 +1488,27 @@ scheduled run or its repeated successful lanes, and pending replacement still applies regardless of the cancellation expression. The bounded 20-run sample contains this collision; it does not establish a recurring or whole-CI saving. +## Cache warming: six-hour recovery interval + +Scheduled warming now runs at 00:41, 06:41, 12:41 and 18:41 UTC instead of hourly. +Main pushes that change cache inputs still seed immediately, and manual dispatch +remains available. All five jobs, probes, keys and publication rules remain. +This removes 20 scheduled workflows and 100 scheduled job starts per day (83%). + +Four consecutive October 2 scheduled runs used the same source. The +[18:50 UTC run](https://github.com/stablyai/orca/actions/runs/37050194510) used 474 +aggregate runner-seconds across five jobs, including 242 seconds on Windows ARM. +That job restored exact package, verification and native caches; package-store +restore alone took about 70 seconds. Repeating that observed duration twenty +fewer times would avoid about 158 runner-minutes daily, but this one-run estimate +is not a billing forecast or measured post-rollout saving. + +The longer interval can delay background repair after eviction or runner-image +changes. Existing consumers retain cold-cache installation/build fallback, and +normal cache reads update last access. Storage was near the repository limit +when audited, so retention and unchanged hit rates are not guaranteed. Observe +misses before reducing the recovery frequency further. + ## Daemon shutdown fixture: remove build tools after compilation The fixture now removes compiler and Python build dependencies, plus npm and @@ -1392,3 +1612,350 @@ one CI failure does not establish a failure-rate reduction. The bounded 50-head main sample ending at 8ff6296 contained no root package metadata changes. Removing app-version metadata from the Windows server cache key would not improve reuse in that sample, so the key remains unchanged. + +## Windows ARM SSH: prepare the inbox capability during independent builds + +The ARM inbox lane starts guarded Windows capability preparation after the pure +provisioning self-test and waits for it before any private SSH server or host cell +runs. Dependency installation and the unchanged native artifacts can run during +that preparation. Preview and x64 lanes keep their existing serial provisioning; +the registered background step completes without mutation in those lanes. + +The preparation and the foreground provider use the same installer and isolation +guards. The receipt must match the source, run, attempt, runner, image and native +architecture. The foreground provider still reads the installed capability and +verifies every native binary and Microsoft signature. Account ownership, ACLs, +DefaultShell, private service identity, host cells and cleanup remain independent +checks. A background failure propagates through the unconditional native wait. + +Two full four-lane pairs used frozen source refs and the same dependency and +native-install policy. The [first baseline](https://github.com/stablyai/orca/actions/runs/36986929163) +ran before the [first candidate](https://github.com/stablyai/orca/actions/runs/36986970976); +the [second candidate](https://github.com/stablyai/orca/actions/runs/36991232037) +was dispatched before the [second baseline](https://github.com/stablyai/orca/actions/runs/36991234729). +Runner image versions matched within each platform in both pairs. + +| Active job, seconds | First baseline | First candidate | Second baseline | Second candidate | +| ------------------- | -------------: | --------------: | --------------: | ---------------: | +| ARM inbox | 2,403 | 1,644 | 2,353 | 1,667 | +| ARM preview | 1,002 | 935 | 886 | 872 | +| x64 inbox | 636 | 732 | 616 | 620 | +| x64 preview | 562 | 561 | 623 | 566 | + +The ARM inbox observations improved by 759 and 686 seconds. Baseline dependency +installation and artifact builds consumed 501 and 498 seconds before capability +installation could start. Candidate capability installation ran during that +work, but also took about 261 and 232 seconds less than the baseline. Candidate +dependency installation was slower, particularly in the second pair. These +observations support overlap on ARM; they do not establish a guaranteed 11–13 +minute saving, a reduction in queue time, or the cause of installer variability. +The x64 lane showed no repeatable gain, so it keeps serial preparation. + +All 16 actual Windows providers and 48 host-cell verdicts passed across the two +pairs. Receipts verify native machine identity, private service absence, owned +process exit, account removal and key removal. Loaded profile disposition remains +separate from those required cleanup checks. Hosted execution also verified the +native background/wait syntax; older actionlint versions do not recognize it. + +### Overlap the private profile observation budgets + +After service deletion and owned process exit, profile cleanup polls each owned +SID with its own full 30-second monotonic budget. Independent budgets now run +together. Every deletion follows a fresh targeted read; loaded profiles remain +for disposable VM destruction. Service identity, PID ownership, process exit, +account removal and key removal still fail the complete provider on error. + +The maintained diagnostics self-test executes the actual cleanup try/catch with +scoped Windows API and clock controls. Eight positive cases cover full windows, +late unload, reload, query overhead, mixed states and missing SIDs; ten specific +failure cases cover foreign profiles and the required cleanup gates. Disposable +shortened-deadline and stale-snapshot mutations fail those controls. A separate +mocked real-clock observation took 30.179 seconds for three loaded profiles, +compared with about 90 seconds for serial full budgets. This measures polling, +not an actual Windows provider or the entire job. + +The third profile no longer gains incidental extra time while earlier profiles +consume their budgets. A profile unloading at 45 seconds may therefore remain +where serial cleanup removed it. This uses the existing disposable-VM fallback; +it does not remove a loaded profile or relax mandatory account/key cleanup. +Hosted qualification of the combined workflow remains pending. + +## Coordinator mail tests: advance observation windows without removing them + +Six cases advance their original six 1,500 ms and ten 100 ms observation windows +with a scoped clock. Real filesystem, SQLite, journal, RPC and runtime work still +finishes asynchronously. The original journal-read gate and all counter and +operation assertions remain. Cancellation during delayed startup and the +Date-only age case retain real timers. Teardown stops the host and closes the +database before advancing the known 2,000 ms orphan repair, then asserts no fake +timers remain and restores the clock in `finally`. + +Two opposite-order local pairs passed the same 23 cases and unchanged source +hashes. Selected-case totals fell from 13.674 to 3.318 seconds and from 13.276 to +6.323 seconds. Whole-file test totals fell from 24.845 to 10.829 seconds and from +21.500 to 19.410 seconds. Process wall times were 41.488/37.810 seconds and +42.140/78.450 seconds; the reverse candidate spent 56.31 seconds importing under +unrelated local load. Local process-wall savings were inconclusive. + +The later [hosted x64 and ARM comparison](https://github.com/stablyai/orca/actions/runs/37001891871) +passed the same 23 cases in `structured-chat-coordinator-mail.test.ts` in both +orders on each architecture, with frozen case and policy hashes. Median full-file +wall time was 33.506 → 23.149 seconds on x64 and 33.438 → 22.504 seconds on ARM. +Median test-body totals were 19.329 → 9.343 and 19.695 → 9.103 seconds, respectively. +These measurements qualify this file; they do not measure whole-PR time. + +Injected extra deliveries at 1,499 ms and 99 ms still fail the original assertions +in both clock modes. The latter candidate fails the unchanged journal-read gate +with the same extra provider start. A separate control confirms the orphan repair +actually executes against the closed database and leaves no fake timers. The +change retains all 121 original expectation sites and adds one teardown check; +it does not shorten the runtime's observation interval or claim a whole-PR gain. + +## Stub child shutdown clocks: Codex and Claude + +[Merged Codex change #24893](https://github.com/stablyai/orca/pull/24893) scopes timeout +clocks to two synthetic-child cases in `codex-app-server-connection.test.ts`. +The full platform graceful deadline and 1,000 ms forced wait remain; the test +waits for the actual stub SIGKILL before advancing the forced window. Streams, +process-table reads, Date and immediate callbacks remain real. Fault controls +still detect late exit, missing EPIPE, missing exit proof and unwanted notification. + +The [hosted ARM comparison](https://github.com/stablyai/orca/actions/runs/37074124526) +passed the same 32 full-file cases in baseline/candidate and candidate/baseline +order. File wall times were 13.671 / 13.674 seconds originally and 1.658 / 1.649 +seconds with scoped clocks. Installer time is excluded; generated caches remain +across the disclosed order. Real-child coverage and production shutdown code remain. + +[Merged Claude change #24897](https://github.com/stablyai/orca/pull/24897) changes only +two synthetic-child cases in `claude-agent-sdk-exit-proof.test.ts`. Both full +33-case runs passed, including the unchanged five real-child cases. In one local +macOS pair, the two bodies took 2,503 / 1,502 ms originally and 1.37 / 0.39 ms with +scoped clocks. They cross a real immediate callback before advancing the complete +1,500 ms graceful and 1,000 ms forced windows, restore timers in `finally`, and +retain the original false exit verdicts. Fault controls detect either deadline +shortened by one millisecond, an unproved true verdict and a leftover timer. +[Normal PR CI](https://github.com/stablyai/orca/actions/runs/37075819218) passed; +these local body measurements do not establish hosted or whole-PR time savings. + +## Sequential static analysis and typecheck: retain separate jobs + +A four-trial hosted screen kept the slim router unchanged and compared the two +independent ARM jobs with one ARM job running their unchanged checks sequentially. +The [compiler/planner census](https://github.com/stablyai/orca/actions/runs/37069472888) +matched all compiler inputs and the full 10,477-file unit inventory in separate, +shared root-only and shared mixed-install states. The [safety qualification](https://github.com/stablyai/orca/actions/runs/37075043747) +verified native joins after compiler failure and a real late action-post failure; +all four guarded downstream sentinels skipped and the audit passed. + +| Trial | Mode | Active ARM seconds | Router finish to heavy finish, seconds | +| -------------------------------------------------------------- | -------- | -----------------: | -------------------------------------: | +| [1](https://github.com/stablyai/orca/actions/runs/37075574191) | Separate | 157 | 124 | +| [2](https://github.com/stablyai/orca/actions/runs/37075887937) | Combined | 134 | 150 | +| [3](https://github.com/stablyai/orca/actions/runs/37076222202) | Combined | 129 | 134 | +| [4](https://github.com/stablyai/orca/actions/runs/37076786281) | Separate | 159 | 194 | + +Both pairs saved active ARM time: 23 and 30 seconds, or 14.6% and 18.9%, with one +heavy admission instead of two. The active critical path was 15 and 8 seconds +longer. Downstream eligibility changed by +26 and −60 seconds; observed ready-to-start +delay differences of +11 and −68 seconds explain that reversal. Created-to-start +delay is recorded separately and does not establish a quota or queue cause. + +Retain separate jobs for now. This screen shows a capacity saving, with a longer +active critical path and no repeatable latency gain. All trials used frozen +`cc73c8e1a72b0e9ee9c29e57458ce307f5f019c2` source, manual workflow dispatches, +Node 24.21.0 and the same four exact primary cache hits. Main's later +[Linux PR root-store policy change #24896](https://github.com/stablyai/orca/pull/24896) +is outside this screen. The trial ran actual heavy checks and proved unit and both +package eligibility, without launching those downstream matrices or measuring a +whole-PR speedup. + +## Linux headless runtime build overlap + +The historical pinned Bun artifact now builds in a native background step while +current native preparation and Node bundling run in the foreground. An +unconditional join precedes the unchanged artifact and cross-runtime tests. Bun +setup stays Linux-only; other platforms register and join a successful no-op. +The producer publishes step outputs consumed only by those tests. The existing +selector, native floors, template builders and cache policies remain. + +A [hosted alternating comparison](https://github.com/stablyai/orca/actions/runs/37072923774) +ran four serial/overlap arms on each of two Linux VMs: + +| Architecture | Serial preparation, seconds | Overlapped preparation, seconds | +| ------------ | --------------------------: | ------------------------------: | +| x64 | 20.831 / 19.576 | 11.149 / 10.914 | +| ARM64 | 15.155 / 14.396 | 8.566 / 8.553 | + +Every arm passed the same 961 cases across 92 files: 930 passed and 31 skipped. +Both cross-runtime persistence cases passed. The two live daemon-handover cases +kept their existing protocol-version skips. All four x64 arms passed actual Node +18 loading and pinned-runtime handoff. Installed/source inputs and artifact +inventories matched; each normal owned-process ledger was clean before cleanup. +Common native compiler warmup preceded timing and retained its generated Python +caches in the strict installed ledger. These are preparation savings of 5.8–9.7 +seconds, excluding setup, cold installs, runner start delays and whole-PR time. + +Actual [Bun failure](https://github.com/stablyai/orca/actions/runs/37078015921) and +[Node failure](https://github.com/stablyai/orca/actions/runs/37078021568) controls +qualified genuine compiler errors with fresh live opposite builders, native joins, +skipped consumers, restored inputs and verified exits. A [normal cancellation +control](https://github.com/stablyai/orca/actions/runs/37079655167) received SIGINT +while the actual Bun builder was freshly live; both builders and the detached +owned child had simultaneous earlier readiness. All three native joins had terminal dispositions of cancelled, success and +cancelled, and every consumer skipped. The temporary observer retired its owned processes; +the collector independently verified their absence and unchanged inputs. This +proves signal delivery and observer-owned retirement, without establishing +runner-only descendant cleanup at the join. The unchanged historical builder +starts finite build/smoke work, and its children retain GitHub's normal orphan +tracking marker. + +Earlier cancellation trials remain excluded from live-build qualification: one +collector stopped its observer before signal routing, and the corrected trial +received the signal after both builders finished. The qualifying trial requested +normal cancellation earlier in the same preparation sequence to account for +observed delivery delay; no workload, wait or proof predicate was shortened. + +## October 3 Terminal Perf dependency preparation + +The daily/manual Terminal Perf workflow still installed current dependencies through +raw lifecycle scripts and a global node-gyp installation. Its historical `ref` +input also accepts revisions that lack the shared installer, so replacing that +path unconditionally would break older runs. The current-profile path now uses +the existing shared installer with explicit Electron preparation and archive +caching. A guard requires GitHub-hosted Linux x64, Node 24/pnpm 12.8.1, the +native-only root postinstall and the needed local action inputs/files. Other +profiles and historical revisions keep their original frozen install. + +The [hosted comparison](https://github.com/stablyai/orca/actions/runs/37101695800) +ran both preparation paths in each of two Linux x64 jobs, reversing their order. +Legacy/shared preparation took 25.164/16.956 seconds and 27.434/18.032 seconds: +8.208 and 9.402 seconds saved. Both used Node 24.21.0, pnpm 12.8.1 and Electron +43.7.5. Both shared native-module cache lookups missed, so this improvement did +not depend on a warm native build. Electron archive and root pnpm cache lookups +hit. Dependency trees, pnpm data and Electron archives were reset between paths; +compiler headers and external services were not. Bootstrap, resets, validation, +post-job cleanup, queueing and the production guard step are outside those times. +These are preparation measurements, not whole-workflow or billing savings. + +Both paths passed a native-module probe inside the actual Electron executable +with `ELECTRON_RUN_AS_NODE=1`, and built the same Electron-vite e2e application. +The candidate's 18 focused routing/fallback tests, workflow actionlint and changed +code-quality checks passed. Performance tests, budgets and report uploads remain +unchanged. The [existing October 2 run](https://github.com/stablyai/orca/actions/runs/36985792125) +failed the same-workspace 50/100-terminal budgets (46.9/50.2 ms against 25 ms). +This dependency change does not claim to resolve those application regressions. + +The [full candidate integration](https://github.com/stablyai/orca/actions/runs/37104625474) +passed on `df71ad849cd854a232f7063562785563743b641a`: current preparation was +selected, its native cache missed and rebuilt, the app built and all 32 report +annotation rows passed the unchanged budget checker. The downloaded report also +passed the same checker locally. This is integration evidence; it does not +attribute application latency changes to dependency preparation. Subsequent +rebases resolved report documentation and incorporated fixture teardown fixes. +Workflow, installer-action and toolchain content stayed unchanged. Main also +added an import and a Windows-only MSBuild setting to the native-runtime script: +the imported helper has no top-level side effects, and the Linux rebuild branch +is unchanged. Focused tests verify its Linux/macOS no-op behavior. Final-head PR +checks qualify separately. + +## October 3 producer follow-up: automatic selection for the measured profile + +The first producer rollout in [#24927](https://github.com/stablyai/orca/pull/24927) +passed all 46 PR checks, all five manual warmers and all 11 manual Headless +qualifications on `a2c489c0cca5e46d24333a4d40ba910af0de0208`. The same root installer +also serves recurring unit, browser and performance workflows that had not opted +in. The follow-up defaults the existing input to `auto`, reusing lookup mode for +non-PR root-only installs on GitHub-hosted Linux/macOS/Windows x64/ARM64 runners, +with no job container, the manifest's Node 24/pnpm 12.8.1 profile and no conflicting +Node override. Explicit `true` and `false` retain their previous meanings. Mixed +lockfiles, other toolchains, containers and self-hosted runners retain full cache +restoration; PR policies are unchanged. The manifest check runs only when the +context is potentially eligible, before setup-node chooses its cache behavior. + +A second cleanup audit distinguished nesting depth. The +[twice-nested control](https://github.com/stablyai/orca/actions/runs/37087090689) +published the environment-path payload and lost the output-path payload with an +`Input required and not supplied: path` warning. The +[direct control](https://github.com/stablyai/orca/actions/runs/37087211236) published +and restored both payloads. Current Electron archive callers are direct, so they +need no cache-path change. Keeping the producer's exported path also makes its +new lookup mode safe for callers that nest the shared installer. These tiny +controls establish publication behavior, not installer time savings. + +The [actual automatic-mode cold publisher control](https://github.com/stablyai/orca/actions/runs/37097980789) +passed both jobs on `7b8858bdc8f`. A twice-nested wrapper called the installer +without overriding its default input. The writer selected lookup, missed its +unique root-lockfile key, completed the frozen policy-checked install and saved +that key during cleanup. A fresh reader restored the exact key and installed the +same dependency successfully. The fixture retained the manifest toolchain and +applicable workspace policies; its one dependency keeps the publication check +small. Two earlier trials failed fixture assertions (the pnpm multi-document +header placement, then its empty cache-miss output), and are excluded. This proves +automatic selection and cold publication, not a new timing result. Local +verification passed eight suites / 184 tests, the changed-code quality gate and +compiled-composite actionlint. + +## October 3 retired-cache collection observation + +The same owner-collection assertion failed in unit shard 3 of +[37098089274](https://github.com/stablyai/orca/actions/runs/37098089274/attempts/1) +and [37100365037](https://github.com/stablyai/orca/actions/runs/37100365037/attempts/1), +requiring a full shard retry despite the focused suite passing locally. Its +three-turn collection budget was shorter than the six-turn plus final yield +pattern already used by the GitLab known-host retirement tests. + +The fixture now uses that existing observation budget. All seven tests, their assertions, +expiry clocks and production code are unchanged. The focused suite passes. A +local fault control changed only the production timer callback to hold its owner +strongly: the owner-collection assertion failed, with the other six tests passing. +The source was restored afterward. Extra collection turns therefore preserve the +strong-retention oracle. Hosted qualification is still required; these observations +do not prove a particular VM-retention cause or quantify avoided retries. + +## October 3 unit-selection evidence: include failed references + +The caller's `needs.test.result == 'success'` condition prevented the advisory +collector from reading failed unit runs, despite the reviewer's existing support +for failed tests. A six-run screen from the October 3 occupancy sample found only +one review artifact; it was a full fallback, so it did not validate selection. +Missing artifacts cannot establish that selection catches red tests. + +The caller now permits both success and failure while excluding cancellation and +skipped tests. The collector remains advisory and absent from `verify` dependencies. +Incomplete, interrupted or inconsistent shard records still cannot become complete +reference evidence. Existing omitted-failure tests preserve that negative control. + +The five artifacts from failed [run 37098089274, attempt 1](https://github.com/stablyai/orca/actions/runs/37098089274/attempts/1) +were reviewed locally using the unchanged script. It recognized a complete failed +reference covering 10,606 files and 9,270,307 worker-ms. Its candidate was the full +fallback, so `selectionEvaluated` remained false and no selection promotion is +justified by this control. Focused workflow/reviewer checks passed 24 tests, +including actual caller-expression outcomes for success, failure, skipped and +cancelled states. This repair supplies needed evidence for a later optimization; +it claims no runner-time savings and does not enable selected tests. + +The updated caller also passed the hosted red-run control in +[37100365037](https://github.com/stablyai/orca/actions/runs/37100365037). +The collector succeeded after one unit shard failed, while required verification +remained red. Its review recognized all five shards as a complete reference +(10,608 files, 8,965,977 worker-ms). This was again a full fallback with +`selectionEvaluated: false`, not evidence for enabling selected tests. + +## October 3 removal fixture cleanup ordering + +[37105566358](https://github.com/stablyai/orca/actions/runs/37105566358) +failed unit shard 4 with `ENOTEMPTY` removing the failed-removal fixture's temporary +directory; the other four shards passed. A client's removal reply intentionally +precedes the detached job's final record persistence. This fixture reset tracking +and removed the directory before waiting for that persistence, allowing a writer +to race cleanup. Its teardown now awaits the existing settlement helper before +resetting tracking or deleting the fixture. Production removal behavior and all +assertions are unchanged. + +All 1,348 runtime tests passed (one existing skip). A temporary controlled queue +held the final record write after the client replied: waiting before reset stayed +pending and passed; resetting before waiting lost the tracked job and failed the +same ordering assertion. The gate was released, both controls drained the captured +job, and the instrumentation was removed. Changed-code quality passed. This proves +the teardown ordering mechanism, not a measured avoided-retry saving. Final-head +hosted qualification remains required. diff --git a/docs/reference/jcode-hook-events.md b/docs/reference/jcode-hook-events.md new file mode 100644 index 000000000000..5eaf0d524636 --- /dev/null +++ b/docs/reference/jcode-hook-events.md @@ -0,0 +1,163 @@ +# jcode hook events + +What jcode actually emits, and why Orca's status mapping is shaped the way it is. +Everything below was captured from jcode **v0.87.1 (944f747e9)** by pointing every +`[hooks]` entry in `config.toml` at a script that appends `$JCODE_HOOK_PAYLOAD` to +a log, then running real turns. Re-capture before changing the mapping; do not +edit it from memory. + +## The six events + +jcode's `[hooks]` table (`crates/jcode-base/src/hooks.rs`) has six lifecycle +points. Five are **observers** — detached, fire-and-forget, they can never slow +the agent. One, `pre_tool`, is a **gate**: jcode spawns it, writes the tool input +to its stdin, and waits for it to exit before the tool runs. + +| Event | When | Orca state | Notable payload fields | +| --------------- | -------------------------------------------- | ---------- | --------------------------------------------------------- | +| `session_start` | TUI open, attach, or `--resume` | none | `source` = `create`/`attach`/`resume`, `model` | +| `turn_start` | prompt submitted, before the model generates | `working` | `source`, `model` | +| `pre_tool` | before each tool call (gate) | `working` | `tool_name`, `tool_input` (argument JSON as a string) | +| `post_tool` | after each tool call | `working` | `tool_name`, `status`, `duration_ms`, `output_bytes`/`error` | +| `turn_end` | turn finished | `done` | `status`, `duration_ms`, `model`, `last_assistant_text`, `error` | +| `session_end` | session closed | `done` | `source` = `close` | + +`session_start` is identity-only. jcode fires it on an idle TUI open, so mapping +it to `working` would spin before the user has typed anything (same reason Devin +does not map its `SessionStart`). + +## Captured payloads + +A `jcode run` turn that read one file and wrote another: + +```json +{"cwd":"/private/tmp/jcode-work","event":"session_start","model":"claude-haiku-4-5","session_id":"session_pawprint_1790149117838_071c2a106812396c","source":"create"} +{"cwd":"/private/tmp/jcode-work","event":"pre_tool","session_id":"session_pawprint_…","tool_input":"{\"file_path\":\"sample.txt\",\"intent\":\"Read sample.txt to get its contents\"}","tool_name":"read"} +{"cwd":"/private/tmp/jcode-work","duration_ms":"0","event":"post_tool","output_bytes":"12","session_id":"session_pawprint_…","status":"ok","tool_name":"read"} +{"cwd":"/private/tmp/jcode-work","event":"pre_tool","session_id":"session_pawprint_…","tool_input":"{\"content\":\"HELLO\",\"file_path\":\"out.txt\",\"intent\":\"Write uppercased contents of sample.txt to out.txt\"}","tool_name":"write"} +{"cwd":"/private/tmp/jcode-work","duration_ms":"9","event":"post_tool","output_bytes":"137","session_id":"session_pawprint_…","status":"ok","tool_name":"write"} +``` + +A TUI turn that failed upstream (note `turn_start`, which the `run` path does not emit): + +```json +{"cwd":"/private/tmp/jcode-work","event":"session_start","model":"claude-opus-5","session_id":"session_snail_…","source":"create"} +{"cwd":"/private/tmp/jcode-work","event":"turn_start","model":"claude-opus-5","session_id":"session_snail_…","source":"chat"} +{"cwd":"/private/tmp/jcode-work","duration_ms":"6868","error":"Anthropic API error (503 Service Unavailable): …","event":"turn_end","model":"claude-opus-5","session_id":"session_snail_…","status":"error"} +``` + +Three consequences the mapping depends on: + +- **`turn_start` only fires on the streaming turn path** (TUI, desktop, swarm + workers, headless sessions), not `jcode run`. It is what fills the otherwise + blank window between a submitted prompt and the first tool call. +- **Only `pre_tool` carries `tool_input`.** `post_tool` reports the name and the + outcome, so the tool preview has to be held from the matching `pre_tool`. +- **Every jcode tool schema has an `intent` string** the model fills in. It is + the preview fallback when no tool-specific key (`file_path`, `command`, …) + matches. + +## Why Orca subscribes to the gate + +`pre_tool` is the only event that can report a tool *while it runs*. Without it a +three-minute `bash` shows no tool at all until it finishes. Two rules keep the +gate from ever costing the agent anything: + +1. **The POST is detached.** jcode calls `child.wait_with_output()`, which waits + for the process *and* reads its stderr to EOF — a backgrounded child that + inherited stderr would hold the gate open for as long as it ran. The managed + script runs the POST as `orca_post_jcode_event >/dev/null 2>&1 &`, so the + inherited pipes are closed and the script exits immediately. +2. **stdin is drained first.** jcode `write_all`s the full tool input to the + hook's stdin. A tool input larger than the pipe buffer (a big `write`) would + block that write until the gate timed out if nobody read it, so the script + drains stdin before any exit path. + +Orca never blocks a jcode tool call: the script always exits 0. + +## Questions and permissions + +jcode has **no interactive per-tool approval prompt**. Its safety model +(`crates/jcode-app-core/src/tool/bash_destructive_gate.rs`) either denies a +command outright or asks the model to justify it — both inside the tool, with no +human in the loop. There is therefore no hook, and no terminal-title state, for +"jcode is waiting on you" during ordinary tool use. + +The one tool a *human* answers is ambient mode's `request_permission` +(`crates/jcode-app-core/src/tool/ambient.rs`), resolved out of band with +`jcode permissions`. Orca maps a `pre_tool` for it to `waiting` and publishes the +tool input as the question card. `post_tool` for the same tool is *not* mapped — +by then the human has already answered. + +Matching is by exact tool name. jcode's live tool set is `agentgrep, apply_patch, +bash, batch, bg, browser, compile_remote, conversation_search, edit, gmail, +integration_tools, ls, macos_computer_use, maintainer_feedback, mcp, memory, +multiedit, open, panel, patch, read, schedule, session_search, side_panel, +skill_manage, swarm, todo, webfetch, websearch, write` plus the ambient tools; +a substring rule over that set would be matching on coincidence. + +## Terminal titles + +jcode paints OSC 0 titles roughly once a second. Captured sequence from one TUI +session: + +``` +jcode → 🐍 jcode Snake → 🐍 jcode/creek Snake → 🌐 jcode Snake · work ~0s → … → 🌐 jcode Snake · last ~6s +``` + +The format is ` jcode [ · +N -M][ · work|last ~]` +(`crates/jcode-tui/src/tui/app/terminal_title.rs`). Orca uses it for tab-bar +identity only — status comes from hooks, never from a parsed title. Note there is +no "needs input" title state; that is the same gap as above, not an omission in +the parser. + +## Per-pane daemons + +jcode runs one server/client daemon per runtime dir, and lifecycle hooks fire +*inside the daemon*. Every TUI client connects the daemon the first pane started, +so without isolation a second jcode pane's events carry the first pane's +`ORCA_PANE_KEY` and its status lands on the wrong tab. + +jcode does forward a client's terminal identity to hooks +(`CLIENT_TERMINAL_ENV_VARS` in `crates/jcode-terminal-launch/src/lib.rs`), but +that allowlist covers tmux/zellij/herdr and the terminal emulators — not +`ORCA_PANE_KEY`. Until it does, Orca stamps a per-pane `JCODE_RUNTIME_DIR` so +each pane gets its own daemon, socket, and lock. The value is a 16-hex hash of +the pane key because the socket path is capped at `SUN_LEN` (104 bytes) and a +full pane key never fits. + +## Windows hook launcher + +Use Jcode **v0.89.0 or newer** on Windows. Earlier observer hooks launch with +`DETACHED_PROCESS`, leaving their children without a console to inherit. A +console program such as the managed hook's `curl.exe` can then open a Windows +Terminal tab on every event. Jcode's launcher fix uses `CREATE_NO_WINDOW` for +observer hooks and the `pre_tool` gate, keeping their descendants invisible. +Changing the managed script alone cannot repair an older Jcode launcher. + +The managed Windows hook redirects its payload file into curl directly, avoiding +the extra shells that a `type ... | curl` pipeline starts. Existing managed scripts +are refreshed on Orca startup without changing the user's hook configuration. + +Report: https://github.com/stablyai/orca/pull/22539#issuecomment-5809618574 +Launcher fix: https://github.com/1jehuang/jcode/pull/1490 + +## Config shape + +`[hooks]` values accept a string or an array of strings (`HookCommands` in +`crates/jcode-config-types/src/lib.rs`), and jcode re-reads the config on reload, +so hooks can be added without restarting. jcode parses a hook command line +shell-style but **executes it directly, not through a shell** — the managed value +must be the script path, never an `if [ -f … ]` wrapper. + +That shell-style parse is `parse_hook_command` +(`crates/jcode-terminal-launch/src/lib.rs`), and it is why Orca stores the path +**shell-quoted**. The tokenizer splits on unquoted whitespace and consumes every +unquoted backslash as an escape, so a bare Windows path reaches `exec` as +`C:Usersme.orcaagent-hooksjcode-hook.cmd` and no hook fires at all; a POSIX home +with a space splits into two arguments. Single quotes pass a path through +verbatim — backslashes are literal inside them — so Orca single-quotes by +default and falls back to double quotes (escaping `\` and `"`) only for a path +that itself contains a single quote. The value is then TOML-quoted on the way +into the file, so neither the raw path nor the shell-quoted string appears +alone. diff --git a/docs/reference/managed-data-accounts.md b/docs/reference/managed-data-accounts.md new file mode 100644 index 000000000000..c0a78d001d51 --- /dev/null +++ b/docs/reference/managed-data-accounts.md @@ -0,0 +1,23 @@ +# Managed OpenCode and Devin accounts + +Run enrollment in a terminal on the machine running Orca: + +```sh +orca account add --agent opencode --label Work +orca account add --agent opencode --integration opencode-go --label Work +orca account add --agent devin --label Work +orca account list --agent opencode --json +orca account select --agent opencode --account +orca account select --agent opencode --account system +orca account rm --agent opencode --account +``` + +OpenCode enrollment requires OpenCode 2 and runs its official `auth login --standalone` command. Devin runs `auth login --force-manual-token-flow`; obtain the enrollment token through Devin's supported login flow. These commands neither reuse a guessed token nor sign out the system account. Settings → AI Provider Accounts provides the enrollment command, refresh, selection, and removal for the selected Orca host. + +Each profile belongs to the execution host. OpenCode's SQLite credentials and Devin's credential TOML stay in private Orca user-data directories. Enrollment isolates XDG data/config/cache/state, copies only authenticated credentials, and then deletes the temporary directory. OpenCode capture rejects databases containing conversations and includes SQLite WAL contents. RPC summaries contain labels, IDs, and integration names, never tokens or credential paths. Only the authenticated local runtime socket can import a credential directory; paired clients cannot ask the host to read arbitrary paths. + +Selection affects newly launched explicit OpenCode/Devin commands and agent launches. It redirects XDG data and state; OpenCode inline-auth/database overrides cannot bypass the profile. Shell wrappers restore this selection after user startup files. Existing provider configuration and environment-based integrations remain available. Running terminals retain their current profile. Stop agents before removing a profile: removal also deletes conversations created in that private profile, without changing the system login. + +For SSH, enroll by running the command on a headless Orca runtime on the remote machine. The remote runtime owns its profiles and selection; a desktop client's credential paths never cross SSH. Direct SSH relay launches and Windows-hosted WSL panes do not consume the desktop host's profiles. Run a headless runtime inside that execution environment instead. Folder workspaces use the same host account store as git worktrees. Older Orca hosts reject new operations before login through capability negotiation. + +Validation covers OpenCode 2.0.16 on macOS and Linux arm64, including isolated official enrollment, selected and System background-terminal credential checks, reselection, and profile deletion. Linux checks used the Node headless runtime in an Ubuntu 24.04 container. Devin 3000.10.31 saved-login recognition was checked on macOS. Fresh Devin manual-token enrollment, a physical SSH host, Linux desktop UI, Windows, and Windows-hosted WSL still require verification. diff --git a/docs/reference/monaco-language-associations.md b/docs/reference/monaco-language-associations.md index a159477e1761..ee985d859597 100644 --- a/docs/reference/monaco-language-associations.md +++ b/docs/reference/monaco-language-associations.md @@ -5,7 +5,11 @@ languages and loads their grammars on demand. Filename detection must not load t editor itself: it also runs during session restoration and before the editor mounts. `monaco-language-associations.json` contains the registration metadata from the -installed package's entry point. Regenerate it after upgrading Monaco: +installed package's entry point, plus curated Ruby associations in the generator. +Those add `.rake`, `.ru`, `.jbuilder`, `.thor`, `Guardfile`, `Capfile`, `Podfile`, +`Brewfile` and `Vagrantfile` to the existing Ruby grammar. Change these in +`config/scripts/generate-monaco-associations.mjs`, not the generated JSON. +Regenerate after changing the curated associations or upgrading Monaco: ```sh node config/scripts/generate-monaco-associations.mjs @@ -13,7 +17,7 @@ pnpm exec oxfmt --write src/renderer/src/lib/monaco-language-associations.json ``` The generator reads syntax trees without executing contributions or grammar loaders. -Its test compares the checked-in metadata to the installed package. The original +Its test compares the checked-in metadata to the installed package and curated associations. The original list was verified against a clone of `microsoft/monaco-editor`, tag `v0.55.1`, commit `516f350bdaf7a82f6731bd128a9ec86a6e5fa47d` (`src/basic-languages` and `src/language`). diff --git a/docs/reference/orchestration-configured-agent-aliases.md b/docs/reference/orchestration-configured-agent-aliases.md new file mode 100644 index 000000000000..2dc95ccd100f --- /dev/null +++ b/docs/reference/orchestration-configured-agent-aliases.md @@ -0,0 +1,22 @@ +# Configured command aliases for orchestration workers + +A worker can use the name of a direct executable configured in **Settings → Agents**. +Choose the built-in agent whose command-line interface the executable implements, +then set that agent's command override to the executable name or quoted full path. +For example, configure Codex's command as `codex-fugu`, then select it with +`orca orchestration worker-start --agent codex-fugu` and the normal placement options. + +The execution host resolves its own configuration. Launch receipts use the canonical +agent (`codex` in this example), and model/effort handling reuses that agent's existing +launch rules. A receipt records applied launch preferences; it does not prove provider +entitlement, successful generation, or an arbitrary vendor's model selection behavior. + +Aliases require a single executable token. Commands containing interpreter arguments, +environment assignments, or shell wrappers are not aliases. Multiple built-in agents +configured with the same executable name are ambiguous and require the canonical agent +ID. Disabled launchers remain disabled. An unconfigured name is refused even if it is +on PATH; Orca cannot infer a compatible launch interface from a process name. + +The same rule applies to folder workspaces and git worktrees. For a remote worker, +configure the command on its execution host. Older hosts may refuse aliases they do not +support. Configuring a command does not create new status producers or grant permissions. diff --git a/docs/site/.gitignore b/docs/site/.gitignore index 104c6398aca1..61917c0abcb0 100644 --- a/docs/site/.gitignore +++ b/docs/site/.gitignore @@ -8,4 +8,4 @@ next-env.d.ts # Keep local deployment credentials out of the publication boundary. .env .env.* -.npmrc +# Project .npmrc contains package policy; keep credentials in user-level npm config. diff --git a/docs/site/.npmrc b/docs/site/.npmrc new file mode 100644 index 000000000000..d7913f44df71 --- /dev/null +++ b/docs/site/.npmrc @@ -0,0 +1 @@ +minimum-release-age=4320 diff --git a/docs/site/content/docs/cli/overview.mdx b/docs/site/content/docs/cli/overview.mdx index 6d2f07928561..b9aecde4fa31 100644 --- a/docs/site/content/docs/cli/overview.mdx +++ b/docs/site/content/docs/cli/overview.mdx @@ -38,8 +38,11 @@ orca status --json ``` orca worktree ps --json orca worktree create --repo id: --name my-task --issue 123 --json +orca worktree create --repo id: --name review-pr-123 --pr 123 --json orca worktree current --json orca worktree set --worktree active --comment "reproduced bug" --json +orca worktree set --worktree active --gitlab-issue 42 --gitlab-mr 77 --json +orca worktree set --worktree active --pr null --json orca worktree rm --worktree id: --force --json ``` diff --git a/docs/site/content/docs/cli/reference.mdx b/docs/site/content/docs/cli/reference.mdx index d4cb4346d652..087caf0e02b4 100644 --- a/docs/site/content/docs/cli/reference.mdx +++ b/docs/site/content/docs/cli/reference.mdx @@ -97,11 +97,14 @@ orca worktree show --worktree active --json orca worktree create --repo id: --name fix-login --json orca worktree create --name child-task --agent codex --prompt "Investigate the flaky login test" --json orca worktree set --worktree active --comment "reproduced failure; testing token refresh fix" --json +orca worktree set --worktree active --workspace-status in-review --unread --json orca worktree rm --worktree id: --force --json ``` When `worktree create` runs from inside an Orca-managed worktree, Orca records the new worktree as a child when it can infer the relationship. Pass `--parent-worktree active` to be explicit, or `--no-parent` when the new work is independent. +`worktree set --unread` puts the unread dot on the workspace in the sidebar, the same one Orca shows when an agent finishes; `--read` clears it. + Agent startup flags: ```bash @@ -112,6 +115,22 @@ orca worktree create --name hidden-setup --setup inherit --json `--agent` launches the selected agent in the first terminal. `--prompt` sends initial work to that agent. `--setup run|skip|inherit` controls repo setup hooks; `inherit` follows the repo policy. +Link issues and reviews using the existing workspace metadata: + +```bash +orca worktree create --repo id: --name review-task --pr 123 --json +orca worktree create --repo id: --name gitlab-task --gitlab-issue 42 --gitlab-mr 77 --json +orca worktree set --worktree active --gitlab-issue '#42' --gitlab-mr '!77' --json +orca worktree set --worktree active --gitlab-issue https://gitlab.example.com/group/project/-/work_items/42 --json +orca worktree set --worktree active --pr null --gitlab-mr null --json +``` + +`--pr` is a GitHub pull request number. `--gitlab-issue` accepts an issue number or `#42`; `--gitlab-mr` accepts a merge request number or `!77`. Numbers must be positive safe integers. The GitLab flags also accept HTTP(S) issue or merge request URLs, including self-hosted instances and nested groups. The URL's host and project must match the workspace's stored GitLab source context or the repository's stored remote. If that identity is missing or different, the command fails before updating metadata. A URL cannot choose another project, change the checkout, or fetch a review branch. + +Omitting a flag leaves its link unchanged. On `set`, literal `null` clears only the named link; `create` refuses `null`. Each provider has separate fields, so setting a GitLab issue or merge request preserves GitHub and Linear links. + +Folder-based repositories can store numeric links through these commands, but a number does not supply a provider host or project. Without existing source context or a stored remote, pasted GitLab URLs are refused and provider details or clickable links may be unavailable. These flags use fields already supported by the runtime; an older runtime that predates a field may ignore it, so check `worktree show --json` after writing to an older host. + ## Terminals ```bash diff --git a/docs/site/content/docs/model/meta.json b/docs/site/content/docs/model/meta.json index 0acd89b80e3c..9ef9d2bfd9df 100644 --- a/docs/site/content/docs/model/meta.json +++ b/docs/site/content/docs/model/meta.json @@ -1,5 +1,12 @@ { "title": "The Orca Model", "defaultOpen": true, - "pages": ["worktrees", "tabs-panes-splits", "agents-sessions", "session-restore", "quick-open"] + "pages": [ + "worktrees", + "orca-yaml", + "tabs-panes-splits", + "agents-sessions", + "session-restore", + "quick-open" + ] } diff --git a/docs/site/content/docs/model/orca-yaml.mdx b/docs/site/content/docs/model/orca-yaml.mdx new file mode 100644 index 000000000000..0cc790121b2f --- /dev/null +++ b/docs/site/content/docs/model/orca-yaml.mdx @@ -0,0 +1,151 @@ +--- +title: orca.yaml & .worktreeinclude +description: Local worktree setup, terminal defaults, shared directories, and copied files. +--- + +Put `orca.yaml` and `.worktreeinclude` at the repository root. `orca.yaml` supplies project defaults; `.worktreeinclude` lists ignored files to carry into new worktrees. Commit these configuration files so other users can use the same rules. Keep secrets in the ignored files they refer to. + +This reference covers **local Git worktrees**. [Folder workspaces](/docs/model/worktrees#multi-repo-project-groups--folder-workspaces) do not create a Git checkout or run these copy/share steps. For other execution targets, see [Ways to run Orca](/docs/ways-to-run). + +## Example + +For a project that uses pnpm, this installs dependencies during setup and opens an agent tab and a Git status tab: + +```yaml +scripts: + setup: | + pnpm install + archive: | + echo Workspace archived +setupAgentStartupPolicy: wait-for-setup +defaultTabs: + - title: Agent + - title: Git status + command: git status --short +worktree: + sharedDirectories: + - .cache +``` + +The `.cache` directory must already exist and be gitignored in the primary checkout. The first tab deliberately has no command: when the desktop create composer launches an agent, its startup takes precedence over the first tab's configured command. + +## Accepted keys and defaults + +All keys are optional. Script, title, command, and path values must be strings; Orca trims them and drops empty or oversized values. Invalid fields are skipped while valid siblings still apply. Unknown keys do not configure additional behavior. + +| Key | Accepted value | When omitted | +| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | +| `scripts.setup` | Nonempty script string, including a YAML block scalar (`\|`). Runs in the new worktree when setup is enabled. | No project setup script. | +| `scripts.archive` | Nonempty script string. Runs in the worktree before archive/removal when hooks are enabled. | No project archive script. | +| `setupAgentStartupPolicy` | Exactly `start-immediately` or `wait-for-setup`. | `start-immediately`; a local **wait** setting still takes precedence. | +| `issueCommand` | Nonempty command template for linked GitHub/GitLab items in the create composer. | No project template; the composer can still supply its built-in agent prompt. | +| `defaultTabs` | List of mappings with optional `title`, `command`, and `color`. At least one valid field is required per entry. `color` accepts `#RGB` or `#RRGGBB`. | Normal initial terminal behavior. | +| `worktree.sharedDirectories` | List of repository-relative directory paths. Only existing, gitignored directories are shared. | Only the user's Settings shared paths apply. | +| `environmentRecipes` | List of per-workspace environment recipes. See [Cloud VMs](/docs/ways-to-run#4-cloud-vms-per-workspace-environments) and the `orca-per-workspace-env` skill for recipe fields and lifecycle commands. | No project recipes. | + +`scripts` and `worktree` must be mappings; `defaultTabs`, `environmentRecipes`, and `sharedDirectories` must be lists. `setupRunPolicy` and `commandSourcePolicy` are **Settings values**, not accepted `orca.yaml` keys. + +### Parse failures and limits + +**Invalid YAML or a duplicate mapping key anywhere rejects the whole file.** Orca does not keep the last duplicate value. A non-mapping root, excessive alias expansion, or an oversized file also produces no configuration. A YAML checker that accepts duplicate keys does not establish that Orca will accept the file. + +| Limit | Result when exceeded | +| --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | +| Whole file: 256 KiB (262,144 UTF-8 bytes) **and** 262,144 UTF-16 code units | Whole file rejected. | +| Each string field: 64 KiB (65,536 UTF-8 bytes) **and** 65,536 UTF-16 code units, measured before trimming | Field dropped. | +| `defaultTabs`: 256 input entries | Entire tab list dropped. | +| `environmentRecipes`: 256 input entries | Entire recipe list dropped, with a recipe diagnostic. | +| `worktree.sharedDirectories`: first 100 input entries | Later entries ignored, even if earlier entries were invalid or duplicates. | +| YAML alias expansion: parser `maxAliasCount` of 100 | Whole file rejected if the parser's expansion budget is exceeded; this is not a simple count of alias tokens. | + +## Which checkout supplies the configuration? + +The **primary checkout** is the repository folder registered in Orca. It may be on a different branch from the new worktree. + +| Configuration | Read from | +| ----------------------------------------------------- | -------------------------------------------------------------------------------------------- | +| Setup script, setup startup policy, and `defaultTabs` | The **new worktree's** `orca.yaml`. | +| Shared directory rules and their source directories | The **primary checkout's** `orca.yaml` and filesystem. | +| Copy rules and their source files | The **primary checkout's** `.worktreeinclude` and filesystem. | +| Archive script | The **primary checkout's** `orca.yaml`, executed with the worktree as its working directory. | +| Issue command | The **primary checkout's** `.orca/issue-command`, then its `orca.yaml`. | + +Changing a feature branch's `.worktreeinclude` or shared-directory list does not update the primary checkout. Before creating another worktree, make sure the primary checkout has the intended rules and the ignored source paths. Setup and tab defaults instead follow the revision checked out in the new worktree. These defaults are applied during creation; they do not synchronize existing tabs or files. + +## Setup, archive, and command selection + +In **Settings → Repository**, setup can run automatically, ask each time, or be skipped by default. A new repository defaults to running setup. The [CLI](/docs/cli/reference#worktrees) accepts `--setup run|skip|inherit`; `inherit` follows that Settings policy. A valid file does not bypass Orca's command approval. + +**Command source & orca.yaml** controls setup and archive scripts: + +| Settings choice | Scripts used | +| ---------------------------------- | ----------------------------------------------- | +| **orca.yaml only** (`shared-only`) | Only the YAML hook; a local hook is ignored. | +| **Local only** (`local-only`) | Only the local hook; the YAML hook is ignored. | +| **Run both** (`run-both`) | YAML first, then local, joined into one script. | + +When no source choice is saved, a nonempty local hook selects **Local only** for that hook; otherwise Orca uses **orca.yaml only**. Shared directories are independent of this command-source choice. + +Setup runs in a **Setup** terminal. On macOS/Linux, Orca writes a Bash runner with `set -e`; supported shell options on a leading `#!` line are replayed. On native Windows, the runner uses `.cmd` syntax and calls each nonempty line, stopping on failure. A leading POSIX-shell `#!` line opts into Bash only when Git Bash is configured and available; otherwise the `.cmd` runner refuses the script before running any commands. Choosing PowerShell as the terminal does not turn a setup script into PowerShell code. + +The setup runner receives: + +| Variable | Value | +| ----------------------------------------- | ----------------------------------------------------------------- | +| `ORCA_ROOT_PATH` | Primary checkout path. | +| `ORCA_WORKTREE_PATH` | New worktree path. | +| `ORCA_WORKSPACE_NAME` | Worktree directory basename, rather than a renamed display title. | +| `CONDUCTOR_ROOT_PATH`, `GHOSTX_ROOT_PATH` | Compatibility aliases for `ORCA_ROOT_PATH`. | + +Paths and shell syntax differ by platform. Use `$ORCA_WORKTREE_PATH` in Bash and `%ORCA_WORKTREE_PATH%` in `.cmd` scripts. `wait-for-setup` waits for successful setup before agent startup; either the YAML or local wait setting enables it. This controls startup order, not whether setup runs. + +Archive hooks run before local removal; the CLI requires `--run-hooks` to enable them. A failed archive hook blocks removal unless the caller explicitly accepts that failure. + +### Terminal defaults and issue commands + +`defaultTabs` creates terminal tabs once for the new worktree. Titles and colors still apply when commands are skipped. Tab commands follow the setup run decision and are suppressed by **Local only** (using the setup command-source policy). + +In the **desktop create composer**, an agent/startup command takes the first template tab, so the **first tab's template command is not run**. Reserve that tab for the agent and put other commands in later tabs. Local CLI creation with `--agent` instead creates a separate startup terminal alongside the configured tabs; every allowed template command can run. Tab commands do not wait for setup to finish, so a command that needs installed dependencies should be run after setup completes. The example's Git status command does not depend on the install. + +`issueCommand` has its own override: nonempty `.orca/issue-command` content wins over YAML. Clearing that local file restores the shared template. This is separate from the setup/archive command-source choice. Templates support `{{artifact_url}}` for the linked item's URL and legacy `{{issue}}` for its number; the composer decides whether to use the template. Shared YAML commands use Orca's approval flow, which can reuse saved trust; local overrides are treated as user-authored and do not trigger that shared-command prompt. + +```yaml +issueCommand: | + echo "Linked item: {{artifact_url}}" +``` + +## Sharing versus copying ignored paths + +All three mechanisms take paths relative to the primary checkout and preserve an existing destination. Settings paths are applied first, then YAML shared directories, then include copies. A path already present through sharing is not copied again. + +| Mechanism | Source entries | Result in a new local worktree | +| ------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| **Settings → Repository → Worktree Shared Paths** | Personal repository setting; existing files or directories. | APFS clone-copy on macOS when available; otherwise a link. A clone has independent contents, while a link shares edits. | +| `worktree.sharedDirectories` | Existing, **gitignored directories** listed in the primary `orca.yaml`. Adds to Settings paths. | Always a link to the primary directory, including on APFS. Windows tries a directory junction before a symlink. Edits affect the shared source. | +| `.worktreeinclude` | Existing, **gitignored files or directories** listed in the primary checkout. | Private copy, using APFS clone-copy when available and ordinary copying otherwise. It never falls back to a shared link. | + +YAML shared paths normalize backslashes to `/`, remove a leading `./` and trailing `/`, and deduplicate. Absolute paths, drive-prefixed paths, empty path segments, `.`/`..` segments, and any `.git` segment are rejected. A file, missing directory, or non-ignored directory is skipped. + +### .worktreeinclude format and copy budget + +```text +# .worktreeinclude +.env +.env.local +.vscode/settings.json +``` + +Use **one literal path per line**, anchored to the repository root. Blank lines and lines starting with `#` are ignored; inline comments are not stripped. Backslashes, a leading `./`, and trailing `/` are normalized and duplicates are removed. Glob patterns containing `*` or `?` and negation starting with `!` are skipped. Use relative paths without traversal or `.git`; only existing, gitignored entries are copied. The include file must be a regular file no larger than 256 KiB; Orca considers at most 1,000 valid literal path candidates. + +Ordinary include copying is limited to **2 GiB total file bytes and 50,000 filesystem entries** per new worktree, measured before copying. APFS clones do not consume the byte budget, but still consume the entry budget; a fallback to ordinary copying must fit the byte budget. An entry that exceeds the remaining budget is skipped and creation reports a warning. Earlier refused entries can also exhaust the bounded sizing walk, leaving later entries unmeasured. These are admission limits, not quotas against files growing during copying. + +Copying a top-level source symlink uses its target's contents. Nested symlinks remain links, so editing through one can still affect its referent. Large dependency trees usually belong in setup or deliberate sharing rather than `.worktreeinclude`. + +## When configuration appears to do nothing + +1. Check the correct checkout from the table above, the exact key spelling, value type, duplicate keys, and limits. A parser failure can disable setup, tabs, and shared directories together. +1. Check setup's run policy, command-source choice, approval, and first-tab startup behavior. `--setup run` changes the run decision; it does not override **Local only**. +1. Check that source paths exist in the primary checkout and are directories for `sharedDirectories`. Verify ignored status there with `git check-ignore -- path/to/entry`; tracked or unignored paths do not qualify for YAML sharing or include copying. +1. Check for an existing destination or a copy-budget warning. Missing shared directories are skipped without a warning; not every skipped field/path has a visible error. Absence of a warning does not prove acceptance. + +See [Worktrees](/docs/model/worktrees#shared-directories--gitignored-files) for the creation flow and [Settings](/docs/settings#repository) for repository preferences. diff --git a/docs/site/content/docs/model/worktrees.mdx b/docs/site/content/docs/model/worktrees.mdx index f71d21c2ca44..c90cf4513516 100644 --- a/docs/site/content/docs/model/worktrees.mdx +++ b/docs/site/content/docs/model/worktrees.mdx @@ -42,11 +42,13 @@ A brand-new worktree is a clean checkout. Dependencies, caches, and local secret Orca fills that gap in three complementary ways: 1. **Worktree Shared Paths** (per repo, in Settings → Repository) — paths materialize from the primary checkout into each new worktree (APFS clone-copy on macOS when possible, otherwise a symlink). -1. **`worktree.sharedDirectories` in `orca.yaml`** — repo-checked-in list of **gitignored directories** to share the same way (symlink/share, not copy). Use this for large rebuildable trees like `node_modules` or `.cache`. Entries must exist as directories in the primary checkout **and** be gitignored; tracked or missing paths are skipped. +1. **`worktree.sharedDirectories` in `orca.yaml`** — repo-checked-in list of **gitignored directories** to share by link (including on APFS). Use this for large rebuildable trees like `node_modules` or `.cache`. Entries must exist as directories in the primary checkout **and** be gitignored; tracked or missing paths are skipped. 1. **`.worktreeinclude` at the repo root** — list of **gitignored files or directories to copy** (not symlink) into each new worktree, so each worktree owns its copy. Typical entries: `.env`, local config under `.vscode/`. Blank lines and `#` comments are allowed. Only **literal** paths are supported today — globs and negation are skipped with a warning. Paths that are tracked, missing, or not gitignored are not copied. `orca.yaml` shared directories **add to** the per-user Worktree Shared Paths list; they never replace it. Paths already shared/linked are not re-copied from `.worktreeinclude`. +See the [orca.yaml & .worktreeinclude reference](/docs/model/orca-yaml) for setup scripts, default tabs, command selection, path rules, limits, and which checkout supplies each setting. + ```yaml # orca.yaml (repo root) worktree: diff --git a/docs/site/content/docs/settings.mdx b/docs/site/content/docs/settings.mdx index a5bbe9b83f48..1bf6dc794343 100644 --- a/docs/site/content/docs/settings.mdx +++ b/docs/site/content/docs/settings.mdx @@ -44,6 +44,7 @@ Settings are grouped into panes. Everything here is searchable with `Cmd-,` then - Ghostty import. - Warp theme import — bring in your Warp YAML themes with **Import themes from Warp** (auto-discovers Warp's themes folder per OS) or **Import from YAML** for any folder of Warp-format theme files. - JIS Yen (¥) to Backslash (\\) for macOS Japanese keyboards. +- **Terminal shell** (local macOS / Linux) — system shell or a custom executable, with login startup by default. **Custom shell → Advanced → Custom args** requests replacement arguments for ordinary local panes. See [startup files and argument limits](/docs/terminal#macos-and-linux-shell). - Windows default shell (PowerShell or CMD). - **Allow TUI Clipboard Writes (OSC 52)** — **on by default**. Lets Zellij, tmux, Neovim, fzf, Grok (and similar) write the system clipboard over the PTY, including over SSH. Turn off if you prefer the older lockdown. diff --git a/docs/site/content/docs/telemetry.mdx b/docs/site/content/docs/telemetry.mdx index 2dc4549401da..a8b8715812a9 100644 --- a/docs/site/content/docs/telemetry.mdx +++ b/docs/site/content/docs/telemetry.mdx @@ -20,7 +20,7 @@ Alongside each event we include basic build and platform information: the Orca v The categories of behavior we observe: - **Lifecycle** — when the app opens. Used to estimate daily, weekly, and monthly active users. -- **Repos and workspaces** — when you add a repo or create a workspace. We record _how_ you did it (e.g. folder picker vs. clone URL; command palette vs. drag-and-drop), never the repo name, URL, path, branch name, or any free-form text. +- **Repos and workspaces** — when you add a repo or create a workspace. We record _how_ you did it (e.g. folder picker vs. clone URL; command palette vs. drag-and-drop), never the repo name, URL, path, branch name, or any free-form text. For a workspace create we also record how long it took and how that time split across its steps, whether a checkout Orca had prepared in advance was reused (and if so, what reset it needed, what prepared it, how long it took to prepare and how long it waited unused; if not, a fixed reason code), for a failed create which step it stopped in, plus coarse context: whether it came from the Orca window or from the CLI, phone or agent interface, whether Git ran locally, in WSL or over SSH, a bucketed worktree count, a bucketed count of the files the repo tracks, how many other creates and prepared-checkout jobs were running, and whether the repo has a post-checkout hook (never its contents). - **Agents** — which agents you use and their token counts. Never prompts or agent output. - **Agent errors** — a coarse error category and which agent kind was involved. We never see raw error messages or stack traces; per-incident detail stays in a local diagnostic trace file on your machine and only reaches Orca if you explicitly share a diagnostic bundle. - **Settings** — when you toggle one of a small whitelisted set of feature-flag or UX preferences. We record which preference changed and whether it's a boolean or an enum, never the raw value of any free-form setting. diff --git a/docs/site/content/docs/terminal.mdx b/docs/site/content/docs/terminal.mdx index 1be3d3892f3e..8db8b24caee7 100644 --- a/docs/site/content/docs/terminal.mdx +++ b/docs/site/content/docs/terminal.mdx @@ -46,6 +46,23 @@ If you've collected themes in Warp, click **Import themes from Warp** in the ter Imported themes appear alongside Orca's built-ins in the theme dropdown. +## macOS and Linux shell + +By default, local terminal panes on macOS and Linux open your system shell (`$SHELL`) as a **login shell** (`-l`). With the default arguments: + +- **zsh** reads `.zshenv`, `.zprofile`, `.zshrc`, and `.zlogin` in order from `$ZDOTDIR`, or your home directory (`~`) if unset. Each user file follows its system counterpart (`zshenv`, `zprofile`, `zshrc`, `zlogin`), usually under `/etc` or `/etc/zsh`. +- **bash** reads `/etc/profile`, then the first of `~/.bash_profile`, `~/.bash_login`, or `~/.profile` that exists. It does **not** read `~/.bashrc` on its own. If your `PATH` or version-manager setup (nvm, asdf, mise) lives in `~/.bashrc`, source it from that login file, as many distributions' default `~/.profile` or `~/.bash_profile` already do: + + ```bash + [ -n "${BASH_VERSION:-}" ] && [ -f "$HOME/.bashrc" ] && . "$HOME/.bashrc" + ``` + + The guard keeps non-bash shells that also read `~/.profile` from loading bash-only setup, even with `set -u` enabled. Add it only if the login file bash reads does not already source `~/.bashrc`. + +When Orca uses its bash integration wrapper, it starts bash with `--rcfile` instead of `-l`. The wrapper sources the same login files without separately sourcing `~/.bashrc`; bash itself is not in login mode. + +To open a different shell, choose **Custom shell** under [Settings → Terminal → Terminal shell](/docs/settings). Under **Advanced → Custom args**, enter one argument per line to replace the default `-l` for ordinary local panes. An empty list requests no arguments: unwrapped interactive bash then reads `~/.bashrc` instead of the login files. When the terminal daemon is unavailable, Orca's bash or zsh integration can override custom arguments and retain login startup. Agent launches, startup commands, and one-off shell choices do not use this argument setting. + ## Windows shell The default shell on Windows is configurable between PowerShell, Command Prompt, and WSL under [Settings → Terminal](/docs/settings). WSL is offered automatically when `wsl.exe --status` succeeds. The **+** dropdown on the tab bar also shows a submenu so you can open a one-off tab in any shell without changing your default. diff --git a/docs/site/package.json b/docs/site/package.json index 89a977c3d40a..010405f418e4 100644 --- a/docs/site/package.json +++ b/docs/site/package.json @@ -18,21 +18,21 @@ "fumadocs-mdx": "^14.3.1", "fumadocs-ui": "^16.8.4", "lucide-react": "^1.6.0", - "next": "16.3.4", - "react": "19.2.4", - "react-dom": "19.2.4", - "tailwind-merge": "^3.5.0", + "next": "16.3.6", + "react": "19.2.8", + "react-dom": "19.2.8", + "tailwind-merge": "^3.7.0", "tw-animate-css": "^1.4.0", "zod": "^4" }, "devDependencies": { "@tailwindcss/postcss": "^4", "@types/mdx": "^2.0.13", - "@types/node": "^20", - "@types/react": "^19", - "@types/react-dom": "^19", + "@types/node": "^22.20.4", + "@types/react": "~19.2.18", + "@types/react-dom": "~19.2.7", "eslint": "^9", - "eslint-config-next": "16.3.4", + "eslint-config-next": "16.3.6", "tailwindcss": "^4", "typescript": "^5", "vercel": "59.11.1" @@ -40,7 +40,7 @@ "engines": { "node": "22.x" }, - "packageManager": "pnpm@10.24.0", + "packageManager": "pnpm@10.34.6", "pnpm": { "onlyBuiltDependencies": [ "esbuild", @@ -50,13 +50,21 @@ "overrides": { "@vercel/fun>tar": "7.5.22", "@vercel/fun>@tootallnate/once": "2.0.1", - "@vercel/node>undici": "6.28.0", + "@vercel/node>undici": "6.28.1", "@vercel/python-analysis>js-yaml": "4.3.2", "@vercel/python-analysis>minimatch": "10.2.6", "vercel>smol-toml": "1.8.0", - "vercel>undici": "6.28.0", + "vercel>undici": "6.28.1", "@vercel/python-analysis>smol-toml": "1.8.0", - "@vercel/rust>smol-toml": "1.8.0" + "@vercel/rust>smol-toml": "1.8.0", + "@vercel/sandbox>undici": "7.29.1", + "@vercel/static-config>ajv": "8.18.0", + "@vercel/backends>path-to-regexp": "8.4.0", + "@vercel/fun>path-to-regexp": "8.4.0", + "brace-expansion@<2": "1.1.21", + "brace-expansion@>=4 <5.0.12": "5.0.12", + "@vercel/express>path-to-regexp": "8.4.0", + "@vercel/hono>path-to-regexp": "8.4.0" } } } diff --git a/docs/site/pnpm-lock.yaml b/docs/site/pnpm-lock.yaml index 0d496ad7c6c3..c69df84feaec 100644 --- a/docs/site/pnpm-lock.yaml +++ b/docs/site/pnpm-lock.yaml @@ -7,13 +7,21 @@ settings: overrides: '@vercel/fun>tar': 7.5.22 '@vercel/fun>@tootallnate/once': 2.0.1 - '@vercel/node>undici': 6.28.0 + '@vercel/node>undici': 6.28.1 '@vercel/python-analysis>js-yaml': 4.3.2 '@vercel/python-analysis>minimatch': 10.2.6 vercel>smol-toml: 1.8.0 - vercel>undici: 6.28.0 + vercel>undici: 6.28.1 '@vercel/python-analysis>smol-toml': 1.8.0 '@vercel/rust>smol-toml': 1.8.0 + '@vercel/sandbox>undici': 7.29.1 + '@vercel/static-config>ajv': 8.18.0 + '@vercel/backends>path-to-regexp': 8.4.0 + '@vercel/fun>path-to-regexp': 8.4.0 + brace-expansion@<2: 1.1.21 + brace-expansion@>=4 <5.0.12: 5.0.12 + '@vercel/express>path-to-regexp': 8.4.0 + '@vercel/hono>path-to-regexp': 8.4.0 importers: @@ -24,28 +32,28 @@ importers: version: 2.1.1 fumadocs-core: specifier: ^16.8.4 - version: 16.12.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.26.0(react@19.2.4))(next@16.3.4(@babel/core@7.29.7)(@types/node@20.19.43)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.4.3) + version: 16.12.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@1.26.0(react@19.2.8))(next@16.3.6(@babel/core@7.29.7)(@types/node@22.20.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3) fumadocs-mdx: specifier: ^14.3.1 - version: 14.3.2(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.17)(fumadocs-core@16.12.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.26.0(react@19.2.4))(next@16.3.4(@babel/core@7.29.7)(@types/node@20.19.43)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.4.3))(next@16.3.4(@babel/core@7.29.7)(@types/node@20.19.43)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) + version: 14.3.2(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.18)(fumadocs-core@16.12.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@1.26.0(react@19.2.8))(next@16.3.6(@babel/core@7.29.7)(@types/node@22.20.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3))(next@16.3.6(@babel/core@7.29.7)(@types/node@22.20.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8) fumadocs-ui: specifier: ^16.8.4 - version: 16.12.1(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.12.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.26.0(react@19.2.4))(next@16.3.4(@babel/core@7.29.7)(@types/node@20.19.43)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.4.3))(next@16.3.4(@babel/core@7.29.7)(@types/node@20.19.43)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tailwindcss@4.3.3) + version: 16.12.1(@types/mdx@2.0.14)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(fumadocs-core@16.12.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@1.26.0(react@19.2.8))(next@16.3.6(@babel/core@7.29.7)(@types/node@22.20.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3))(next@16.3.6(@babel/core@7.29.7)(@types/node@22.20.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tailwindcss@4.3.3) lucide-react: specifier: ^1.6.0 - version: 1.26.0(react@19.2.4) + version: 1.26.0(react@19.2.8) next: - specifier: 16.3.4 - version: 16.3.4(@babel/core@7.29.7)(@types/node@20.19.43)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 16.3.6 + version: 16.3.6(@babel/core@7.29.7)(@types/node@22.20.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) react: - specifier: 19.2.4 - version: 19.2.4 + specifier: 19.2.8 + version: 19.2.8 react-dom: - specifier: 19.2.4 - version: 19.2.4(react@19.2.4) + specifier: 19.2.8 + version: 19.2.8(react@19.2.8) tailwind-merge: - specifier: ^3.5.0 - version: 3.6.0 + specifier: ^3.7.0 + version: 3.7.0 tw-animate-css: specifier: ^1.4.0 version: 1.4.0 @@ -60,20 +68,20 @@ importers: specifier: ^2.0.13 version: 2.0.14 '@types/node': - specifier: ^20 - version: 20.19.43 + specifier: ^22.20.4 + version: 22.20.4 '@types/react': - specifier: ^19 - version: 19.2.17 + specifier: ~19.2.18 + version: 19.2.18 '@types/react-dom': - specifier: ^19 - version: 19.2.3(@types/react@19.2.17) + specifier: ~19.2.7 + version: 19.2.7(@types/react@19.2.18) eslint: specifier: ^9 version: 9.39.5(jiti@2.7.0) eslint-config-next: - specifier: 16.3.4 - version: 16.3.4(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + specifier: 16.3.6 + version: 16.3.6(@typescript-eslint/parser@8.71.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) tailwindcss: specifier: ^4 version: 4.3.3 @@ -102,8 +110,8 @@ packages: resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} engines: {node: '>=6.9.0'} - '@babel/generator@7.29.7': - resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} engines: {node: '>=6.9.0'} '@babel/helper-compilation-targets@7.29.7': @@ -140,8 +148,8 @@ packages: resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} engines: {node: '>=6.9.0'} - '@babel/parser@7.29.7': - resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + '@babel/parser@7.29.9': + resolution: {integrity: sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==} engines: {node: '>=6.0.0'} hasBin: true @@ -149,12 +157,12 @@ packages: resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.29.7': - resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + '@babel/traverse@7.29.8': + resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==} engines: {node: '>=6.9.0'} - '@babel/types@7.29.7': - resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} engines: {node: '>=6.9.0'} '@bytecodealliance/preview2-shim@0.17.6': @@ -605,144 +613,160 @@ packages: resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} - '@img/sharp-darwin-arm64@0.35.4': - resolution: {integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==} + '@img/sharp-darwin-arm64@0.35.5': + resolution: {integrity: sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.35.4': - resolution: {integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==} + '@img/sharp-darwin-x64@0.35.5': + resolution: {integrity: sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==} engines: {node: '>=20.9.0'} cpu: [x64] os: [darwin] - '@img/sharp-freebsd-wasm32@0.35.4': - resolution: {integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==} + '@img/sharp-freebsd-wasm32@0.35.5': + resolution: {integrity: sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==} engines: {node: '>=20.9.0'} os: [freebsd] - '@img/sharp-libvips-darwin-arm64@1.3.3': - resolution: {integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==} + '@img/sharp-libvips-darwin-arm64@1.3.4': + resolution: {integrity: sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.3.3': - resolution: {integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==} + '@img/sharp-libvips-darwin-x64@1.3.4': + resolution: {integrity: sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.3.3': - resolution: {integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==} + '@img/sharp-libvips-linux-arm64@1.3.4': + resolution: {integrity: sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==} cpu: [arm64] os: [linux] + libc: [glibc] - '@img/sharp-libvips-linux-arm@1.3.3': - resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==} + '@img/sharp-libvips-linux-arm@1.3.4': + resolution: {integrity: sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==} cpu: [arm] os: [linux] + libc: [glibc] - '@img/sharp-libvips-linux-ppc64@1.3.3': - resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==} + '@img/sharp-libvips-linux-ppc64@1.3.4': + resolution: {integrity: sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==} cpu: [ppc64] os: [linux] + libc: [glibc] - '@img/sharp-libvips-linux-riscv64@1.3.3': - resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==} + '@img/sharp-libvips-linux-riscv64@1.3.4': + resolution: {integrity: sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==} cpu: [riscv64] os: [linux] + libc: [glibc] - '@img/sharp-libvips-linux-s390x@1.3.3': - resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==} + '@img/sharp-libvips-linux-s390x@1.3.4': + resolution: {integrity: sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==} cpu: [s390x] os: [linux] + libc: [glibc] - '@img/sharp-libvips-linux-x64@1.3.3': - resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==} + '@img/sharp-libvips-linux-x64@1.3.4': + resolution: {integrity: sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==} cpu: [x64] os: [linux] + libc: [glibc] - '@img/sharp-libvips-linuxmusl-arm64@1.3.3': - resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==} + '@img/sharp-libvips-linuxmusl-arm64@1.3.4': + resolution: {integrity: sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==} cpu: [arm64] os: [linux] + libc: [musl] - '@img/sharp-libvips-linuxmusl-x64@1.3.3': - resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==} + '@img/sharp-libvips-linuxmusl-x64@1.3.4': + resolution: {integrity: sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==} cpu: [x64] os: [linux] + libc: [musl] - '@img/sharp-linux-arm64@0.35.4': - resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==} + '@img/sharp-linux-arm64@0.35.5': + resolution: {integrity: sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] + libc: [glibc] - '@img/sharp-linux-arm@0.35.4': - resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==} + '@img/sharp-linux-arm@0.35.5': + resolution: {integrity: sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==} engines: {node: '>=20.9.0'} cpu: [arm] os: [linux] + libc: [glibc] - '@img/sharp-linux-ppc64@0.35.4': - resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==} + '@img/sharp-linux-ppc64@0.35.5': + resolution: {integrity: sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==} engines: {node: '>=20.9.0'} cpu: [ppc64] os: [linux] + libc: [glibc] - '@img/sharp-linux-riscv64@0.35.4': - resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==} + '@img/sharp-linux-riscv64@0.35.5': + resolution: {integrity: sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==} engines: {node: '>=20.9.0'} cpu: [riscv64] os: [linux] + libc: [glibc] - '@img/sharp-linux-s390x@0.35.4': - resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==} + '@img/sharp-linux-s390x@0.35.5': + resolution: {integrity: sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==} engines: {node: '>=20.9.0'} cpu: [s390x] os: [linux] + libc: [glibc] - '@img/sharp-linux-x64@0.35.4': - resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==} + '@img/sharp-linux-x64@0.35.5': + resolution: {integrity: sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] + libc: [glibc] - '@img/sharp-linuxmusl-arm64@0.35.4': - resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==} + '@img/sharp-linuxmusl-arm64@0.35.5': + resolution: {integrity: sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] + libc: [musl] - '@img/sharp-linuxmusl-x64@0.35.4': - resolution: {integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==} + '@img/sharp-linuxmusl-x64@0.35.5': + resolution: {integrity: sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] + libc: [musl] - '@img/sharp-wasm32@0.35.4': - resolution: {integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==} + '@img/sharp-wasm32@0.35.5': + resolution: {integrity: sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==} engines: {node: '>=20.9.0'} - '@img/sharp-webcontainers-wasm32@0.35.4': - resolution: {integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==} + '@img/sharp-webcontainers-wasm32@0.35.5': + resolution: {integrity: sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==} engines: {node: '>=20.9.0'} cpu: [wasm32] - '@img/sharp-win32-arm64@0.35.4': - resolution: {integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==} + '@img/sharp-win32-arm64@0.35.5': + resolution: {integrity: sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.35.4': - resolution: {integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==} + '@img/sharp-win32-ia32@0.35.5': + resolution: {integrity: sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==} engines: {node: ^20.9.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.35.4': - resolution: {integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==} + '@img/sharp-win32-x64@0.35.5': + resolution: {integrity: sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==} engines: {node: '>=20.9.0'} cpu: [x64] os: [win32] @@ -804,30 +828,35 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@napi-rs/keyring-linux-arm64-musl@1.2.0': resolution: {integrity: sha512-8TDymrpC4P1a9iDEaegT7RnrkmrJN5eNZh3Im3UEV5PPYGtrb82CRxsuFohthCWQW81O483u1bu+25+XA4nKUw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@napi-rs/keyring-linux-riscv64-gnu@1.2.0': resolution: {integrity: sha512-awsB5XI1MYL7fwfjMDGmKOWvNgJEO7mM7iVEMS0fO39f0kVJnOSjlu7RHcXAF0LOx+0VfF3oxbWqJmZbvRCRHw==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] + libc: [glibc] '@napi-rs/keyring-linux-x64-gnu@1.2.0': resolution: {integrity: sha512-8E+7z4tbxSJXxIBqA+vfB1CGajpCDRyTyqXkBig5NtASrv4YXcntSo96Iah2QDR5zD3dSTsmbqJudcj9rKKuHQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@napi-rs/keyring-linux-x64-musl@1.2.0': resolution: {integrity: sha512-8RZ8yVEnmWr/3BxKgBSzmgntI7lNEsY7xouNfOsQkuVAiCNmxzJwETspzK3PQ2FHtDxgz5vHQDEBVGMyM4hUHA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@napi-rs/keyring-win32-arm64-msvc@1.2.0': resolution: {integrity: sha512-AoqaDZpQ6KPE19VBLpxyORcp+yWmHI9Xs9Oo0PJ4mfHma4nFSLVdhAubJCxdlNptHe5va7ghGCHj3L9Akiv4cQ==} @@ -857,56 +886,67 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 - '@next/env@16.3.4': - resolution: {integrity: sha512-cjWZnUUa6jZq2kFaNe/ZyJdZonOZ/QoN0Zka2nz/FLOrfx14pQuM9c5RaSVkWMqgdt4ksgPAMWPyHSs/CyV48Q==} + '@napi-rs/wasm-runtime@1.2.4': + resolution: {integrity: sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + peerDependencies: + '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 + '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 + + '@next/env@16.3.6': + resolution: {integrity: sha512-x9Vblze1EbtltQYnNH38xCPWU3TVfBd1eXqA3+w9+BTpedkkdNpAaltXlGQ/nsc1+E0mVTNrtcbX3GoO09zeLQ==} - '@next/eslint-plugin-next@16.3.4': - resolution: {integrity: sha512-szW9y2Aumu4z88YXfTzcFsgUAg2k64uzbtcO5L9f1AKS4w/GUKJcbFllRflROVyNPgJtGOnvNxiyp3v6b+prIA==} + '@next/eslint-plugin-next@16.3.6': + resolution: {integrity: sha512-jowwDX+7DOlDIjJLgTMxudw+k37QnWu1JkZLkSi9MaJBfDYcfhAPMKBhXL0idYzFN/AGg//axnOR4cLkHX/Rng==} - '@next/swc-darwin-arm64@16.3.4': - resolution: {integrity: sha512-iBr3I5LZNk5/bgl5//iTgD2tcym14MX0Xo7fD//u9dYAEgGzza1y9oywluPtf74YnOswVdH1908aK9xVz7zQTw==} + '@next/swc-darwin-arm64@16.3.6': + resolution: {integrity: sha512-E/7GEqaUkt8mk/T8v9lAnrhzR06kdq1ZBkC12F8tAMkdIadwNp3H1KqHynDHrpcTlGCUdq/qu6vUL2aYVyYBdw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.3.4': - resolution: {integrity: sha512-2dpiSyl2Jw/NrBPaU2MAKGSa+2MR82pJIn4Sm5Rjr+gxAeuh0z158Su3Z2O8zn7UNNq+ej4bToed6RcRN/Lydg==} + '@next/swc-darwin-x64@16.3.6': + resolution: {integrity: sha512-yBE893/nDWTlaiBD1p+qgt7NUen4U5R6FXyH0s67Npq1S3E0cVSef1WIXC2xBRgQvwAvJq6DnS6Y6PrY0cy4Ew==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.3.4': - resolution: {integrity: sha512-+t+U8HZT+fApePCS5h89CSH3datz29MkzyfCn+6fpsZBG/oiEOhINcb9rtkv6sdpToLGFn2e6146NzaKCXkqrA==} + '@next/swc-linux-arm64-gnu@16.3.6': + resolution: {integrity: sha512-KJDpjBqBPYlvkivmyrp+Qys6k/7ksbqGQvRVc6ZEGfR+cjQxx+nUkJaWmNZJsmoOrqYNbaXByF8wa0lBwDhB3Q==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] - '@next/swc-linux-arm64-musl@16.3.4': - resolution: {integrity: sha512-mx03GNs1ocQA5JQ4FxDMmIsNkdrZh8cuezKCrId28e5/gIPU/l7Kcy2+vmCCzdjnnmXJy+iOAu+7K0QppO6Urg==} + '@next/swc-linux-arm64-musl@16.3.6': + resolution: {integrity: sha512-mqNg2K+hvWskSRb/QM+Ix412DvBsuSF0XV+frTSw5vmoucNnIlynFwKYew8D01bfATErMOM7Bujrf0BA5DRKFA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] - '@next/swc-linux-x64-gnu@16.3.4': - resolution: {integrity: sha512-YIhGY6fSMfha52bnVxnzc9zaVBzJg+cqQTOD8tXIBSx4fuv0pVMxQTE0PaS59YhnMOiYiG09IMwxJAf/CFm/Dw==} + '@next/swc-linux-x64-gnu@16.3.6': + resolution: {integrity: sha512-nFncBNGAYouRHjRVaITs9beZRfhX4ssVwpnvPIAbkZVH6LtGoAVlH4bJ8Cnf9SOo9bsXgPFer/GdHtEE3JNOkw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] - '@next/swc-linux-x64-musl@16.3.4': - resolution: {integrity: sha512-+eaaX6axpDb0yF1GCpiERe6njplvdC+nks/fKfcHu3XPGRrald8P3/X7yv7QLdjA51knnxwl9pxdIJsg+w1L+Q==} + '@next/swc-linux-x64-musl@16.3.6': + resolution: {integrity: sha512-5Mf3cHDGR/Iz0ng2Bj3zUR3p5QS9YK3Hn2QiAfavFmyF48zwThAjpFoiTKNIcOHLYS4zEk+gzyJ/9deQ2ZB8yQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] - '@next/swc-win32-arm64-msvc@16.3.4': - resolution: {integrity: sha512-0jcXW7Xs/uzICrmgV3MhDYDeRy++1CqnpDIerlPIqYO4bhzB4WNbX/aRnQclustsAyTkFKB0z6rbcjmNg5tR8A==} + '@next/swc-win32-arm64-msvc@16.3.6': + resolution: {integrity: sha512-0jkJy0C2kbrJWTk4YLa3xk80pVBpx8FCHJym7CnUfDAXe/FWv5qT7SQJbR0KuemyxaEDlEx5WT4VQJoTW+/9Qw==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.3.4': - resolution: {integrity: sha512-vvBzwu1pYQCp92maZCFCIw/XgOTMR5tur9GjakwIo2cmwRTMKajRZZDS9+e4KsUZWKu1E007WUeAFXRRjZeuzw==} + '@next/swc-win32-x64-msvc@16.3.6': + resolution: {integrity: sha512-/YXjI1e5OXcZ7YpxRwgP/1jAV/SBKTzeVKqN2mk7mLpcICsyn3Gl5+dIfDTJp70M0ccMhyMMRso4v6mPDCGepg==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -978,48 +1018,56 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@oxc-parser/binding-linux-arm64-musl@0.121.0': resolution: {integrity: sha512-qT663J/W8yQFw3dtscbEi9LKJevr20V7uWs2MPGTnvNZ3rm8anhhE16gXGpxDOHeg9raySaSHKhd4IGa3YZvuw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@oxc-parser/binding-linux-ppc64-gnu@0.121.0': resolution: {integrity: sha512-mYNe4NhVvDBbPkAP8JaVS8lC1dsoJZWH5WCjpw5E+sjhk1R08wt3NnXYUzum7tIiWPfgQxbCMcoxgeemFASbRw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@oxc-parser/binding-linux-riscv64-gnu@0.121.0': resolution: {integrity: sha512-+QiFoGxhAbaI/amqX567784cDyyuZIpinBrJNxUzb+/L2aBRX67mN6Jv40pqduHf15yYByI+K5gUEygCuv0z9w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@oxc-parser/binding-linux-riscv64-musl@0.121.0': resolution: {integrity: sha512-9ykEgyTa5JD/Uhv2sttbKnCfl2PieUfOjyxJC/oDL2UO0qtXOtjPLl7H8Kaj5G7p3hIvFgu3YWvAxvE0sqY+hQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [musl] '@oxc-parser/binding-linux-s390x-gnu@0.121.0': resolution: {integrity: sha512-DB1EW5VHZdc1lIRjOI3bW/wV6R6y0xlfvdVrqj6kKi7Ayu2U3UqUBdq9KviVkcUGd5Oq+dROqvUEEFRXGAM7EQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@oxc-parser/binding-linux-x64-gnu@0.121.0': resolution: {integrity: sha512-s4lfobX9p4kPTclvMiH3gcQUd88VlnkMTF6n2MTMDAyX5FPNRhhRSFZK05Ykhf8Zy5NibV4PbGR6DnK7FGNN6A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@oxc-parser/binding-linux-x64-musl@0.121.0': resolution: {integrity: sha512-P9KlyTpuBuMi3NRGpJO8MicuGZfOoqZVRP1WjOecwx8yk4L/+mrCRNc5egSi0byhuReblBF2oVoDSMgV9Bj4Hw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@oxc-parser/binding-openharmony-arm64@0.121.0': resolution: {integrity: sha512-R+4jrWOfF2OAPPhj3Eb3U5CaKNAH9/btMveMULIrcNW/hjfysFQlF8wE0GaVBr81dWz8JLgQlsxwctoL78JwXw==} @@ -1103,48 +1151,56 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@oxc-transform/binding-linux-arm64-musl@0.111.0': resolution: {integrity: sha512-ARyfcMCIxVLDgLf6FQ8Oo1/TFySpnquV+vuSb4SFQZfYDqgMklzwv0NYXxWD0aB6enElyMDs6pQJBzusEKCkOg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@oxc-transform/binding-linux-ppc64-gnu@0.111.0': resolution: {integrity: sha512-PKpVRrSvBNK3tv9vwxn7Fay+QWZmprPGlEqJcseBJllQc5mFMD4Q/w44chu5iR9ZLsDeSHzmNWrgMLo4J0sP2A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@oxc-transform/binding-linux-riscv64-gnu@0.111.0': resolution: {integrity: sha512-9bUml6rMgk+8GF5rvNMweFspkzSiCjqpV6HduwiUyexqfGKrmjq9IZOxxvnzkE2RGdQzP507NNDoVNYIoGQYuA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@oxc-transform/binding-linux-riscv64-musl@0.111.0': resolution: {integrity: sha512-tzGCohGxaeH6KRJjfYZd4mHCoGjCai6N+zZi1Oj+tSDMAAdyvs1dRzYb8PNUGnybCg3Te4M0jLPzWZaSmnKraQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [musl] '@oxc-transform/binding-linux-s390x-gnu@0.111.0': resolution: {integrity: sha512-sRG1KIfZ0ML9ToEygm5aM/5GJeBA05uHlgW3M0Rx/DNWMJhuahLmqWuB02aWSmijndLfEKXLLXIWhvWupRG8lg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@oxc-transform/binding-linux-x64-gnu@0.111.0': resolution: {integrity: sha512-T0Kmvk+OdlUdABdXlDIf3MQReMzFfC75NEI9x8jxy5pKooACEFg0k0V8gyR3gq4DzbDCfucqFQDWNvSgIopAbQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@oxc-transform/binding-linux-x64-musl@0.111.0': resolution: {integrity: sha512-EgoutsP3YfqzN8a9vpc9+XLr0bmBl0dA3uOMiP77+exATCPxJBkJErGmQkqk6RtTp5XqX6q6mB45qWQyKk6+pA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@oxc-transform/binding-openharmony-arm64@0.111.0': resolution: {integrity: sha512-d8J+ejc0j5WODbVwR/QxFaI65YMwvG0W53vcVCHwa6ja1QI5lpe7sislrefG2EFYgnY47voMRzlXab5d4gEcDw==} @@ -1570,24 +1626,28 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.0.0-rc.1': resolution: {integrity: sha512-UvApLEGholmxw/HIwmUnLq3CwdydbhaHHllvWiCTNbyGom7wTwOtz5OAQbAKZYyiEOeIXZNPkM7nA4Dtng7CLw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@rolldown/binding-linux-x64-gnu@1.0.0-rc.1': resolution: {integrity: sha512-uVctNgZHiGnJx5Fij7wHLhgw4uyZBVi6mykeWKOqE7bVy9Hcxn0fM/IuqdMwk6hXlaf9fFShDTFz2+YejP+x0A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-musl@1.0.0-rc.1': resolution: {integrity: sha512-T6Eg0xWwcxd/MzBcuv4Z37YVbUbJxy5cMNnbIt/Yr99wFwli30O4BPlY8hKeGyn6lWNtU0QioBS46lVzDN38bg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@rolldown/binding-openharmony-arm64@1.0.0-rc.1': resolution: {integrity: sha512-PuGZVS2xNJyLADeh2F04b+Cz4NwvpglbtWACgrDOa5YDTEHKwmiTDjoD5eZ9/ptXtcpeFrMqD2H4Zn33KAh1Eg==} @@ -1705,24 +1765,28 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.3.3': resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.3.3': resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.3.3': resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.3.3': resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} @@ -1765,6 +1829,9 @@ packages: '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + '@tybys/wasm-util@0.10.4': + resolution: {integrity: sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==} + '@types/debug@4.1.13': resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} @@ -1795,16 +1862,16 @@ packages: '@types/node@20.11.0': resolution: {integrity: sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==} - '@types/node@20.19.43': - resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==} + '@types/node@22.20.4': + resolution: {integrity: sha512-zJRE40jpHtKqE/C4fgHrAKQLJuSpzEnP9ff9Y7YtoR3Wd2pwqzlekDeEuUQXjRd+QCYnVnNwuJYmhdk9XV8gvA==} - '@types/react-dom@19.2.3': - resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + '@types/react-dom@19.2.7': + resolution: {integrity: sha512-I8bPpDLcHBv1qiIiXDCy71Rt8eQDKJP0sMSWJphDdAcdqiJ1sGpZamavoEIRZmYzjia9LuEb2HlYdDpmoENpvQ==} peerDependencies: '@types/react': ^19.2.0 - '@types/react@19.2.17': - resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + '@types/react@19.2.18': + resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -1812,63 +1879,63 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - '@typescript-eslint/eslint-plugin@8.65.0': - resolution: {integrity: sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==} + '@typescript-eslint/eslint-plugin@8.71.0': + resolution: {integrity: sha512-pqcS9c1HxZTHt7End4nXqd0s5lJrrFzrgCkKFJrsbUnaL6M3+6oBFZaslg6Gjsl3argl2DDRFROnXARaZ2e4Nw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.65.0 + '@typescript-eslint/parser': ^8.71.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.65.0': - resolution: {integrity: sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==} + '@typescript-eslint/parser@8.71.0': + resolution: {integrity: sha512-CG4nPk1f2zc8yw4pALqHsFYH2hdo+h1T9daSp21+Hnxi9LOE3GT9hAfTKJCBXVNM2GmYs1eMEP615wPoeOgk3A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.65.0': - resolution: {integrity: sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==} + '@typescript-eslint/project-service@8.71.0': + resolution: {integrity: sha512-aABjw5rjBacYONVPaPiWOCjJu0vEF4a25iQuodlmQYL1trtLZ0X/y+2Vzl3BKI1odM4LnwLE1oUDXYp1wzx1TQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.65.0': - resolution: {integrity: sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==} + '@typescript-eslint/scope-manager@8.71.0': + resolution: {integrity: sha512-gWF0BhUcnjZxSpLE8ngS/59n2SB0J3YqRxvX1+2aoRJk9hNtHSLOV+TcarFiOr5ipXm3yc1QrI4c9YZc8zyCxw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.65.0': - resolution: {integrity: sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==} + '@typescript-eslint/tsconfig-utils@8.71.0': + resolution: {integrity: sha512-Z1UlWHADEK2Mlb9NpWfDeSjqoZ5EyrOv4R3eQpbkzqn/EwaIdOpXXupEA1+0ZIOSJSZZDBHG0BrQyN8zUG6Pwg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.65.0': - resolution: {integrity: sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==} + '@typescript-eslint/type-utils@8.71.0': + resolution: {integrity: sha512-i8uO1qbdxeKgRnS5sCRt6On3/nfo2d2DwQe3Yvjx543zLy7r8ySqRuPPiIIXAhS03U0v5NfAFx+rUgxFzKKwNw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.65.0': - resolution: {integrity: sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==} + '@typescript-eslint/types@8.71.0': + resolution: {integrity: sha512-cJ4OoxPGWvFnBTnSZyaU+qJzGTqPTGJY+gDchj6cRyLRdmIdt4rcsE4twj+zPfrNiWuVi38wijHzShL++Z9atQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.65.0': - resolution: {integrity: sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==} + '@typescript-eslint/typescript-estree@8.71.0': + resolution: {integrity: sha512-PEEF4G5sLLWAS5BpPrUvms4ySZkiBQQZM4z+3ReI46axK5Vqr/vXBQatJQIZZOYdGyPUAKTtsrWzpqKuU+3DEw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.65.0': - resolution: {integrity: sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==} + '@typescript-eslint/utils@8.71.0': + resolution: {integrity: sha512-pKR/tEMVrXZG23UFKUn5BQf3zfmfk7KQceI2cGzywZ5nxM5Eu3hEJU1utjWzydtzBbcJAQhHN8iPCxobHpPcZQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.65.0': - resolution: {integrity: sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==} + '@typescript-eslint/visitor-keys@8.71.0': + resolution: {integrity: sha512-8eQ9R218XORK+KLosnf4bu/QsUXvUyVwTbArg7/0NMB1Pu87OJKvj4nhFblkYE8gQV73mW1dx1ptlPCkwRGa7A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ungap/structured-clone@1.3.3': @@ -1913,51 +1980,61 @@ packages: resolution: {integrity: sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==} cpu: [arm64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.12.2': resolution: {integrity: sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==} cpu: [arm64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': resolution: {integrity: sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==} cpu: [loong64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-loong64-musl@1.12.2': resolution: {integrity: sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==} cpu: [loong64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': resolution: {integrity: sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==} cpu: [ppc64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': resolution: {integrity: sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==} cpu: [riscv64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': resolution: {integrity: sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==} cpu: [riscv64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': resolution: {integrity: sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==} cpu: [s390x] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.12.2': resolution: {integrity: sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==} cpu: [x64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.12.2': resolution: {integrity: sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==} cpu: [x64] os: [linux] + libc: [musl] '@unrs/resolver-binding-openharmony-arm64@1.12.2': resolution: {integrity: sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==} @@ -2191,8 +2268,8 @@ packages: ajv@6.15.0: resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} - ajv@8.6.3: - resolution: {integrity: sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==} + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} @@ -2219,8 +2296,8 @@ packages: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} - array-includes@3.1.9: - resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + array-includes@3.2.0: + resolution: {integrity: sha512-VXY5eFRarnXcYxwBjJzPmEhH55+rmP79/+ueDhi0F+TuqfHCItagIHqxeUZrmgrOPa31QTh9H85DjX3FfJ0FTg==} engines: {node: '>= 0.4'} array.prototype.findlast@1.2.5: @@ -2279,8 +2356,8 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - axe-core@4.12.1: - resolution: {integrity: sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==} + axe-core@4.13.0: + resolution: {integrity: sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==} engines: {node: '>=4'} axobject-query@4.1.0: @@ -2313,27 +2390,27 @@ packages: bare-abort-controller: optional: true - baseline-browser-mapping@2.11.1: - resolution: {integrity: sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A==} + baseline-browser-mapping@2.11.26: + resolution: {integrity: sha512-GLQdD3y6UF8iVuMJl5fHgE4jdn/ua7n+toKfLgNlg3BqQtOZjpy68T8Tup8/wGWZCDlm7KMg7tPb4MPn7oN0TQ==} engines: {node: '>=6.0.0'} hasBin: true bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} - brace-expansion@1.1.18: - resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==} + brace-expansion@1.1.21: + resolution: {integrity: sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==} - brace-expansion@5.0.9: - resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + brace-expansion@5.0.12: + resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} engines: {node: 20 || >=22} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.28.7: - resolution: {integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==} + browserslist@4.29.2: + resolution: {integrity: sha512-/u9r8k8ue4ZhHCw9ovDtltpWdXeXzjhdxCGj6vm1RGLPXb8lu33EMdoCHF5Sx8oYBS+Q/FYYNmAqlHncxR3RmA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -2360,8 +2437,8 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - caniuse-lite@1.0.30001806: - resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==} + caniuse-lite@1.0.30001813: + resolution: {integrity: sha512-zfjJo4rM0+fUomGDBW/xcDjhIwz/210DGvip2MAMDZ8KHcRPnOHmEgHPZP0UHlZoxr8fYKVJOqcORhYQcG4FKQ==} ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -2545,8 +2622,8 @@ packages: engines: {node: '>=16'} hasBin: true - electron-to-chromium@1.5.396: - resolution: {integrity: sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==} + electron-to-chromium@1.5.440: + resolution: {integrity: sha512-SghDzqdJokdz8zP8YNlvS74+CwLRoUPDGvP/gFA+HrKVw+pOshUAgx8pXR37xl/u16zGae746rFNWvZ/22kU2Q==} emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} @@ -2635,8 +2712,8 @@ packages: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} - eslint-config-next@16.3.4: - resolution: {integrity: sha512-35/8RM10huEL9vlr8hUZMERMENHBrnyHN3ZZkF9efSgzGaqK34jIqry44A956//zriUhUAUW0XSkcolhrryqAA==} + eslint-config-next@16.3.6: + resolution: {integrity: sha512-1Upt3U7BDwU+ilpe2byZjAfts9oNq4d4fv/zXEvs8/4yS+cwOQW/WCxUNy8gCDquX67SzeehDvKblVC6ZBMocQ==} peerDependencies: eslint: '>=9.0.0' typescript: '>=3.3.1' @@ -2823,6 +2900,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-uri@3.1.8: + resolution: {integrity: sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -3051,6 +3131,9 @@ packages: get-tsconfig@4.14.0: resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + get-tsconfig@4.14.3: + resolution: {integrity: sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==} + github-slugger@2.0.0: resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} @@ -3172,8 +3255,8 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - ignore@7.0.6: - resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==} + ignore@7.0.10: + resolution: {integrity: sha512-HpbUakT7xp5miBUywCHf36ZEuAJNklBJDDsGpUIjMzOSmM8ELSfA9Sa/QDPeNeqeoN31u+UTCkL4klCOVvRm4Q==} engines: {node: '>= 4'} import-fresh@3.3.1: @@ -3227,8 +3310,8 @@ packages: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} - is-core-module@2.16.2: - resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + is-core-module@2.17.0: + resolution: {integrity: sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==} engines: {node: '>= 0.4'} is-data-view@1.0.2: @@ -3455,24 +3538,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -3783,6 +3870,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@3.3.19: + resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + napi-postinstall@0.3.4: resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -3797,8 +3889,8 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next@16.3.4: - resolution: {integrity: sha512-/Ztf6CeRH+ejEXUrYtqI4gkS66eFIHuSwqi60RgcpWKodxFZx2/dqVCMKBwILfAHXQ+F1b1vAudgj3mnxqtoIA==} + next@16.3.6: + resolution: {integrity: sha512-L+otWM/aQbYTx98aZhgEoMb4bZAXx1YVW4UMA/vuCyCoWG5HJyZUili8QAkqzrcC+5///tsz3s0M+SlyB5bLMw==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -3853,8 +3945,8 @@ packages: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true - node-releases@2.0.51: - resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} + node-releases@2.0.57: + resolution: {integrity: sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==} engines: {node: '>=18'} nopt@8.1.0: @@ -3992,12 +4084,8 @@ packages: path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} - path-to-regexp@8.2.0: - resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} - engines: {node: '>=16'} - - path-to-regexp@8.3.0: - resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + path-to-regexp@8.4.0: + resolution: {integrity: sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==} pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} @@ -4059,10 +4147,10 @@ packages: resolution: {integrity: sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==} engines: {node: '>= 0.8'} - react-dom@19.2.4: - resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} + react-dom@19.2.8: + resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==} peerDependencies: - react: ^19.2.4 + react: ^19.2.8 react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -4097,8 +4185,8 @@ packages: '@types/react': optional: true - react@19.2.4: - resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} + react@19.2.8: + resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} engines: {node: '>=0.10.0'} readdirp@4.1.2: @@ -4258,8 +4346,8 @@ packages: setprototypeof@1.1.1: resolution: {integrity: sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==} - sharp@0.35.4: - resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==} + sharp@0.35.5: + resolution: {integrity: sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==} engines: {node: '>=20.9.0'} peerDependencies: '@types/node': '*' @@ -4350,8 +4438,8 @@ packages: resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} engines: {node: '>= 0.4'} - string.prototype.matchall@4.0.12: - resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + string.prototype.matchall@4.1.0: + resolution: {integrity: sha512-tHNHTxInrYLCga9O9YGxWA3G9/nnzQw8UGAyqGx3Ar1pSTTzIuM4woFSq4SowkXCjJIwq5sIiQvEfRI9tCH1qQ==} engines: {node: '>= 0.4'} string.prototype.repeat@1.0.0: @@ -4411,8 +4499,8 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - tailwind-merge@3.6.0: - resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} + tailwind-merge@3.7.0: + resolution: {integrity: sha512-XPPUyAc+cvspz3lHTcR/QgPfW2A0lv/xQNIjX3HGhLR+Nq2lHaLq5MtTesHn8GUr3W3DguT2KT5x3NVgRtYwmA==} tailwindcss@4.3.3: resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} @@ -4509,16 +4597,16 @@ packages: resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} engines: {node: '>= 0.4'} - typed-array-byte-offset@1.0.4: - resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + typed-array-byte-offset@1.0.5: + resolution: {integrity: sha512-0FHJvLPqZ7KJzp17O13jfsAjsqazgrxBu2zEK95PmUz8lv2+GjRuxUInCr2Rk9Dms3ihN21zJ929ZO43yJ95QQ==} engines: {node: '>= 0.4'} typed-array-length@1.0.8: resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==} engines: {node: '>= 0.4'} - typescript-eslint@8.65.0: - resolution: {integrity: sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==} + typescript-eslint@8.71.0: + resolution: {integrity: sha512-fBdHYiqQ14RW6mOMXD14Svn82ZsCYAoQSzGRzyEjR59S5A2Krh/l7fGTOQ7iCr8gGy/mHVXtEF7s5fgjEdV0Pw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -4542,12 +4630,12 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici@6.28.0: - resolution: {integrity: sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==} + undici@6.28.1: + resolution: {integrity: sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA==} engines: {node: '>=18.17'} - undici@7.29.0: - resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} + undici@7.29.1: + resolution: {integrity: sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==} engines: {node: '>=20.18.1'} unified@11.0.5: @@ -4585,8 +4673,8 @@ packages: unrs-resolver@1.12.2: resolution: {integrity: sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==} - update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + update-browserslist-db@1.3.3: + resolution: {integrity: sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -4656,8 +4744,8 @@ packages: resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} engines: {node: '>= 0.4'} - which-typed-array@1.1.22: - resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} + which-typed-array@1.1.24: + resolution: {integrity: sha512-wk4Mf4pR5mRP7eYuuTBCIQ9d0ud2Fv2jRLQpfgnRjbOxAFHmjKFValgTpitVKzJJS8ajnYQV2Du1SZ8j6b/EUQ==} engines: {node: '>= 0.4'} which@2.0.2: @@ -4755,14 +4843,14 @@ snapshots: '@babel/core@7.29.7': dependencies: '@babel/code-frame': 7.29.7 - '@babel/generator': 7.29.7 + '@babel/generator': 7.29.8 '@babel/helper-compilation-targets': 7.29.7 '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) '@babel/helpers': 7.29.7 - '@babel/parser': 7.29.7 + '@babel/parser': 7.29.9 '@babel/template': 7.29.7 - '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 @@ -4772,10 +4860,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.29.7': + '@babel/generator@7.29.8': dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 @@ -4784,7 +4872,7 @@ snapshots: dependencies: '@babel/compat-data': 7.29.7 '@babel/helper-validator-option': 7.29.7 - browserslist: 4.28.7 + browserslist: 4.29.2 lru-cache: 5.1.1 semver: 6.3.1 @@ -4792,8 +4880,8 @@ snapshots: '@babel/helper-module-imports@7.29.7': dependencies: - '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color @@ -4802,7 +4890,7 @@ snapshots: '@babel/core': 7.29.7 '@babel/helper-module-imports': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.8 transitivePeerDependencies: - supports-color @@ -4815,31 +4903,31 @@ snapshots: '@babel/helpers@7.29.7': dependencies: '@babel/template': 7.29.7 - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 - '@babel/parser@7.29.7': + '@babel/parser@7.29.9': dependencies: - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 '@babel/template@7.29.7': dependencies: '@babel/code-frame': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 - '@babel/traverse@7.29.7': + '@babel/traverse@7.29.8': dependencies: '@babel/code-frame': 7.29.7 - '@babel/generator': 7.29.7 + '@babel/generator': 7.29.8 '@babel/helper-globals': 7.29.7 - '@babel/parser': 7.29.7 + '@babel/parser': 7.29.9 '@babel/template': 7.29.7 - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 debug: 4.4.3 transitivePeerDependencies: - supports-color - '@babel/types@7.29.7': + '@babel/types@7.29.8': dependencies: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 @@ -5095,20 +5183,20 @@ snapshots: '@floating-ui/core': 1.8.0 '@floating-ui/utils': 0.2.12 - '@floating-ui/react-dom@2.1.9(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@floating-ui/react-dom@2.1.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@floating-ui/dom': 1.8.0 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) '@floating-ui/utils@0.2.12': {} - '@fuma-translate/react@1.0.2(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@fuma-translate/react@1.0.2(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 '@fumadocs/tailwind@0.1.1(tailwindcss@4.3.3)': optionalDependencies: @@ -5133,108 +5221,108 @@ snapshots: '@img/colour@1.1.0': optional: true - '@img/sharp-darwin-arm64@0.35.4': + '@img/sharp-darwin-arm64@0.35.5': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.3.3 + '@img/sharp-libvips-darwin-arm64': 1.3.4 optional: true - '@img/sharp-darwin-x64@0.35.4': + '@img/sharp-darwin-x64@0.35.5': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.3.3 + '@img/sharp-libvips-darwin-x64': 1.3.4 optional: true - '@img/sharp-freebsd-wasm32@0.35.4': + '@img/sharp-freebsd-wasm32@0.35.5': dependencies: - '@img/sharp-wasm32': 0.35.4 + '@img/sharp-wasm32': 0.35.5 optional: true - '@img/sharp-libvips-darwin-arm64@1.3.3': + '@img/sharp-libvips-darwin-arm64@1.3.4': optional: true - '@img/sharp-libvips-darwin-x64@1.3.3': + '@img/sharp-libvips-darwin-x64@1.3.4': optional: true - '@img/sharp-libvips-linux-arm64@1.3.3': + '@img/sharp-libvips-linux-arm64@1.3.4': optional: true - '@img/sharp-libvips-linux-arm@1.3.3': + '@img/sharp-libvips-linux-arm@1.3.4': optional: true - '@img/sharp-libvips-linux-ppc64@1.3.3': + '@img/sharp-libvips-linux-ppc64@1.3.4': optional: true - '@img/sharp-libvips-linux-riscv64@1.3.3': + '@img/sharp-libvips-linux-riscv64@1.3.4': optional: true - '@img/sharp-libvips-linux-s390x@1.3.3': + '@img/sharp-libvips-linux-s390x@1.3.4': optional: true - '@img/sharp-libvips-linux-x64@1.3.3': + '@img/sharp-libvips-linux-x64@1.3.4': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.3.3': + '@img/sharp-libvips-linuxmusl-arm64@1.3.4': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.3.3': + '@img/sharp-libvips-linuxmusl-x64@1.3.4': optional: true - '@img/sharp-linux-arm64@0.35.4': + '@img/sharp-linux-arm64@0.35.5': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.3.3 + '@img/sharp-libvips-linux-arm64': 1.3.4 optional: true - '@img/sharp-linux-arm@0.35.4': + '@img/sharp-linux-arm@0.35.5': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.3.3 + '@img/sharp-libvips-linux-arm': 1.3.4 optional: true - '@img/sharp-linux-ppc64@0.35.4': + '@img/sharp-linux-ppc64@0.35.5': optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.3.3 + '@img/sharp-libvips-linux-ppc64': 1.3.4 optional: true - '@img/sharp-linux-riscv64@0.35.4': + '@img/sharp-linux-riscv64@0.35.5': optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.3.3 + '@img/sharp-libvips-linux-riscv64': 1.3.4 optional: true - '@img/sharp-linux-s390x@0.35.4': + '@img/sharp-linux-s390x@0.35.5': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.3.3 + '@img/sharp-libvips-linux-s390x': 1.3.4 optional: true - '@img/sharp-linux-x64@0.35.4': + '@img/sharp-linux-x64@0.35.5': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.3.3 + '@img/sharp-libvips-linux-x64': 1.3.4 optional: true - '@img/sharp-linuxmusl-arm64@0.35.4': + '@img/sharp-linuxmusl-arm64@0.35.5': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.4 optional: true - '@img/sharp-linuxmusl-x64@0.35.4': + '@img/sharp-linuxmusl-x64@0.35.5': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.3.3 + '@img/sharp-libvips-linuxmusl-x64': 1.3.4 optional: true - '@img/sharp-wasm32@0.35.4': + '@img/sharp-wasm32@0.35.5': dependencies: '@emnapi/runtime': 1.11.3 optional: true - '@img/sharp-webcontainers-wasm32@0.35.4': + '@img/sharp-webcontainers-wasm32@0.35.5': dependencies: - '@img/sharp-wasm32': 0.35.4 + '@img/sharp-wasm32': 0.35.5 optional: true - '@img/sharp-win32-arm64@0.35.4': + '@img/sharp-win32-arm64@0.35.5': optional: true - '@img/sharp-win32-ia32@0.35.4': + '@img/sharp-win32-ia32@0.35.5': optional: true - '@img/sharp-win32-x64@0.35.4': + '@img/sharp-win32-x64@0.35.5': optional: true '@isaacs/fs-minipass@4.0.1': @@ -5354,51 +5442,51 @@ snapshots: '@napi-rs/keyring-win32-ia32-msvc': 1.2.0 '@napi-rs/keyring-win32-x64-msvc': 1.2.0 - '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)': dependencies: '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 + '@emnapi/runtime': 1.11.3 '@tybys/wasm-util': 0.10.3 optional: true - '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)': + '@napi-rs/wasm-runtime@1.2.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.11.3 - '@tybys/wasm-util': 0.10.3 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.4 optional: true - '@next/env@16.3.4': {} + '@next/env@16.3.6': {} - '@next/eslint-plugin-next@16.3.4(eslint@9.39.5(jiti@2.7.0))': + '@next/eslint-plugin-next@16.3.6(eslint@9.39.5(jiti@2.7.0))': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.5(jiti@2.7.0)) fast-glob: 3.3.1 transitivePeerDependencies: - eslint - '@next/swc-darwin-arm64@16.3.4': + '@next/swc-darwin-arm64@16.3.6': optional: true - '@next/swc-darwin-x64@16.3.4': + '@next/swc-darwin-x64@16.3.6': optional: true - '@next/swc-linux-arm64-gnu@16.3.4': + '@next/swc-linux-arm64-gnu@16.3.6': optional: true - '@next/swc-linux-arm64-musl@16.3.4': + '@next/swc-linux-arm64-musl@16.3.6': optional: true - '@next/swc-linux-x64-gnu@16.3.4': + '@next/swc-linux-x64-gnu@16.3.6': optional: true - '@next/swc-linux-x64-musl@16.3.4': + '@next/swc-linux-x64-musl@16.3.6': optional: true - '@next/swc-win32-arm64-msvc@16.3.4': + '@next/swc-win32-arm64-msvc@16.3.6': optional: true - '@next/swc-win32-x64-msvc@16.3.4': + '@next/swc-win32-x64-msvc@16.3.6': optional: true '@nodelib/fs.scandir@2.1.5': @@ -5555,352 +5643,352 @@ snapshots: '@radix-ui/primitive@1.1.7': {} - '@radix-ui/react-accordion@1.2.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-accordion@1.2.18(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-collapsible': 1.1.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-collection': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-compose-refs': 1.1.4(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-context': 1.2.1(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-direction': 1.1.3(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-id': 1.1.3(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.5(@types/react@19.2.17)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-collapsible': 1.1.18(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-collection': 1.1.13(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-compose-refs': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.1(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-id': 1.1.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.8(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.5(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.7(@types/react@19.2.18) - '@radix-ui/react-arrow@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-arrow@1.1.13(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/react-primitive': 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-primitive': 2.1.8(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.7(@types/react@19.2.18) - '@radix-ui/react-collapsible@1.1.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-collapsible@1.1.18(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-compose-refs': 1.1.4(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-context': 1.2.1(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-id': 1.1.3(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-presence': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.5(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.3(@types/react@19.2.17)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.1(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-id': 1.1.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-presence': 1.1.9(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.8(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.3(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.7(@types/react@19.2.18) - '@radix-ui/react-collection@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-collection@1.1.13(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/react-compose-refs': 1.1.4(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-context': 1.2.1(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-slot': 1.3.1(@types/react@19.2.17)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.1(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.8(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-slot': 1.3.1(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.7(@types/react@19.2.18) - '@radix-ui/react-compose-refs@1.1.4(@types/react@19.2.17)(react@19.2.4)': + '@radix-ui/react-compose-refs@1.1.4(@types/react@19.2.18)(react@19.2.8)': dependencies: - react: 19.2.4 + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-context@1.2.1(@types/react@19.2.17)(react@19.2.4)': + '@radix-ui/react-context@1.2.1(@types/react@19.2.18)(react@19.2.8)': dependencies: - react: 19.2.4 + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-dialog@1.1.21(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-dialog@1.1.21(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-compose-refs': 1.1.4(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-context': 1.2.1(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-dismissable-layer': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-focus-guards': 1.1.5(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-focus-scope': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-id': 1.1.3(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-portal': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-presence': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-slot': 1.3.1(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.5(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.3(@types/react@19.2.17)(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.1(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-dismissable-layer': 1.1.17(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-focus-guards': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-focus-scope': 1.1.14(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-id': 1.1.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-portal': 1.1.15(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-presence': 1.1.9(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.8(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-slot': 1.3.1(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.3(@types/react@19.2.18)(react@19.2.8) aria-hidden: 1.2.6 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.4) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-remove-scroll: 2.7.2(@types/react@19.2.18)(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.7(@types/react@19.2.18) - '@radix-ui/react-direction@1.1.3(@types/react@19.2.17)(react@19.2.4)': + '@radix-ui/react-direction@1.1.3(@types/react@19.2.18)(react@19.2.8)': dependencies: - react: 19.2.4 + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-dismissable-layer@1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-dismissable-layer@1.1.17(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-compose-refs': 1.1.4(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.3(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-use-effect-event': 0.0.4(@types/react@19.2.17)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.8(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-effect-event': 0.0.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.7(@types/react@19.2.18) - '@radix-ui/react-focus-guards@1.1.5(@types/react@19.2.17)(react@19.2.4)': + '@radix-ui/react-focus-guards@1.1.5(@types/react@19.2.18)(react@19.2.8)': dependencies: - react: 19.2.4 + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-focus-scope@1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-focus-scope@1.1.14(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/react-compose-refs': 1.1.4(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.3(@types/react@19.2.17)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.8(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.3(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.7(@types/react@19.2.18) - '@radix-ui/react-id@1.1.3(@types/react@19.2.17)(react@19.2.4)': + '@radix-ui/react-id@1.1.3(@types/react@19.2.18)(react@19.2.8)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.3(@types/react@19.2.17)(react@19.2.4) - react: 19.2.4 + '@radix-ui/react-use-layout-effect': 1.1.3(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-navigation-menu@1.2.20(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-navigation-menu@1.2.20(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-collection': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-compose-refs': 1.1.4(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-context': 1.2.1(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-direction': 1.1.3(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-dismissable-layer': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-id': 1.1.3(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-presence': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.3(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.5(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.3(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-use-previous': 1.1.3(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-visually-hidden': 1.2.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-collection': 1.1.13(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-compose-refs': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.1(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-dismissable-layer': 1.1.17(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-id': 1.1.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-presence': 1.1.9(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.8(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-previous': 1.1.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-visually-hidden': 1.2.9(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.7(@types/react@19.2.18) - '@radix-ui/react-popover@1.1.21(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-popover@1.1.21(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-compose-refs': 1.1.4(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-context': 1.2.1(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-dismissable-layer': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-focus-guards': 1.1.5(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-focus-scope': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-id': 1.1.3(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-popper': 1.3.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-portal': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-presence': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-slot': 1.3.1(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.5(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.3(@types/react@19.2.17)(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.1(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-dismissable-layer': 1.1.17(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-focus-guards': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-focus-scope': 1.1.14(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-id': 1.1.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-popper': 1.3.5(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-portal': 1.1.15(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-presence': 1.1.9(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.8(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-slot': 1.3.1(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.3(@types/react@19.2.18)(react@19.2.8) aria-hidden: 1.2.6 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.4) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-remove-scroll: 2.7.2(@types/react@19.2.18)(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-popper@1.3.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@floating-ui/react-dom': 2.1.9(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-arrow': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-compose-refs': 1.1.4(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-context': 1.2.1(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.3(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.3(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-use-rect': 1.1.3(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-use-size': 1.1.3(@types/react@19.2.17)(react@19.2.4) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.7(@types/react@19.2.18) + + '@radix-ui/react-popper@1.3.5(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@floating-ui/react-dom': 2.1.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-arrow': 1.1.13(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-compose-refs': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.1(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.8(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-rect': 1.1.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-size': 1.1.3(@types/react@19.2.18)(react@19.2.8) '@radix-ui/rect': 1.1.3 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.7(@types/react@19.2.18) - '@radix-ui/react-portal@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-portal@1.1.15(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/react-primitive': 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.3(@types/react@19.2.17)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-primitive': 2.1.8(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.3(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.7(@types/react@19.2.18) - '@radix-ui/react-presence@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-presence@1.1.9(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.3(@types/react@19.2.17)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.3(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.7(@types/react@19.2.18) - '@radix-ui/react-primitive@2.1.8(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-primitive@2.1.8(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/react-slot': 1.3.1(@types/react@19.2.17)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-slot': 1.3.1(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.7(@types/react@19.2.18) - '@radix-ui/react-roving-focus@1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-roving-focus@1.1.17(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-collection': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-compose-refs': 1.1.4(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-context': 1.2.1(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-direction': 1.1.3(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-id': 1.1.3(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.3(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.5(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-use-is-hydrated': 0.1.2(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.3(@types/react@19.2.17)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-collection': 1.1.13(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-compose-refs': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.1(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-id': 1.1.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.8(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-is-hydrated': 0.1.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.3(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.7(@types/react@19.2.18) - '@radix-ui/react-scroll-area@1.2.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-scroll-area@1.2.16(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/number': 1.1.3 '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-compose-refs': 1.1.4(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-context': 1.2.1(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-direction': 1.1.3(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-presence': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.3(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.3(@types/react@19.2.17)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.1(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-presence': 1.1.9(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.8(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.3(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.7(@types/react@19.2.18) - '@radix-ui/react-slot@1.3.1(@types/react@19.2.17)(react@19.2.4)': + '@radix-ui/react-slot@1.3.1(@types/react@19.2.18)(react@19.2.8)': dependencies: '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-compose-refs': 1.1.4(@types/react@19.2.17)(react@19.2.4) - react: 19.2.4 + '@radix-ui/react-compose-refs': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-tabs@1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-tabs@1.1.19(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-context': 1.2.1(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-direction': 1.1.3(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-id': 1.1.3(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-presence': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-roving-focus': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.5(@types/react@19.2.17)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-context': 1.2.1(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-id': 1.1.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-presence': 1.1.9(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.8(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-roving-focus': 1.1.17(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.5(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.7(@types/react@19.2.18) - '@radix-ui/react-use-callback-ref@1.1.3(@types/react@19.2.17)(react@19.2.4)': + '@radix-ui/react-use-callback-ref@1.1.3(@types/react@19.2.18)(react@19.2.8)': dependencies: - react: 19.2.4 + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-use-controllable-state@1.2.5(@types/react@19.2.17)(react@19.2.4)': + '@radix-ui/react-use-controllable-state@1.2.5(@types/react@19.2.18)(react@19.2.8)': dependencies: '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-use-effect-event': 0.0.4(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.3(@types/react@19.2.17)(react@19.2.4) - react: 19.2.4 + '@radix-ui/react-use-effect-event': 0.0.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.3(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-use-effect-event@0.0.4(@types/react@19.2.17)(react@19.2.4)': + '@radix-ui/react-use-effect-event@0.0.4(@types/react@19.2.18)(react@19.2.8)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.3(@types/react@19.2.17)(react@19.2.4) - react: 19.2.4 + '@radix-ui/react-use-layout-effect': 1.1.3(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-use-is-hydrated@0.1.2(@types/react@19.2.17)(react@19.2.4)': + '@radix-ui/react-use-is-hydrated@0.1.2(@types/react@19.2.18)(react@19.2.8)': dependencies: - react: 19.2.4 + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-use-layout-effect@1.1.3(@types/react@19.2.17)(react@19.2.4)': + '@radix-ui/react-use-layout-effect@1.1.3(@types/react@19.2.18)(react@19.2.8)': dependencies: - react: 19.2.4 + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-use-previous@1.1.3(@types/react@19.2.17)(react@19.2.4)': + '@radix-ui/react-use-previous@1.1.3(@types/react@19.2.18)(react@19.2.8)': dependencies: - react: 19.2.4 + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-use-rect@1.1.3(@types/react@19.2.17)(react@19.2.4)': + '@radix-ui/react-use-rect@1.1.3(@types/react@19.2.18)(react@19.2.8)': dependencies: '@radix-ui/rect': 1.1.3 - react: 19.2.4 + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-use-size@1.1.3(@types/react@19.2.17)(react@19.2.4)': + '@radix-ui/react-use-size@1.1.3(@types/react@19.2.18)(react@19.2.8)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.3(@types/react@19.2.17)(react@19.2.4) - react: 19.2.4 + '@radix-ui/react-use-layout-effect': 1.1.3(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-visually-hidden@1.2.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-visually-hidden@1.2.9(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/react-primitive': 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-primitive': 2.1.8(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.7(@types/react@19.2.18) '@radix-ui/rect@1.1.3': {} @@ -6091,6 +6179,11 @@ snapshots: tslib: 2.8.1 optional: true + '@tybys/wasm-util@0.10.4': + dependencies: + tslib: 2.8.1 + optional: true + '@types/debug@4.1.13': dependencies: '@types/ms': 2.1.0 @@ -6121,15 +6214,15 @@ snapshots: dependencies: undici-types: 5.26.5 - '@types/node@20.19.43': + '@types/node@22.20.4': dependencies: undici-types: 6.21.0 - '@types/react-dom@19.2.3(@types/react@19.2.17)': + '@types/react-dom@19.2.7(@types/react@19.2.18)': dependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@types/react@19.2.17': + '@types/react@19.2.18': dependencies: csstype: 3.2.3 @@ -6137,57 +6230,57 @@ snapshots: '@types/unist@3.0.3': {} - '@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.71.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.65.0 - '@typescript-eslint/type-utils': 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/utils': 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.65.0 + '@typescript-eslint/parser': 8.71.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.71.0 + '@typescript-eslint/type-utils': 8.71.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/utils': 8.71.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.71.0 eslint: 9.39.5(jiti@2.7.0) - ignore: 7.0.6 + ignore: 7.0.10 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/parser@8.71.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.65.0 - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.65.0 + '@typescript-eslint/scope-manager': 8.71.0 + '@typescript-eslint/types': 8.71.0 + '@typescript-eslint/typescript-estree': 8.71.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.71.0 debug: 4.4.3 eslint: 9.39.5(jiti@2.7.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.65.0(typescript@5.9.3)': + '@typescript-eslint/project-service@8.71.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@5.9.3) - '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/tsconfig-utils': 8.71.0(typescript@5.9.3) + '@typescript-eslint/types': 8.71.0 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.65.0': + '@typescript-eslint/scope-manager@8.71.0': dependencies: - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/visitor-keys': 8.65.0 + '@typescript-eslint/types': 8.71.0 + '@typescript-eslint/visitor-keys': 8.71.0 - '@typescript-eslint/tsconfig-utils@8.65.0(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.71.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.71.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/types': 8.71.0 + '@typescript-eslint/typescript-estree': 8.71.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.71.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) debug: 4.4.3 eslint: 9.39.5(jiti@2.7.0) ts-api-utils: 2.5.0(typescript@5.9.3) @@ -6195,14 +6288,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.65.0': {} + '@typescript-eslint/types@8.71.0': {} - '@typescript-eslint/typescript-estree@8.65.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.71.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.65.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@5.9.3) - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/visitor-keys': 8.65.0 + '@typescript-eslint/project-service': 8.71.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.71.0(typescript@5.9.3) + '@typescript-eslint/types': 8.71.0 + '@typescript-eslint/visitor-keys': 8.71.0 debug: 4.4.3 minimatch: 10.2.6 semver: 7.8.5 @@ -6212,20 +6305,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/utils@8.71.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)) - '@typescript-eslint/scope-manager': 8.65.0 - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.71.0 + '@typescript-eslint/types': 8.71.0 + '@typescript-eslint/typescript-estree': 8.71.0(typescript@5.9.3) eslint: 9.39.5(jiti@2.7.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.65.0': + '@typescript-eslint/visitor-keys@8.71.0': dependencies: - '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/types': 8.71.0 eslint-visitor-keys: 5.0.1 '@ungap/structured-clone@1.3.3': {} @@ -6288,7 +6381,7 @@ snapshots: dependencies: '@emnapi/core': 1.10.0 '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@napi-rs/wasm-runtime': 1.2.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': @@ -6309,7 +6402,7 @@ snapshots: fs-extra: 11.1.0 get-port: 5.1.1 oxc-transform: 0.111.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3) - path-to-regexp: 8.3.0 + path-to-regexp: 8.4.0 resolve.exports: 2.0.3 rolldown: 1.0.0-rc.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3) srvx: 0.11.16 @@ -6330,7 +6423,7 @@ snapshots: is-buffer: 2.0.5 is-node-process: 1.2.0 throttleit: 2.1.0 - undici: 6.28.0 + undici: 6.28.1 '@vercel/build-utils@14.9.0': dependencies: @@ -6393,7 +6486,7 @@ snapshots: '@vercel/node': 12.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0) '@vercel/static-config': 3.4.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3) fs-extra: 11.1.0 - path-to-regexp: 8.3.0 + path-to-regexp: 8.4.0 ts-morph: 12.0.0 zod: 3.22.4 transitivePeerDependencies: @@ -6424,7 +6517,7 @@ snapshots: micro: 9.3.5-canary.3 ms: 2.1.1 node-fetch: 2.6.7 - path-to-regexp: 8.2.0 + path-to-regexp: 8.4.0 promisepipe: 3.0.0 semver: 7.5.4 stat-mode: 0.3.0 @@ -6474,7 +6567,7 @@ snapshots: '@vercel/node': 12.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0) '@vercel/static-config': 3.4.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3) fs-extra: 11.1.0 - path-to-regexp: 8.3.0 + path-to-regexp: 8.4.0 ts-morph: 12.0.0 zod: 3.22.4 transitivePeerDependencies: @@ -6568,7 +6661,7 @@ snapshots: ts-morph: 12.0.0 tsx: 4.21.0 typescript: 5.9.3 - undici: 6.28.0 + undici: 6.28.1 transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -6653,7 +6746,7 @@ snapshots: ms: 2.1.3 picocolors: 1.1.1 tar-stream: 3.1.7 - undici: 7.29.0 + undici: 7.29.1 xdg-app-paths: 5.1.0 zod: 4.4.3 transitivePeerDependencies: @@ -6673,7 +6766,7 @@ snapshots: '@vercel/static-config@3.4.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)': dependencies: - ajv: 8.6.3 + ajv: 8.18.0 json-schema-to-ts: 1.6.4 oxc-parser: 0.121.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3) ts-morph: 12.0.0 @@ -6716,12 +6809,12 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ajv@8.6.3: + ajv@8.18.0: dependencies: fast-deep-equal: 3.1.3 + fast-uri: 3.1.8 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 - uri-js: 4.4.1 ansi-styles@4.3.0: dependencies: @@ -6744,14 +6837,14 @@ snapshots: call-bound: 1.0.4 is-array-buffer: 3.0.5 - array-includes@3.1.9: + array-includes@3.2.0: dependencies: call-bind: 1.0.9 call-bound: 1.0.4 define-properties: 1.2.1 es-abstract: 1.24.2 es-object-atoms: 1.1.2 - get-intrinsic: 1.3.0 + es-shim-unscopables: 1.1.0 is-string: 1.1.1 math-intrinsics: 1.1.0 @@ -6828,7 +6921,7 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 - axe-core@4.12.1: {} + axe-core@4.13.0: {} axobject-query@4.1.0: {} @@ -6842,18 +6935,18 @@ snapshots: bare-events@2.9.2: {} - baseline-browser-mapping@2.11.1: {} + baseline-browser-mapping@2.11.26: {} bindings@1.5.0: dependencies: file-uri-to-path: 1.0.0 - brace-expansion@1.1.18: + brace-expansion@1.1.21: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@5.0.9: + brace-expansion@5.0.12: dependencies: balanced-match: 4.0.4 @@ -6861,13 +6954,13 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.28.7: + browserslist@4.29.2: dependencies: - baseline-browser-mapping: 2.11.1 - caniuse-lite: 1.0.30001806 - electron-to-chromium: 1.5.396 - node-releases: 2.0.51 - update-browserslist-db: 1.2.3(browserslist@4.28.7) + baseline-browser-mapping: 2.11.26 + caniuse-lite: 1.0.30001813 + electron-to-chromium: 1.5.440 + node-releases: 2.0.57 + update-browserslist-db: 1.3.3(browserslist@4.29.2) buffer-crc32@0.2.13: {} @@ -6892,7 +6985,7 @@ snapshots: callsites@3.1.0: {} - caniuse-lite@1.0.30001806: {} + caniuse-lite@1.0.30001813: {} ccount@2.0.1: {} @@ -7049,7 +7142,7 @@ snapshots: signal-exit: 4.0.2 time-span: 4.0.0 - electron-to-chromium@1.5.396: {} + electron-to-chromium@1.5.440: {} emoji-regex@9.2.2: {} @@ -7127,10 +7220,10 @@ snapshots: string.prototype.trimstart: 1.0.8 typed-array-buffer: 1.0.3 typed-array-byte-length: 1.0.3 - typed-array-byte-offset: 1.0.4 + typed-array-byte-offset: 1.0.5 typed-array-length: 1.0.8 unbox-primitive: 1.1.0 - which-typed-array: 1.1.22 + which-typed-array: 1.1.24 es-define-property@1.0.1: {} @@ -7261,18 +7354,18 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-config-next@16.3.4(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3): + eslint-config-next@16.3.6(@typescript-eslint/parser@8.71.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3): dependencies: - '@next/eslint-plugin-next': 16.3.4(eslint@9.39.5(jiti@2.7.0)) + '@next/eslint-plugin-next': 16.3.6(eslint@9.39.5(jiti@2.7.0)) eslint: 9.39.5(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.5(jiti@2.7.0)) eslint-plugin-react: 7.37.5(eslint@9.39.5(jiti@2.7.0)) eslint-plugin-react-hooks: 7.1.1(eslint@9.39.5(jiti@2.7.0)) globals: 16.4.0 - typescript-eslint: 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + typescript-eslint: 8.71.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -7284,7 +7377,7 @@ snapshots: eslint-import-resolver-node@0.3.10: dependencies: debug: 3.2.7 - is-core-module: 2.16.2 + is-core-module: 2.17.0 resolve: 2.0.0-next.7 transitivePeerDependencies: - supports-color @@ -7294,31 +7387,31 @@ snapshots: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 eslint: 9.39.5(jiti@2.7.0) - get-tsconfig: 4.14.0 + get-tsconfig: 4.14.3 is-bun-module: 2.0.0 stable-hash: 0.0.5 tinyglobby: 0.2.17 unrs-resolver: 1.12.2 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.14.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)): + eslint-module-utils@2.14.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': 8.71.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) eslint: 9.39.5(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)): dependencies: '@rtsao/scc': 1.1.0 - array-includes: 3.1.9 + array-includes: 3.2.0 array.prototype.findlastindex: 1.2.6 array.prototype.flat: 1.3.3 array.prototype.flatmap: 1.3.3 @@ -7326,9 +7419,9 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.5(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)) + eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)) hasown: 2.0.4 - is-core-module: 2.16.2 + is-core-module: 2.17.0 is-glob: 4.0.3 minimatch: 3.1.5 object.fromentries: 2.0.8 @@ -7338,7 +7431,7 @@ snapshots: string.prototype.trimend: 1.0.10 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': 8.71.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -7347,10 +7440,10 @@ snapshots: eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.5(jiti@2.7.0)): dependencies: aria-query: 5.3.2 - array-includes: 3.1.9 + array-includes: 3.2.0 array.prototype.flatmap: 1.3.3 ast-types-flow: 0.0.8 - axe-core: 4.12.1 + axe-core: 4.13.0 axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 @@ -7366,7 +7459,7 @@ snapshots: eslint-plugin-react-hooks@7.1.1(eslint@9.39.5(jiti@2.7.0)): dependencies: '@babel/core': 7.29.7 - '@babel/parser': 7.29.7 + '@babel/parser': 7.29.9 eslint: 9.39.5(jiti@2.7.0) hermes-parser: 0.25.1 zod: 4.4.3 @@ -7376,7 +7469,7 @@ snapshots: eslint-plugin-react@7.37.5(eslint@9.39.5(jiti@2.7.0)): dependencies: - array-includes: 3.1.9 + array-includes: 3.2.0 array.prototype.findlast: 1.2.5 array.prototype.flatmap: 1.3.3 array.prototype.tosorted: 1.1.4 @@ -7393,7 +7486,7 @@ snapshots: prop-types: 15.8.1 resolve: 2.0.0-next.7 semver: 6.3.1 - string.prototype.matchall: 4.0.12 + string.prototype.matchall: 4.1.0 string.prototype.repeat: 1.0.0 eslint-scope@8.4.0: @@ -7566,6 +7659,8 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-uri@3.1.8: {} + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -7604,14 +7699,14 @@ snapshots: dependencies: is-callable: 1.2.7 - framer-motion@12.42.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + framer-motion@12.42.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: motion-dom: 12.42.2 motion-utils: 12.39.0 tslib: 2.8.1 optionalDependencies: - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) fs-extra@11.1.0: dependencies: @@ -7628,7 +7723,7 @@ snapshots: fsevents@2.3.3: optional: true - fumadocs-core@16.12.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.26.0(react@19.2.4))(next@16.3.4(@babel/core@7.29.7)(@types/node@20.19.43)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.4.3): + fumadocs-core@16.12.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@1.26.0(react@19.2.8))(next@16.3.6(@babel/core@7.29.7)(@types/node@22.20.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3): dependencies: '@orama/orama': 3.1.18 estree-util-value-to-estree: 3.5.0 @@ -7653,23 +7748,23 @@ snapshots: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.5 '@types/mdast': 4.0.4 - '@types/react': 19.2.17 - lucide-react: 1.26.0(react@19.2.4) - next: 16.3.4(@babel/core@7.29.7)(@types/node@20.19.43)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@types/react': 19.2.18 + lucide-react: 1.26.0(react@19.2.8) + next: 16.3.6(@babel/core@7.29.7)(@types/node@22.20.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) zod: 4.4.3 transitivePeerDependencies: - supports-color - fumadocs-mdx@14.3.2(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.17)(fumadocs-core@16.12.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.26.0(react@19.2.4))(next@16.3.4(@babel/core@7.29.7)(@types/node@20.19.43)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.4.3))(next@16.3.4(@babel/core@7.29.7)(@types/node@20.19.43)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4): + fumadocs-mdx@14.3.2(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.18)(fumadocs-core@16.12.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@1.26.0(react@19.2.8))(next@16.3.6(@babel/core@7.29.7)(@types/node@22.20.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3))(next@16.3.6(@babel/core@7.29.7)(@types/node@22.20.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8): dependencies: '@mdx-js/mdx': 3.1.1 '@standard-schema/spec': 1.1.0 chokidar: 5.0.0 esbuild: 0.28.1 estree-util-value-to-estree: 3.5.0 - fumadocs-core: 16.12.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.26.0(react@19.2.4))(next@16.3.4(@babel/core@7.29.7)(@types/node@20.19.43)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.4.3) + fumadocs-core: 16.12.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@1.26.0(react@19.2.8))(next@16.3.6(@babel/core@7.29.7)(@types/node@22.20.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3) js-yaml: 4.3.2 mdast-util-mdx: 3.0.0 mdast-util-to-markdown: 2.1.2 @@ -7685,43 +7780,43 @@ snapshots: optionalDependencies: '@types/mdast': 4.0.4 '@types/mdx': 2.0.14 - '@types/react': 19.2.17 - next: 16.3.4(@babel/core@7.29.7)(@types/node@20.19.43)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 + '@types/react': 19.2.18 + next: 16.3.6(@babel/core@7.29.7)(@types/node@22.20.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 transitivePeerDependencies: - supports-color - fumadocs-ui@16.12.1(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.12.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.26.0(react@19.2.4))(next@16.3.4(@babel/core@7.29.7)(@types/node@20.19.43)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.4.3))(next@16.3.4(@babel/core@7.29.7)(@types/node@20.19.43)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tailwindcss@4.3.3): + fumadocs-ui@16.12.1(@types/mdx@2.0.14)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(fumadocs-core@16.12.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@1.26.0(react@19.2.8))(next@16.3.6(@babel/core@7.29.7)(@types/node@22.20.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3))(next@16.3.6(@babel/core@7.29.7)(@types/node@22.20.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tailwindcss@4.3.3): dependencies: - '@fuma-translate/react': 1.0.2(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@fuma-translate/react': 1.0.2(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@fumadocs/tailwind': 0.1.1(tailwindcss@4.3.3) - '@radix-ui/react-accordion': 1.2.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-collapsible': 1.1.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-dialog': 1.1.21(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-direction': 1.1.3(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-navigation-menu': 1.2.20(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-popover': 1.1.21(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-presence': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-scroll-area': 1.2.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-slot': 1.3.1(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-tabs': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-accordion': 1.2.18(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-collapsible': 1.1.18(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-dialog': 1.1.21(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-direction': 1.1.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-navigation-menu': 1.2.20(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-popover': 1.1.21(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-presence': 1.1.9(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-scroll-area': 1.2.16(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-slot': 1.3.1(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-tabs': 1.1.19(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) class-variance-authority: 0.7.1 cnfast: 0.0.8 - fumadocs-core: 16.12.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.26.0(react@19.2.4))(next@16.3.4(@babel/core@7.29.7)(@types/node@20.19.43)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.4.3) - lucide-react: 1.26.0(react@19.2.4) - motion: 12.42.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - next-themes: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.4) + fumadocs-core: 16.12.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@1.26.0(react@19.2.8))(next@16.3.6(@babel/core@7.29.7)(@types/node@22.20.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3) + lucide-react: 1.26.0(react@19.2.8) + motion: 12.42.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + next-themes: 0.4.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-remove-scroll: 2.7.2(@types/react@19.2.18)(react@19.2.8) rehype-raw: 7.0.0 scroll-into-view-if-needed: 3.1.0 shiki: 4.3.1 unist-util-visit: 5.1.0 optionalDependencies: '@types/mdx': 2.0.14 - '@types/react': 19.2.17 - next: 16.3.4(@babel/core@7.29.7)(@types/node@20.19.43)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@types/react': 19.2.18 + next: 16.3.6(@babel/core@7.29.7)(@types/node@22.20.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) transitivePeerDependencies: - '@emotion/is-prop-valid' - '@types/react-dom' @@ -7787,6 +7882,10 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + get-tsconfig@4.14.3: + dependencies: + resolve-pkg-maps: 1.0.0 + github-slugger@2.0.0: {} glob-parent@5.1.2: @@ -7979,7 +8078,7 @@ snapshots: ignore@5.3.2: {} - ignore@7.0.6: {} + ignore@7.0.10: {} import-fresh@3.3.1: dependencies: @@ -8036,7 +8135,7 @@ snapshots: is-callable@1.2.7: {} - is-core-module@2.16.2: + is-core-module@2.17.0: dependencies: hasown: 2.0.4 @@ -8122,7 +8221,7 @@ snapshots: is-typed-array@1.1.15: dependencies: - which-typed-array: 1.1.22 + which-typed-array: 1.1.24 is-weakmap@2.0.2: {} @@ -8197,7 +8296,7 @@ snapshots: jsx-ast-utils@3.3.5: dependencies: - array-includes: 3.1.9 + array-includes: 3.2.0 array.prototype.flat: 1.3.3 object.assign: 4.1.7 object.values: 1.2.1 @@ -8290,9 +8389,9 @@ snapshots: dependencies: yallist: 4.0.0 - lucide-react@1.26.0(react@19.2.4): + lucide-react@1.26.0(react@19.2.8): dependencies: - react: 19.2.4 + react: 19.2.8 luxon@3.7.2: {} @@ -8758,11 +8857,11 @@ snapshots: minimatch@10.2.6: dependencies: - brace-expansion: 5.0.9 + brace-expansion: 5.0.12 minimatch@3.1.5: dependencies: - brace-expansion: 1.1.18 + brace-expansion: 1.1.21 minimist@1.2.8: {} @@ -8780,13 +8879,13 @@ snapshots: motion-utils@12.39.0: {} - motion@12.42.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + motion@12.42.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: - framer-motion: 12.42.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + framer-motion: 12.42.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) tslib: 2.8.1 optionalDependencies: - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) mri@1.2.0: {} @@ -8798,35 +8897,37 @@ snapshots: nanoid@3.3.18: {} + nanoid@3.3.19: {} + napi-postinstall@0.3.4: {} natural-compare@1.4.0: {} - next-themes@0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + next-themes@0.4.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) - next@16.3.4(@babel/core@7.29.7)(@types/node@20.19.43)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + next@16.3.6(@babel/core@7.29.7)(@types/node@22.20.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: - '@next/env': 16.3.4 + '@next/env': 16.3.6 '@swc/helpers': 0.5.23 - baseline-browser-mapping: 2.11.1 - caniuse-lite: 1.0.30001806 + baseline-browser-mapping: 2.11.26 + caniuse-lite: 1.0.30001813 postcss: 8.5.23 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - styled-jsx: 5.1.6(@babel/core@7.29.7)(react@19.2.4) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + styled-jsx: 5.1.6(@babel/core@7.29.7)(react@19.2.8) optionalDependencies: - '@next/swc-darwin-arm64': 16.3.4 - '@next/swc-darwin-x64': 16.3.4 - '@next/swc-linux-arm64-gnu': 16.3.4 - '@next/swc-linux-arm64-musl': 16.3.4 - '@next/swc-linux-x64-gnu': 16.3.4 - '@next/swc-linux-x64-musl': 16.3.4 - '@next/swc-win32-arm64-msvc': 16.3.4 - '@next/swc-win32-x64-msvc': 16.3.4 - sharp: 0.35.4(@types/node@20.19.43) + '@next/swc-darwin-arm64': 16.3.6 + '@next/swc-darwin-x64': 16.3.6 + '@next/swc-linux-arm64-gnu': 16.3.6 + '@next/swc-linux-arm64-musl': 16.3.6 + '@next/swc-linux-x64-gnu': 16.3.6 + '@next/swc-linux-x64-musl': 16.3.6 + '@next/swc-win32-arm64-msvc': 16.3.6 + '@next/swc-win32-x64-msvc': 16.3.6 + sharp: 0.35.5(@types/node@22.20.4) transitivePeerDependencies: - '@babel/core' - '@types/node' @@ -8853,7 +8954,7 @@ snapshots: node-gyp-build@4.8.4: {} - node-releases@2.0.51: {} + node-releases@2.0.57: {} nopt@8.1.0: dependencies: @@ -9052,9 +9153,7 @@ snapshots: path-to-regexp@6.3.0: {} - path-to-regexp@8.2.0: {} - - path-to-regexp@8.3.0: {} + path-to-regexp@8.4.0: {} pend@1.2.0: {} @@ -9070,7 +9169,7 @@ snapshots: postcss@8.5.23: dependencies: - nanoid: 3.3.18 + nanoid: 3.3.19 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -9112,41 +9211,41 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 - react-dom@19.2.4(react@19.2.4): + react-dom@19.2.8(react@19.2.8): dependencies: - react: 19.2.4 + react: 19.2.8 scheduler: 0.27.0 react-is@16.13.1: {} - react-remove-scroll-bar@2.3.8(@types/react@19.2.17)(react@19.2.4): + react-remove-scroll-bar@2.3.8(@types/react@19.2.18)(react@19.2.8): dependencies: - react: 19.2.4 - react-style-singleton: 2.2.3(@types/react@19.2.17)(react@19.2.4) + react: 19.2.8 + react-style-singleton: 2.2.3(@types/react@19.2.18)(react@19.2.8) tslib: 2.8.1 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - react-remove-scroll@2.7.2(@types/react@19.2.17)(react@19.2.4): + react-remove-scroll@2.7.2(@types/react@19.2.18)(react@19.2.8): dependencies: - react: 19.2.4 - react-remove-scroll-bar: 2.3.8(@types/react@19.2.17)(react@19.2.4) - react-style-singleton: 2.2.3(@types/react@19.2.17)(react@19.2.4) + react: 19.2.8 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.18)(react@19.2.8) + react-style-singleton: 2.2.3(@types/react@19.2.18)(react@19.2.8) tslib: 2.8.1 - use-callback-ref: 1.3.3(@types/react@19.2.17)(react@19.2.4) - use-sidecar: 1.1.3(@types/react@19.2.17)(react@19.2.4) + use-callback-ref: 1.3.3(@types/react@19.2.18)(react@19.2.8) + use-sidecar: 1.1.3(@types/react@19.2.18)(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - react-style-singleton@2.2.3(@types/react@19.2.17)(react@19.2.4): + react-style-singleton@2.2.3(@types/react@19.2.18)(react@19.2.8): dependencies: get-nonce: 1.0.1 - react: 19.2.4 + react: 19.2.8 tslib: 2.8.1 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - react@19.2.4: {} + react@19.2.8: {} readdirp@4.1.2: {} @@ -9288,7 +9387,7 @@ snapshots: resolve@2.0.0-next.7: dependencies: es-errors: 1.3.0 - is-core-module: 2.16.2 + is-core-module: 2.17.0 node-exports-info: 1.6.2 object-keys: 1.1.1 path-parse: 1.0.7 @@ -9397,38 +9496,38 @@ snapshots: setprototypeof@1.1.1: {} - sharp@0.35.4(@types/node@20.19.43): + sharp@0.35.5(@types/node@22.20.4): dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 semver: 7.8.5 optionalDependencies: - '@img/sharp-darwin-arm64': 0.35.4 - '@img/sharp-darwin-x64': 0.35.4 - '@img/sharp-freebsd-wasm32': 0.35.4 - '@img/sharp-libvips-darwin-arm64': 1.3.3 - '@img/sharp-libvips-darwin-x64': 1.3.3 - '@img/sharp-libvips-linux-arm': 1.3.3 - '@img/sharp-libvips-linux-arm64': 1.3.3 - '@img/sharp-libvips-linux-ppc64': 1.3.3 - '@img/sharp-libvips-linux-riscv64': 1.3.3 - '@img/sharp-libvips-linux-s390x': 1.3.3 - '@img/sharp-libvips-linux-x64': 1.3.3 - '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 - '@img/sharp-libvips-linuxmusl-x64': 1.3.3 - '@img/sharp-linux-arm': 0.35.4 - '@img/sharp-linux-arm64': 0.35.4 - '@img/sharp-linux-ppc64': 0.35.4 - '@img/sharp-linux-riscv64': 0.35.4 - '@img/sharp-linux-s390x': 0.35.4 - '@img/sharp-linux-x64': 0.35.4 - '@img/sharp-linuxmusl-arm64': 0.35.4 - '@img/sharp-linuxmusl-x64': 0.35.4 - '@img/sharp-webcontainers-wasm32': 0.35.4 - '@img/sharp-win32-arm64': 0.35.4 - '@img/sharp-win32-ia32': 0.35.4 - '@img/sharp-win32-x64': 0.35.4 - '@types/node': 20.19.43 + '@img/sharp-darwin-arm64': 0.35.5 + '@img/sharp-darwin-x64': 0.35.5 + '@img/sharp-freebsd-wasm32': 0.35.5 + '@img/sharp-libvips-darwin-arm64': 1.3.4 + '@img/sharp-libvips-darwin-x64': 1.3.4 + '@img/sharp-libvips-linux-arm': 1.3.4 + '@img/sharp-libvips-linux-arm64': 1.3.4 + '@img/sharp-libvips-linux-ppc64': 1.3.4 + '@img/sharp-libvips-linux-riscv64': 1.3.4 + '@img/sharp-libvips-linux-s390x': 1.3.4 + '@img/sharp-libvips-linux-x64': 1.3.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.4 + '@img/sharp-libvips-linuxmusl-x64': 1.3.4 + '@img/sharp-linux-arm': 0.35.5 + '@img/sharp-linux-arm64': 0.35.5 + '@img/sharp-linux-ppc64': 0.35.5 + '@img/sharp-linux-riscv64': 0.35.5 + '@img/sharp-linux-s390x': 0.35.5 + '@img/sharp-linux-x64': 0.35.5 + '@img/sharp-linuxmusl-arm64': 0.35.5 + '@img/sharp-linuxmusl-x64': 0.35.5 + '@img/sharp-webcontainers-wasm32': 0.35.5 + '@img/sharp-win32-arm64': 0.35.5 + '@img/sharp-win32-ia32': 0.35.5 + '@img/sharp-win32-x64': 0.35.5 + '@types/node': 22.20.4 optional: true shebang-command@2.0.0: @@ -9526,7 +9625,7 @@ snapshots: define-properties: 1.2.1 es-abstract: 1.24.2 - string.prototype.matchall@4.0.12: + string.prototype.matchall@4.1.0: dependencies: call-bind: 1.0.9 call-bound: 1.0.4 @@ -9590,10 +9689,10 @@ snapshots: dependencies: inline-style-parser: 0.2.7 - styled-jsx@5.1.6(@babel/core@7.29.7)(react@19.2.4): + styled-jsx@5.1.6(@babel/core@7.29.7)(react@19.2.8): dependencies: client-only: 0.0.1 - react: 19.2.4 + react: 19.2.8 optionalDependencies: '@babel/core': 7.29.7 @@ -9603,7 +9702,7 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - tailwind-merge@3.6.0: {} + tailwind-merge@3.7.0: {} tailwindcss@4.3.3: {} @@ -9708,13 +9807,12 @@ snapshots: has-proto: 1.2.0 is-typed-array: 1.1.15 - typed-array-byte-offset@1.0.4: + typed-array-byte-offset@1.0.5: dependencies: available-typed-arrays: 1.0.7 call-bind: 1.0.9 for-each: 0.3.5 gopd: 1.2.0 - has-proto: 1.2.0 is-typed-array: 1.1.15 reflect.getprototypeof: 1.0.10 @@ -9727,12 +9825,12 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3): + typescript-eslint@8.71.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.65.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/parser': 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.65.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.71.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': 8.71.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.71.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.71.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) eslint: 9.39.5(jiti@2.7.0) typescript: 5.9.3 transitivePeerDependencies: @@ -9753,9 +9851,9 @@ snapshots: undici-types@6.21.0: {} - undici@6.28.0: {} + undici@6.28.1: {} - undici@7.29.0: {} + undici@7.29.1: {} unified@11.0.5: dependencies: @@ -9830,9 +9928,9 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.12.2 '@unrs/resolver-binding-win32-x64-msvc': 1.12.2 - update-browserslist-db@1.2.3(browserslist@4.28.7): + update-browserslist-db@1.3.3(browserslist@4.29.2): dependencies: - browserslist: 4.28.7 + browserslist: 4.29.2 escalade: 3.2.0 picocolors: 1.1.1 @@ -9840,20 +9938,20 @@ snapshots: dependencies: punycode: 2.3.1 - use-callback-ref@1.3.3(@types/react@19.2.17)(react@19.2.4): + use-callback-ref@1.3.3(@types/react@19.2.18)(react@19.2.8): dependencies: - react: 19.2.4 + react: 19.2.8 tslib: 2.8.1 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - use-sidecar@1.1.3(@types/react@19.2.17)(react@19.2.4): + use-sidecar@1.1.3(@types/react@19.2.18)(react@19.2.8): dependencies: detect-node-es: 1.1.0 - react: 19.2.4 + react: 19.2.8 tslib: 2.8.1 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 uuid@14.0.1: {} @@ -9893,7 +9991,7 @@ snapshots: luxon: 3.7.2 sandbox: 4.1.0 smol-toml: 1.8.0 - undici: 6.28.0 + undici: 6.28.1 uuid: 14.0.1 zod: 4.1.11 optionalDependencies: @@ -9960,7 +10058,7 @@ snapshots: isarray: 2.0.5 which-boxed-primitive: 1.1.1 which-collection: 1.0.2 - which-typed-array: 1.1.22 + which-typed-array: 1.1.24 which-collection@1.0.2: dependencies: @@ -9969,7 +10067,7 @@ snapshots: is-weakmap: 2.0.2 is-weakset: 2.0.4 - which-typed-array@1.1.22: + which-typed-array@1.1.24: dependencies: available-typed-arrays: 1.0.7 call-bind: 1.0.9 diff --git a/docs/site/src/components/docs/SearchDialog.tsx b/docs/site/src/components/docs/SearchDialog.tsx index 36c56dfab65d..e0bc8032c15d 100644 --- a/docs/site/src/components/docs/SearchDialog.tsx +++ b/docs/site/src/components/docs/SearchDialog.tsx @@ -122,6 +122,16 @@ export default function SearchDialog({ dialogId = 'docs-search-dialog', onClose return query.data }, [query.data]) + const highlightedResults = useMemo(() => { + const highlights = new Map() + for (const result of results) { + if (result.content && !highlights.has(result.content)) { + highlights.set(result.content, renderHighlighted(result.content)) + } + } + return highlights + }, [results]) + const selectedIndex = Math.min(activeIndex, Math.max(results.length - 1, 0)) useEffect(() => { @@ -354,7 +364,10 @@ export default function SearchDialog({ dialogId = 'docs-search-dialog', onClose : 'text-muted-foreground hover:bg-accent hover:text-foreground' )} > - {g.page.content ? renderHighlighted(g.page.content) : g.page.url} + {g.page.content + ? (highlightedResults.get(g.page.content) ?? + renderHighlighted(g.page.content)) + : g.page.url} )}