diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index fda98e1b5..ba687e63e 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -3,21 +3,21 @@ name: Desktop Release on: push: tags: - - "v*" - - "desktop-v*" - - "desktop-macos-v*" - - "desktop-linux-v*" - - "desktop-windows-v*" + - 'v*' + - 'desktop-v*' + - 'desktop-macos-v*' + - 'desktop-linux-v*' + - 'desktop-windows-v*' workflow_dispatch: inputs: tag: - description: "Existing tag to build (e.g. v0.1.0)" + description: 'Existing tag to build (e.g. v0.1.0)' required: true type: string platform: - description: "Optional desktop platform to build." + description: 'Optional desktop platform to build.' required: false - default: "all" + default: 'all' type: choice options: - all @@ -31,6 +31,8 @@ concurrency: env: SOURCE_TAG: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref_name }} + DESKTOP_WORKSPACE: '@getpaseo/desktop-electron' + DESKTOP_PACKAGE_PATH: 'packages/desktop-electron' jobs: publish-macos: @@ -40,9 +42,9 @@ jobs: matrix: include: - runner: macos-14 - rust_target: aarch64-apple-darwin + electron_arch: arm64 - runner: macos-15-intel - rust_target: x86_64-apple-darwin + electron_arch: x64 permissions: contents: write packages: read @@ -75,85 +77,39 @@ jobs: echo "IS_SMOKE_TAG=false" >> "$GITHUB_ENV" fi - - name: Set desktop version from tag - shell: bash - run: | - node <<'NODE' - const fs = require('node:fs'); - const path = require('node:path'); - const version = process.env.DESKTOP_VERSION; - if (!version) throw new Error('DESKTOP_VERSION env var is missing'); - console.log(`Setting desktop version to ${version}`); - - const tauriConfPath = path.join('packages', 'desktop', 'src-tauri', 'tauri.conf.json'); - const tauriConfText = fs.readFileSync(tauriConfPath, 'utf8'); - const tauriRe = /("version"\s*:\s*")([^"]+)(")/; - if (!tauriRe.test(tauriConfText)) throw new Error('Failed to find version in tauri.conf.json'); - fs.writeFileSync(tauriConfPath, tauriConfText.replace(tauriRe, `$1${version}$3`)); - - const cargoTomlPath = path.join('packages', 'desktop', 'src-tauri', 'Cargo.toml'); - const lines = fs.readFileSync(cargoTomlPath, 'utf8').split(/\r?\n/); - let inPackage = false, updated = false; - const result = lines.map((line) => { - if (/^\[package\]\s*$/.test(line)) inPackage = true; - else if (inPackage && /^\[/.test(line)) inPackage = false; - if (inPackage && /^version\s*=\s*".*"\s*$/.test(line)) { - updated = true; - return `version = "${version}"`; - } - return line; - }); - if (!updated) throw new Error('Failed to update Cargo.toml version'); - fs.writeFileSync(cargoTomlPath, result.join('\n') + '\n'); - NODE - - name: Setup Node uses: actions/setup-node@v4 with: - node-version: "22" - cache: "npm" + node-version: '22' + cache: 'npm' cache-dependency-path: package-lock.json - registry-url: "https://npm.pkg.github.com" - scope: "@boudra" - - - name: Install Rust stable - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.rust_target }} - - - name: Restore Rust cache - uses: Swatinem/rust-cache@v2 - with: - shared-key: desktop-release-macos-${{ matrix.rust_target }} - workspaces: | - . - packages/desktop/src-tauri -> target + registry-url: 'https://npm.pkg.github.com' + scope: '@boudra' - name: Install JS dependencies run: npm ci env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Build web app for Tauri + - name: Set desktop package version from tag + shell: bash + run: | + node <<'NODE' + const fs = require('node:fs'); + const path = require('node:path'); + + const version = process.env.DESKTOP_VERSION; + if (!version) throw new Error('DESKTOP_VERSION env var is missing'); + + const packageJsonPath = path.join(process.env.DESKTOP_PACKAGE_PATH, 'package.json'); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + packageJson.version = version; + fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`); + NODE + + - name: Build web app for desktop run: npm run build:web --workspace=@getpaseo/app - - name: Build managed runtime - run: npm run prepare:managed-runtime --workspace=@getpaseo/desktop - - - name: Validate managed runtime bundle - run: npm run validate:managed-runtime --workspace=@getpaseo/desktop - - - name: Import Apple code-signing certificate - uses: apple-actions/import-codesign-certs@v3 - with: - p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }} - p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - - - name: Sign bundled managed runtime - env: - APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} - run: node ./packages/desktop/scripts/sign-managed-runtime-macos.mjs - - name: Detect existing GitHub release state if: env.IS_SMOKE_TAG != 'true' env: @@ -162,76 +118,36 @@ jobs: run: | set -euo pipefail if release_draft="$(gh release view "$RELEASE_TAG" --repo "${{ github.repository }}" --json isDraft --jq '.isDraft' 2>/dev/null)"; then - : + if [[ "$release_draft" == "true" ]]; then + release_type="draft" + else + release_type="release" + fi else - release_draft="false" + release_type="release" fi - echo "RELEASE_DRAFT=$release_draft" >> "$GITHUB_ENV" + echo "RELEASE_TYPE=$release_type" >> "$GITHUB_ENV" - - name: Build and publish macOS Tauri release - if: env.IS_SMOKE_TAG != 'true' - id: tauri_build - uses: tauri-apps/tauri-action@v0 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} - APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} - with: - projectPath: packages/desktop - tagName: ${{ env.RELEASE_TAG }} - releaseName: Paseo ${{ env.RELEASE_TAG }} - releaseBody: See the assets to download and install this version. - releaseDraft: ${{ env.RELEASE_DRAFT }} - prerelease: false - args: --target ${{ matrix.rust_target }} - - - name: Notarize and re-upload DMG - if: env.IS_SMOKE_TAG != 'true' - env: - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Build desktop release shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }} + CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + CSC_NAME: ${{ secrets.APPLE_SIGNING_IDENTITY }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} run: | set -euo pipefail - artifacts='${{ steps.tauri_build.outputs.artifactPaths }}' - dmg_path=$(echo "$artifacts" | jq -r '.[] | select(endswith(".dmg"))') - if [ -z "$dmg_path" ]; then - echo "::error::No DMG found in tauri build artifacts" - exit 1 + publish_mode="never" + publish_args=() + if [[ "$IS_SMOKE_TAG" != "true" ]]; then + publish_mode="always" + publish_args+=("-c.publish.releaseType=$RELEASE_TYPE") fi - echo "DMG: $dmg_path" - echo "Signing DMG..." - codesign --force --sign "$APPLE_SIGNING_IDENTITY" --timestamp "$dmg_path" - - echo "Submitting DMG for notarization..." - xcrun notarytool submit "$dmg_path" \ - --apple-id "$APPLE_ID" \ - --password "$APPLE_PASSWORD" \ - --team-id "$APPLE_TEAM_ID" \ - --wait - - echo "Stapling notarization ticket..." - xcrun stapler staple "$dmg_path" - - echo "Verifying..." - spctl --assess --type install --verbose "$dmg_path" - - echo "Replacing release asset with notarized DMG..." - gh release upload "$RELEASE_TAG" "$dmg_path" --repo "${{ github.repository }}" --clobber - - - name: Build macOS app (smoke only) - if: env.IS_SMOKE_TAG == 'true' - run: npm run tauri --workspace=@getpaseo/desktop build -- --target ${{ matrix.rust_target }} --no-bundle + npm run build --workspace="$DESKTOP_WORKSPACE" -- --publish "$publish_mode" --mac --${{ matrix.electron_arch }} "${publish_args[@]}" publish-linux: if: ${{ (github.event_name == 'workflow_dispatch' && (github.event.inputs.platform == 'all' || github.event.inputs.platform == 'linux')) || (github.event_name == 'push' && (startsWith(github.ref_name, 'v') || startsWith(github.ref_name, 'desktop-v') || startsWith(github.ref_name, 'desktop-linux-v'))) }} @@ -267,89 +183,38 @@ jobs: echo "IS_SMOKE_TAG=false" >> "$GITHUB_ENV" fi - - name: Set desktop version from tag - shell: bash - run: | - node <<'NODE' - const fs = require('node:fs'); - const path = require('node:path'); - const version = process.env.DESKTOP_VERSION; - if (!version) throw new Error('DESKTOP_VERSION env var is missing'); - console.log(`Setting desktop version to ${version}`); - - const tauriConfPath = path.join('packages', 'desktop', 'src-tauri', 'tauri.conf.json'); - const tauriConfText = fs.readFileSync(tauriConfPath, 'utf8'); - const tauriRe = /("version"\s*:\s*")([^"]+)(")/; - if (!tauriRe.test(tauriConfText)) throw new Error('Failed to find version in tauri.conf.json'); - fs.writeFileSync(tauriConfPath, tauriConfText.replace(tauriRe, `$1${version}$3`)); - - const cargoTomlPath = path.join('packages', 'desktop', 'src-tauri', 'Cargo.toml'); - const lines = fs.readFileSync(cargoTomlPath, 'utf8').split(/\r?\n/); - let inPackage = false, updated = false; - const result = lines.map((line) => { - if (/^\[package\]\s*$/.test(line)) inPackage = true; - else if (inPackage && /^\[/.test(line)) inPackage = false; - if (inPackage && /^version\s*=\s*".*"\s*$/.test(line)) { - updated = true; - return `version = "${version}"`; - } - return line; - }); - if (!updated) throw new Error('Failed to update Cargo.toml version'); - fs.writeFileSync(cargoTomlPath, result.join('\n') + '\n'); - NODE - - - name: Install Linux packaging dependencies - run: | - sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libappindicator3-dev librsvg2-dev patchelf libfuse2 - - name: Setup Node uses: actions/setup-node@v4 with: - node-version: "22" - cache: "npm" + node-version: '22' + cache: 'npm' cache-dependency-path: package-lock.json - registry-url: "https://npm.pkg.github.com" - scope: "@boudra" - - - name: Install Rust stable - uses: dtolnay/rust-toolchain@stable - - - name: Restore Rust cache - uses: Swatinem/rust-cache@v2 - with: - shared-key: desktop-release-linux - workspaces: | - . - packages/desktop/src-tauri -> target + registry-url: 'https://npm.pkg.github.com' + scope: '@boudra' - name: Install JS dependencies run: npm ci env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Build web app for Tauri - run: npm run build:web --workspace=@getpaseo/app - - - name: Build managed runtime - run: npm run prepare:managed-runtime --workspace=@getpaseo/desktop - - - name: Validate managed runtime bundle - run: npm run validate:managed-runtime --workspace=@getpaseo/desktop - - - name: Strip CUDA dependencies from onnxruntime + - name: Set desktop package version from tag shell: bash run: | - find packages/desktop/src-tauri/resources/managed-runtime -path '*/onnxruntime-node/bin/*' \( -name '*cuda*' -o -name '*tensorrt*' \) -delete || true - # Remove CUDA shared library references from onnxruntime .so files so linuxdeploy - # doesn't try to bundle them (they're optional runtime deps, not needed for CPU inference) - for f in $(find packages/desktop/src-tauri/resources/managed-runtime -path '*/onnxruntime-node/bin/*' \( -name '*.so' -o -name '*.so.*' \)); do - for lib in $(patchelf --print-needed "$f" 2>/dev/null | grep -iE 'cublas|cudnn|cudart|cufft|curand|cusolver|cusparse|nccl|nvrtc|tensorrt|nvinfer'); do - echo "Removing needed $lib from $f" - patchelf --remove-needed "$lib" "$f" - done - done + node <<'NODE' + const fs = require('node:fs'); + const path = require('node:path'); + + const version = process.env.DESKTOP_VERSION; + if (!version) throw new Error('DESKTOP_VERSION env var is missing'); + + const packageJsonPath = path.join(process.env.DESKTOP_PACKAGE_PATH, 'package.json'); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + packageJson.version = version; + fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`); + NODE + + - name: Build web app for desktop + run: npm run build:web --workspace=@getpaseo/app - name: Detect existing GitHub release state if: env.IS_SMOKE_TAG != 'true' @@ -359,141 +224,30 @@ jobs: run: | set -euo pipefail if release_draft="$(gh release view "$RELEASE_TAG" --repo "${{ github.repository }}" --json isDraft --jq '.isDraft' 2>/dev/null)"; then - : + if [[ "$release_draft" == "true" ]]; then + release_type="draft" + else + release_type="release" + fi else - release_draft="false" + release_type="release" fi - echo "RELEASE_DRAFT=$release_draft" >> "$GITHUB_ENV" + echo "RELEASE_TYPE=$release_type" >> "$GITHUB_ENV" - - name: Build Linux Tauri release - if: env.IS_SMOKE_TAG != 'true' - id: linux_tauri - continue-on-error: true - env: - TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} - NO_STRIP: "1" - APPIMAGE_EXTRACT_AND_RUN: "1" + - name: Build desktop release shell: bash - run: | - set -euo pipefail - npm run tauri --workspace=@getpaseo/desktop build -- --bundles appimage - - - name: Attempt manual Linux AppImage fallback - if: env.IS_SMOKE_TAG != 'true' && steps.linux_tauri.outcome == 'failure' - env: - TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} - shell: bash - run: | - set -euxo pipefail - appimage_dir="packages/desktop/src-tauri/target/release/bundle/appimage" - appdir_path="$appimage_dir/Paseo.AppDir" - canonical_appimage="$appimage_dir/Paseo_${DESKTOP_VERSION}_amd64.AppImage" - existing_appimage="$(find "$appimage_dir" -maxdepth 1 -type f -name '*.AppImage' | head -n 1)" - if [ -n "$existing_appimage" ]; then - if [ "$existing_appimage" != "$canonical_appimage" ]; then - mv "$existing_appimage" "$canonical_appimage" - fi - if [ ! -f "$canonical_appimage.sig" ]; then - npx tauri signer sign "$canonical_appimage" - fi - exit 0 - fi - if [ ! -d "$appdir_path" ]; then - echo "::error::AppDir was not generated at $appdir_path" - exit 1 - fi - cp --remove-destination "$appdir_path/usr/share/applications/Paseo.desktop" "$appdir_path/Paseo.desktop" - cp --remove-destination "$appdir_path/Paseo.png" "$appdir_path/.DirIcon" - env | sort | grep -E '^(APPIMAGE|DESKTOP_VERSION|NO_STRIP|RELEASE_TAG|SOURCE_TAG|TAURI_)' || true - tools_dir="$(mktemp -d)" - curl -fsSL https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage -o "$tools_dir/appimagetool-x86_64.AppImage" - chmod +x "$tools_dir/appimagetool-x86_64.AppImage" - ARCH=x86_64 APPIMAGE_EXTRACT_AND_RUN=1 "$tools_dir/appimagetool-x86_64.AppImage" "$appdir_path" "$canonical_appimage" - npx tauri signer sign "$canonical_appimage" - - - name: Fail Linux release when AppImage bundling fails - if: env.IS_SMOKE_TAG != 'true' && steps.linux_tauri.outcome == 'failure' - shell: bash - run: | - set -euo pipefail - shopt -s nullglob - assets=( - packages/desktop/src-tauri/target/release/bundle/appimage/*.AppImage - packages/desktop/src-tauri/target/release/bundle/appimage/*.AppImage.sig - ) - if [ "${#assets[@]}" -eq 0 ]; then - echo "::error::Linux AppImage assets are still missing after the manual fallback." - exit 1 - fi - - - name: Patch AppImage for Wayland compatibility - if: env.IS_SMOKE_TAG != 'true' - env: - TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} - APPIMAGE_EXTRACT_AND_RUN: "1" - shell: bash - run: | - set -euo pipefail - appimage_dir="$GITHUB_WORKSPACE/packages/desktop/src-tauri/target/release/bundle/appimage" - appimage="$(find "$appimage_dir" -maxdepth 1 -type f -name '*.AppImage' | head -n 1)" - if [ -z "$appimage" ]; then - echo "::error::No AppImage found to patch" - exit 1 - fi - echo "Patching $appimage for Wayland compatibility..." - - # Extract the AppImage - workdir="$(mktemp -d)" - cd "$workdir" - "$appimage" --appimage-extract - - # Remove GDK_BACKEND=x11 from the GTK plugin hook. - # The bundled libgdk-3.so has Wayland support built in; forcing x11 - # prevents the app from starting on Wayland-only systems. - gtk_hook="squashfs-root/apprun-hooks/linuxdeploy-plugin-gtk.sh" - if [ -f "$gtk_hook" ]; then - sed -i 's/^export GDK_BACKEND=x11/#export GDK_BACKEND=x11/' "$gtk_hook" - echo "Patched $gtk_hook — removed GDK_BACKEND=x11" - fi - - # Repackage - tools_dir="$(mktemp -d)" - curl -fsSL https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage \ - -o "$tools_dir/appimagetool" - chmod +x "$tools_dir/appimagetool" - ARCH=x86_64 "$tools_dir/appimagetool" squashfs-root "$appimage" - - # Re-sign - rm -f "$appimage.sig" - cd "$GITHUB_WORKSPACE" - npx tauri signer sign "$appimage" - echo "Repackaged and re-signed $appimage" - - - name: Upload Linux release assets - if: env.IS_SMOKE_TAG != 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - shell: bash run: | set -euo pipefail - shopt -s nullglob - assets=( - packages/desktop/src-tauri/target/release/bundle/appimage/*.AppImage - packages/desktop/src-tauri/target/release/bundle/appimage/*.AppImage.sig - ) - if [ "${#assets[@]}" -eq 0 ]; then - echo "::error::No Linux AppImage assets were produced." - exit 1 + publish_mode="never" + publish_args=() + if [[ "$IS_SMOKE_TAG" != "true" ]]; then + publish_mode="always" + publish_args+=("-c.publish.releaseType=$RELEASE_TYPE") fi - printf 'Uploading Linux assets:\n%s\n' "${assets[@]}" - gh release upload "$RELEASE_TAG" "${assets[@]}" --repo "${{ github.repository }}" --clobber - - name: Build Linux app (smoke only) - if: env.IS_SMOKE_TAG == 'true' - run: npm run tauri --workspace=@getpaseo/desktop build -- --no-bundle + npm run build --workspace="$DESKTOP_WORKSPACE" -- --publish "$publish_mode" --linux --x64 "${publish_args[@]}" publish-windows: if: ${{ (github.event_name == 'workflow_dispatch' && (github.event.inputs.platform == 'all' || github.event.inputs.platform == 'windows')) || (github.event_name == 'push' && (startsWith(github.ref_name, 'v') || startsWith(github.ref_name, 'desktop-v') || startsWith(github.ref_name, 'desktop-windows-v'))) }} @@ -529,76 +283,43 @@ jobs: echo "IS_SMOKE_TAG=false" >> "$GITHUB_ENV" fi - - name: Set desktop version from tag - shell: bash - run: | - node <<'NODE' - const fs = require('node:fs'); - const path = require('node:path'); - const version = process.env.DESKTOP_VERSION; - if (!version) throw new Error('DESKTOP_VERSION env var is missing'); - console.log(`Setting desktop version to ${version}`); - - const tauriConfPath = path.join('packages', 'desktop', 'src-tauri', 'tauri.conf.json'); - const tauriConfText = fs.readFileSync(tauriConfPath, 'utf8'); - const tauriRe = /("version"\s*:\s*")([^"]+)(")/; - if (!tauriRe.test(tauriConfText)) throw new Error('Failed to find version in tauri.conf.json'); - fs.writeFileSync(tauriConfPath, tauriConfText.replace(tauriRe, `$1${version}$3`)); - - const cargoTomlPath = path.join('packages', 'desktop', 'src-tauri', 'Cargo.toml'); - const lines = fs.readFileSync(cargoTomlPath, 'utf8').split(/\r?\n/); - let inPackage = false, updated = false; - const result = lines.map((line) => { - if (/^\[package\]\s*$/.test(line)) inPackage = true; - else if (inPackage && /^\[/.test(line)) inPackage = false; - if (inPackage && /^version\s*=\s*".*"\s*$/.test(line)) { - updated = true; - return `version = "${version}"`; - } - return line; - }); - if (!updated) throw new Error('Failed to update Cargo.toml version'); - fs.writeFileSync(cargoTomlPath, result.join('\n') + '\n'); - NODE - - name: Setup Node uses: actions/setup-node@v4 with: - node-version: "22" - cache: "npm" + node-version: '22' + cache: 'npm' cache-dependency-path: package-lock.json - registry-url: "https://npm.pkg.github.com" - scope: "@boudra" - - - name: Install Rust stable - uses: dtolnay/rust-toolchain@stable - - - name: Restore Rust cache - uses: Swatinem/rust-cache@v2 - with: - shared-key: desktop-release-windows - workspaces: | - . - packages/desktop/src-tauri -> target + registry-url: 'https://npm.pkg.github.com' + scope: '@boudra' - name: Install JS dependencies run: npm ci env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Build web app for Tauri + - name: Set desktop package version from tag + shell: bash + run: | + node <<'NODE' + const fs = require('node:fs'); + const path = require('node:path'); + + const version = process.env.DESKTOP_VERSION; + if (!version) throw new Error('DESKTOP_VERSION env var is missing'); + + const packageJsonPath = path.join(process.env.DESKTOP_PACKAGE_PATH, 'package.json'); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + packageJson.version = version; + fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`); + NODE + + - name: Build web app for desktop shell: pwsh run: | $patchPath = (Get-Item "$env:GITHUB_WORKSPACE/scripts/metro-config-windows-loader-patch.cjs").FullName $env:NODE_OPTIONS = "--require=$patchPath" npm run build:web --workspace=@getpaseo/app - - name: Build managed runtime - run: npm run prepare:managed-runtime --workspace=@getpaseo/desktop - - - name: Validate managed runtime bundle - run: npm run validate:managed-runtime --workspace=@getpaseo/desktop - - name: Detect existing GitHub release state if: env.IS_SMOKE_TAG != 'true' env: @@ -607,31 +328,27 @@ jobs: run: | set -euo pipefail if release_draft="$(gh release view "$RELEASE_TAG" --repo "${{ github.repository }}" --json isDraft --jq '.isDraft' 2>/dev/null)"; then - : + if [[ "$release_draft" == "true" ]]; then + release_type="draft" + else + release_type="release" + fi else - release_draft="false" + release_type="release" fi - echo "RELEASE_DRAFT=$release_draft" >> "$GITHUB_ENV" + echo "RELEASE_TYPE=$release_type" >> "$GITHUB_ENV" - - name: Build and publish Windows Tauri release - if: env.IS_SMOKE_TAG != 'true' - uses: tauri-apps/tauri-action@v0 + - name: Build desktop release + shell: bash env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} - with: - projectPath: packages/desktop - tagName: ${{ env.RELEASE_TAG }} - releaseName: Paseo ${{ env.RELEASE_TAG }} - releaseBody: See the assets to download and install this version. - releaseDraft: ${{ env.RELEASE_DRAFT }} - prerelease: false - args: --bundles nsis + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + publish_mode="never" + publish_args=() + if [[ "$IS_SMOKE_TAG" != "true" ]]; then + publish_mode="always" + publish_args+=("-c.publish.releaseType=$RELEASE_TYPE") + fi - - name: Build Windows app (smoke only) - if: env.IS_SMOKE_TAG == 'true' - run: npm run tauri --workspace=@getpaseo/desktop build -- --no-bundle - env: - TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + npm run build --workspace="$DESKTOP_WORKSPACE" -- --publish "$publish_mode" --win --x64 "${publish_args[@]}" diff --git a/.gitignore b/.gitignore index e37d4f1e6..f5c8aac8e 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,9 @@ test-results/ # Vercel .vercel/ +# Expo +.expo/ + # Misc *.pem .vercel diff --git a/CHANGELOG.md b/CHANGELOG.md index 926d0787d..dedd1a7c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -166,7 +166,7 @@ - Redesigned the website get-started experience into a clearer two-step flow. - Simplified website GitHub navigation and changelog headings. - Improved app draft/new-agent UX with clearer working directory placeholder and empty-state messaging. -- Enabled drag interactions in previously unhandled areas on the desktop (Tauri) draft screen. +- Enabled drag interactions in previously unhandled areas on the desktop draft screen. - Hid empty filter groups in the left sidebar. ### Fixed @@ -188,7 +188,7 @@ - Improved new worktree-agent defaults by prefilling CWD to the main repository. - Improved desktop command autocomplete behavior to match combobox interactions. - Improved git sync UX by simplifying sync labels and only showing Sync when a branch diverges from origin. -- Improved desktop settings and permissions UX in Tauri. +- Improved desktop settings and permissions UX on desktop. - Improved scrollbar visibility, drag interactions, tracking, and animation timing on web/desktop. ### Fixed @@ -227,7 +227,7 @@ - Fixed stuck "send while running" recovery across app and server session handling. - Fixed Claude session identity preservation when reloading existing agents. - Fixed combobox option behavior and related interactions. -- Fixed Tauri file-drop listener cleanup to avoid uncaught unlisten errors. +- Fixed desktop file-drop listener cleanup to avoid uncaught unlisten errors. - Fixed web tool-detail wheel event routing at scroll edges. ## 0.1.7 - 2026-02-16 diff --git a/CLAUDE.md b/CLAUDE.md index 41483ba6f..9035fe370 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,7 +12,7 @@ This is an npm workspace monorepo: - `packages/app` — Mobile + web client (Expo) - `packages/cli` — Docker-style CLI (`paseo run/ls/logs/wait`) - `packages/relay` — E2E encrypted relay for remote access -- `packages/desktop` — Tauri desktop wrapper +- `packages/desktop` — Electron desktop wrapper - `packages/website` — Marketing site (paseo.sh) ## Documentation diff --git a/README.md b/README.md index 38cdd0fbf..0c3ecc4a0 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ Quick monorepo package map: - `packages/server`: Paseo daemon (agent process orchestration, WebSocket API, MCP server) - `packages/app`: Expo client (iOS, Android, web) - `packages/cli`: `paseo` CLI for daemon and agent workflows -- `packages/desktop`: Tauri desktop app +- `packages/desktop`: Electron desktop app - `packages/relay`: Relay package for remote connectivity - `packages/website`: Marketing site and documentation (`paseo.sh`) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 8a2334ce7..c2f29dcf0 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -9,7 +9,7 @@ Your code never leaves your machine. Paseo is local-first. ``` ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ Mobile App │ │ CLI │ │ Desktop App │ -│ (Expo) │ │ (Commander) │ │ (Tauri) │ +│ (Expo) │ │ (Commander) │ │ (Electron) │ └──────┬───────┘ └──────┬──────┘ └──────┬──────┘ │ │ │ │ WebSocket │ WebSocket │ Managed subprocess @@ -90,9 +90,9 @@ Enables remote access when the daemon is behind a firewall. See [SECURITY.md](../SECURITY.md) for the full threat model. -### `packages/desktop` — Desktop app (Tauri) +### `packages/desktop` — Desktop app (Electron) -Tauri wrapper for macOS, Linux, and Windows. +Electron wrapper for macOS, Linux, and Windows. - Can spawn the daemon as a managed subprocess - Native file access for workspace integration @@ -180,5 +180,5 @@ $PASEO_HOME/ ## Deployment models 1. **Local daemon** (default): `paseo daemon start` on `127.0.0.1:6767` -2. **Managed desktop**: Tauri app spawns daemon as subprocess +2. **Managed desktop**: Electron app spawns daemon as subprocess 3. **Remote + relay**: Daemon behind firewall, relay bridges with E2E encryption diff --git a/docs/ELECTRON_MIGRATION.md b/docs/ELECTRON_MIGRATION.md deleted file mode 100644 index 5c212ee07..000000000 --- a/docs/ELECTRON_MIGRATION.md +++ /dev/null @@ -1,192 +0,0 @@ -# Electron Migration - -## Why switch - -Paseo Desktop is a webview that loads an Expo web build plus a Node daemon that manages AI coding agents. Tauri is designed for apps that need a thin native wrapper — it assumes your backend logic lives in Rust. Paseo's backend is entirely Node/TypeScript. The result is that we're reimplementing Electron's built-in Node bundling inside Tauri's resource system. - -### Pain points with Tauri today - -**Managed runtime bundling is the #1 source of complexity.** `build-managed-runtime.mjs` (590 lines) downloads a Node distribution, packs three workspace tarballs, installs them with the bundled npm, separately installs sherpa-onnx platform packages, prunes ONNX/node-pty/ripgrep binaries per platform, and writes a runtime manifest. This exists solely because Tauri doesn't ship Node. - -**Linux is fragile.** We wrote a custom AppImage Wayland patch (`maybePatchLinuxAddonRunpath` in sherpa-onnx-node-loader, plus AppImage patching scripts) because Tauri's Linux webview has Wayland issues. Electron handles Wayland/X11 transparently via Chromium's `--ozone-platform-hint=auto`. - -**Native notification callbacks required Objective-C.** We wrote `paseo_notifications.m` (UNUserNotificationCenter delegate bridge) and compiled it into the Tauri build just to get notification click callbacks on macOS. Linux and Windows notification callbacks are unimplemented. Electron's `Notification` API provides click/reply/action callbacks cross-platform with zero native code. - -**Windows is undertested.** The Rust code has `#[cfg(windows)]` blocks for named pipes, PowerShell `Expand-Archive`, `CREATE_NO_WINDOW` flags, NSIS installer hooks — all written speculatively with limited testing. Electron's cross-platform abstractions cover all of this. - -**IPC is boilerplate-heavy.** Every desktop command requires a Rust `#[tauri::command]` function, a `spawn_blocking` wrapper, serde types, registration in `generate_handler![]`, and a TypeScript `invokeDesktopCommand()` call. There are 22 commands today. In Electron, the main process is Node — these become direct function calls or simple `ipcMain.handle()` handlers in the same language as the rest of the codebase. - -**Tauri plugin gaps.** Features like tray menus with dynamic updates, global shortcuts, deep links, system idle detection, auto-launch on login, and rich clipboard access all require Rust plugins (often per-platform). Electron provides these as JS API calls. - -### What we gain - -- **Delete `build-managed-runtime.mjs`** — Node comes with Electron -- **Delete the Wayland/AppImage patching** — Chromium handles it -- **Delete `paseo_notifications.m`** — `Notification` API works cross-platform -- **Delete `runtime_manager.rs`** (1080 lines) — daemon becomes a `child_process.fork()` call -- **Delete all Rust code** — `lib.rs` (683 lines), `desktop_notifications.rs`, `build.rs`, Cargo.toml dependencies -- **Delete sherpa rpath patching** — `electron-rebuild` handles native addon ABI matching -- **One language** for the entire desktop app (TypeScript) -- **Windows support** works out of the box -- **Mature auto-update** via electron-updater (GitHub Releases, S3, etc.) -- **Mature code signing** for macOS and Windows without custom scripts - -### What we lose - -- **App size increases** from ~20 MB to ~150-200 MB (ships Chromium) -- **Memory footprint increases** (Chromium process model) -- **Tauri's Rust escape hatch** — though we only use Rust because Tauri requires it, not because we need it - -### ONNX / Sherpa native addons - -`sherpa-onnx-node` and `onnxruntime-node` ship prebuilt `.node` addons per platform. In Electron, `@electron/rebuild` rebuilds or verifies native addons against Electron's Node ABI. This is standard practice — packages like `better-sqlite3`, `sharp`, and `node-pty` all work this way. The manual sherpa platform package installation and rpath patching goes away entirely. - -## What can be collapsed - -### Build infrastructure (delete entirely) - -| File | Lines | Purpose | -|---|---|---| -| `packages/desktop/scripts/build-managed-runtime.mjs` | 590 | Bundle Node + workspaces into Tauri sidecar | -| `packages/desktop/scripts/validate-managed-runtime.mjs` | ~100 | Validate bundled runtime integrity | -| `packages/desktop/scripts/sign-managed-runtime-macos.mjs` | ~100 | Code-sign macOS sidecar bundles | -| `packages/desktop/scripts/managed-daemon-smoke.mjs` | ~50 | Smoke test for managed daemon | - -Electron replacement: None. Node is part of Electron. Workspace code loads directly via `require`/`import`. - -### Rust backend (delete entirely) - -| File | Lines | Purpose | -|---|---|---| -| `src-tauri/src/lib.rs` | 683 | App setup, IPC commands, menu, zoom | -| `src-tauri/src/runtime_manager.rs` | 1080 | Managed runtime/daemon lifecycle | -| `src-tauri/src/desktop_notifications.rs` | ~80 | Notification bridge | -| `src-tauri/src/main.rs` | ~10 | Entry point | -| `src-tauri/build.rs` | ~30 | Build script (compiles ObjC) | -| `src-tauri/Cargo.toml` | ~30 | Rust dependencies | -| `src-tauri/tauri.conf.json` | ~100 | Tauri configuration | - -Electron replacement: A single `main.ts` (~200-300 lines) covering window creation, menu, IPC handlers, and daemon lifecycle. - -### Native platform code (delete entirely) - -| File | Purpose | -|---|---| -| `src-tauri/macos/paseo_notifications.h` | ObjC notification bridge header | -| `src-tauri/macos/paseo_notifications.m` | ObjC UNUserNotificationCenter delegate | -| `src-tauri/Info.plist` | macOS bundle config | -| `src-tauri/Entitlements.plist` | macOS entitlements | -| `src-tauri/installer-hooks.nsh` | Windows NSIS post-install hooks | - -Electron replacement: `Notification` API (2-3 lines per notification). macOS entitlements handled by `electron-builder` config. - -### Daemon management (massive simplification) - -**Tauri today:** Bundled Node binary → spawn as child process → pipe stdout/stderr → poll `daemon status --json` in a loop → parse JSON → return via Tauri IPC. - -**Electron:** `child_process.fork('./packages/server/dist/scripts/daemon-runner.js')` — same Node runtime, same ABI, direct IPC via Node's built-in channel. - -### Local daemon transport (simplification) - -**Tauri today:** `runtime_manager.rs` implements a full WebSocket client in Rust (tokio-tungstenite) with session management, base64 binary encoding over Tauri events, and manual read/write task spawning. ~300 lines of Rust. - -**Electron:** The renderer can connect to Unix sockets / named pipes directly via the main process using Node's `ws` package (already a dependency). Or use `ipcMain` to proxy — still TypeScript, still ~50 lines. - -### Attachment file management (simplification) - -**Tauri today:** 6 Rust commands (`write_attachment_base64`, `copy_attachment_file`, `read_file_base64`, `delete_attachment_file`, `garbage_collect_attachment_files`) with path validation, base64 encoding/decoding, and directory traversal protection. ~200 lines of Rust. - -**Electron:** Same logic in TypeScript using `fs` and `app.getPath('userData')`. ~50 lines. - -### Window management (simplification) - -**Tauri today:** Custom `tauri-window.ts` with `plugin:window|start_dragging`, `plugin:window|toggle_maximize`, `plugin:window|is_fullscreen` invocations. - -**Electron:** `BrowserWindow` API — `win.maximize()`, `win.isFullScreen()`, etc. Frameless window dragging via CSS `-webkit-app-region: drag`. - -### App updates (simplification) - -**Tauri today:** Rust `check_app_update` / `install_app_update` commands using `tauri_plugin_updater`. Custom update check UI in the renderer. - -**Electron:** `electron-updater` with `autoUpdater.checkForUpdatesAndNotify()`. Built-in download progress, restart handling, differential updates. - -## Migration plan - -### Guiding principle - -Electron is mature. We are not doing anything new. Every feature we need has official documentation and established patterns. Never use workarounds — if something doesn't work, find the official way. - -### Phase 1: Scaffold in parallel - -Create `packages/desktop-electron` alongside `packages/desktop` (Tauri). Both coexist. - -- Initialize with `electron-forge` or `electron-builder` -- Configure `BrowserWindow` to load the Expo web build (same as Tauri's webview) -- Set up `electron-builder` for macOS, Linux, Windows packaging -- Get a window rendering the Expo web app with no desktop features - -### Phase 2: Introduce `isElectron` branching - -Add platform detection infrastructure in `packages/app`: - -- Add `isElectron()` / `isElectronMac()` alongside existing `isTauri()` / `isTauriMac()` -- Introduce a `isDesktop()` helper that returns `isTauri() || isElectron()` -- Create `packages/app/src/desktop/electron/invoke-desktop-command.ts` — Electron's equivalent of the Tauri invoke bridge, using `ipcRenderer.invoke()` -- Update `invokeDesktopCommand()` to route to the correct backend based on platform - -### Phase 3: Port features incrementally - -Work through each Tauri feature, adding the Electron variant. Never break Tauri while doing this. - -**Tier 1 — Core (must work for the app to be usable):** - -1. **Daemon lifecycle** — `child_process.fork()` the daemon runner from the main process. Expose `start/stop/restart/status` via `ipcMain.handle()`. -2. **Local daemon transport** — WebSocket client in the main process using `ws`, proxied to renderer via IPC. -3. **Window management** — Frameless window with overlay title bar, drag regions, zoom controls, fullscreen. -4. **Menu** — `Menu.buildFromTemplate()` with zoom controls and macOS app menu. - -**Tier 2 — Desktop features:** - -5. **Notifications** — `new Notification()` with click handlers. -6. **App updates** — `electron-updater` with GitHub Releases. -7. **File dialogs** — `dialog.showOpenDialog()` for directory picker. -8. **Confirm dialogs** — `dialog.showMessageBox()`. -9. **Open external URLs** — `shell.openExternal()`. -10. **Attachment file management** — `fs` operations in main process via IPC. - -**Tier 3 — Polish:** - -11. **CLI symlink instructions** — Generate platform-specific instructions in Node. -12. **Logging** — `electron-log` or simple file-based logging. -13. **Single instance** — `app.requestSingleInstanceLock()`. -14. **Console forwarding** — No bridge needed, main process is already Node. -15. **NSIS installer hooks** — `electron-builder` NSIS config for CLI symlink. - -### Phase 4: Feature parity validation - -- Test all features on macOS, Linux (X11 + Wayland), Windows -- Verify sherpa-onnx-node loads correctly via `@electron/rebuild` -- Verify daemon start/stop/restart lifecycle -- Verify auto-update flow -- Verify notification callbacks on all platforms -- Smoke test CLI passthrough mode - -### Phase 5: Cut over - -- Update CI to build Electron packages instead of Tauri -- Remove `packages/desktop` (Tauri) -- Remove all `isTauri()` branching, keep only `isElectron()` (rename to `isDesktop()`) -- Delete `src-tauri/` and all Rust code -- Delete `build-managed-runtime.mjs` and related scripts -- Delete sherpa rpath patching code - -## Estimated code impact - -| Category | Lines removed | Lines added | -|---|---|---| -| Rust backend | ~1,900 | 0 | -| ObjC native code | ~150 | 0 | -| Build scripts | ~850 | 0 | -| Tauri TypeScript bridge | ~300 | ~150 (Electron IPC) | -| Electron main process | 0 | ~300 | -| **Net** | **~3,200 removed** | **~450 added** | diff --git a/docs/PANEL-REFACTOR-PLAN.md b/docs/PANEL-REFACTOR-PLAN.md deleted file mode 100644 index fdf9feafc..000000000 --- a/docs/PANEL-REFACTOR-PLAN.md +++ /dev/null @@ -1,341 +0,0 @@ -# Panel Interface Refactor Plan - -**Goal:** Replace the hardcoded panel switch statements with a registry-based panel interface. This is a pure refactor — all product surfaces stay identical. The motivation is to prepare for split panes (VSCode-style), where each split independently renders panels. - -## The Problem - -The workspace screen (`packages/app/src/screens/workspace/workspace-screen.tsx`, ~2084 lines) has a `renderContent()` function (line 1437) that switches on `target.kind` to render each panel type with bespoke props. The same pattern repeats in: - -- `workspace-tab-model.ts` — switches on `target.kind` to build tab descriptors (labels, subtitles, status) -- `workspace-tab-presentation.tsx` — switches on kind for icons and status indicators - -Every new panel type requires editing 3+ files. This must become a registry where panels self-register. - -## Target Architecture - -### 1. PanelRegistration Interface - -```typescript -// panels/panel-registry.ts - -interface PanelDescriptor { - label: string; - subtitle: string; - titleState: "ready" | "loading"; - icon: React.ComponentType<{ size: number; color: string }>; - statusBucket: SidebarStateBucket | null; -} - -interface PanelRegistration { - kind: K; - component: React.ComponentType; - useDescriptor( - target: Extract, - context: { serverId: string; workspaceId: string }, - ): PanelDescriptor; - confirmClose?( - target: Extract, - context: { serverId: string; workspaceId: string }, - ): Promise; -} -``` - -### 2. Panel Registry - -```typescript -const panelRegistry = new Map(); - -function registerPanel(registration: PanelRegistration): void { - panelRegistry.set(registration.kind, registration); -} - -function getPanelRegistration(kind: string): PanelRegistration | undefined { - return panelRegistry.get(kind); -} -``` - -### 3. PaneContext - -Every panel gets workspace-level context via `usePaneContext()`. No prop drilling of serverId/workspaceId through panel-specific props. - -```typescript -interface PaneContextValue { - serverId: string; - workspaceId: string; - tabId: string; - target: WorkspaceTabTarget; - openTab(target: WorkspaceTabTarget): void; - closeCurrentTab(): void; - retargetCurrentTab(target: WorkspaceTabTarget): void; - openFileInWorkspace(filePath: string): void; -} -``` - -### 4. WorkspaceTabTarget stays unchanged - -```typescript -type WorkspaceTabTarget = - | { kind: "draft"; draftId: string } - | { kind: "agent"; agentId: string } - | { kind: "terminal"; terminalId: string } - | { kind: "file"; path: string }; -``` - -No store migration needed. `serverId` and `workspaceId` come from the pane context, not the target. - -## Panel Implementations - -Each panel type gets its own file that exports a `PanelRegistration`. Panels use `usePaneContext()` for workspace-level context and read their own data from stores directly. - -### Agent Panel Example - -```typescript -// panels/agent-panel.ts - -function useAgentPanelDescriptor( - target: { kind: "agent"; agentId: string }, - context: { serverId: string }, -): PanelDescriptor { - const agent = useSessionStore( - (s) => s.agentsByServer.get(context.serverId)?.get(target.agentId) ?? null, - ); - const provider = agent?.provider ?? "codex"; - const label = resolveAgentLabel(agent?.title); - return { - label: label ?? "", - subtitle: `${formatProviderLabel(provider)} agent`, - titleState: label ? "ready" : "loading", - icon: agentIconForProvider(provider), - statusBucket: agent ? deriveAgentStatusBucket(agent) : null, - }; -} - -function AgentPanel() { - const { serverId, target, openFileInWorkspace } = usePaneContext(); - invariant(target.kind === "agent", "AgentPanel requires agent target"); - return ( - - ); -} - -export const agentPanelRegistration: PanelRegistration<"agent"> = { - kind: "agent", - component: AgentPanel, - useDescriptor: useAgentPanelDescriptor, - async confirmClose(target, context) { - const agent = useSessionStore.getState().agentsByServer.get(context.serverId)?.get(target.agentId); - if (agent?.status === "running") { - return confirmDialog({ title: "Agent is still running. Close anyway?" }); - } - return true; - }, -}; -``` - -### Terminal Panel Example - -```typescript -function useTerminalPanelDescriptor( - target: { kind: "terminal"; terminalId: string }, - _context: { serverId: string; workspaceId: string }, -): PanelDescriptor { - // read terminal data from appropriate store - return { - label: "Terminal", - subtitle: "Terminal", - titleState: "ready", - icon: TerminalIcon, - statusBucket: null, - }; -} - -function TerminalPanel() { - const { serverId, workspaceId, target, openTab } = usePaneContext(); - invariant(target.kind === "terminal", "TerminalPanel requires terminal target"); - return ( - { - if (terminalId) { - openTab({ kind: "terminal", terminalId }); - } - }} - hideHeader - manageTerminalDirectorySubscription={false} - /> - ); -} -``` - -### Draft Panel Example - -```typescript -function useDraftPanelDescriptor( - _target: { kind: "draft"; draftId: string }, - _context: { serverId: string; workspaceId: string }, -): PanelDescriptor { - return { - label: "New Agent", - subtitle: "New Agent", - titleState: "ready", - icon: PencilIcon, - statusBucket: null, - }; -} - -function DraftPanel() { - const { serverId, workspaceId, tabId, target, openFileInWorkspace, retargetCurrentTab } = usePaneContext(); - invariant(target.kind === "draft", "DraftPanel requires draft target"); - return ( - { - retargetCurrentTab({ kind: "agent", agentId: agentSnapshot.id }); - }} - /> - ); -} -``` - -### File Panel Example - -```typescript -function useFilePanelDescriptor( - target: { kind: "file"; path: string }, - _context: { serverId: string; workspaceId: string }, -): PanelDescriptor { - const fileName = target.path.split("/").filter(Boolean).pop() ?? target.path; - return { - label: fileName, - subtitle: target.path, - titleState: "ready", - icon: FileTextIcon, - statusBucket: null, - }; -} - -function FilePanel() { - const { serverId, workspaceId, target } = usePaneContext(); - invariant(target.kind === "file", "FilePanel requires file target"); - return ( - - ); -} -``` - -## How the Tab Bar Uses It - -Each tab chip calls the panel's `useDescriptor` hook: - -```typescript -function TabChip({ tabId, target, serverId, workspaceId }: { - tabId: string; - target: WorkspaceTabTarget; - serverId: string; - workspaceId: string; -}) { - const registration = getPanelRegistration(target.kind); - invariant(registration, `No panel registration for kind: ${target.kind}`); - const descriptor = registration.useDescriptor(target, { serverId, workspaceId }); - return ( - } - statusBucket={descriptor.statusBucket} - /> - ); -} -``` - -## How the Workspace Screen Renders Content - -Replaces the entire `renderContent()` switch: - -```typescript -function PaneContent({ tabId, target, serverId, workspaceId }: { - tabId: string; - target: WorkspaceTabTarget; - serverId: string; - workspaceId: string; -}) { - const registration = getPanelRegistration(target.kind); - if (!registration) return null; - const Component = registration.component; - return ( - - - - ); -} -``` - -## Implementation Steps - -### Step 1: Create panel registry infrastructure - -Create the following new files: - -- `packages/app/src/panels/panel-registry.ts` — `PanelRegistration`, `PanelDescriptor` types, registry map, `registerPanel()`, `getPanelRegistration()` -- `packages/app/src/panels/pane-context.ts` — `PaneContextValue` type, React context, `PaneProvider`, `usePaneContext()` hook - -### Step 2: Create panel registration files - -Move panel-specific logic out of workspace-screen, workspace-tab-model, and workspace-tab-presentation into self-contained panel modules: - -- `packages/app/src/panels/agent-panel.ts` — agent component wrapper + `useDescriptor` + `confirmClose` -- `packages/app/src/panels/draft-panel.ts` — draft component wrapper + `useDescriptor` -- `packages/app/src/panels/terminal-panel.ts` — terminal component wrapper + `useDescriptor` -- `packages/app/src/panels/file-panel.ts` — file component wrapper + `useDescriptor` -- `packages/app/src/panels/register-panels.ts` — imports all panels, calls `registerPanel()` for each - -### Step 3: Refactor workspace-tab-model.ts - -Replace the per-kind descriptor derivation in `deriveWorkspaceTabModel()` with calls to `getPanelRegistration(target.kind).useDescriptor(...)`. - -Note: `deriveWorkspaceTabModel` is a pure function, not a hook. The `useDescriptor` hooks are called from React components (the tab bar). The model derivation may need to be restructured — the tab bar calls `useDescriptor` per tab, and the model just handles ordering and active-tab resolution. - -### Step 4: Refactor workspace-screen.tsx renderContent() - -Replace the `renderContent()` switch with `` that uses the registry. Wire up the `PaneProvider` with the action callbacks that currently live as inline functions in the workspace screen. - -### Step 5: Refactor workspace-tab-presentation.tsx - -Move icon components and status derivation into each panel's registration. The shared `WorkspaceTabIcon` component becomes a thin wrapper that calls `registration.useDescriptor()` and renders the icon from the descriptor. - -### Step 6: Verify - -- `npm run typecheck` must pass -- All existing tab behavior must work identically: open, close, reorder, retarget, keyboard shortcuts, context menus -- Mobile tab switcher must work unchanged -- No visual regressions in tab bar, icons, status indicators - -## Constraints - -- **Pure refactor** — zero user-visible behavior changes -- **No new features** — no splits, no new panel types, no new keyboard shortcuts -- **WorkspaceTabTarget stays unchanged** — no store migration -- **workspace-tabs-store.ts stays unchanged** — the store is not part of this refactor -- **Do not create index.ts barrel files** — project convention -- **Use `invariant` from `tiny-invariant`** for asserting panel target kinds -- **Use `function` declarations** — project convention (no arrow function components) -- **Use `interface` over `type` where possible** — project convention -- **Run `npm run typecheck` after every change** — project rule diff --git a/docs/RELEASE.md b/docs/RELEASE.md index 507579cd7..c323047c6 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -31,6 +31,7 @@ npm run release:finalize # Publish npm, promote draft to published - `draft-release:patch` creates the GitHub Release as a draft so desktop assets, APK uploads, and synced notes attach to it - `release:finalize` publishes npm and promotes the same draft release - Use the same semver tag for both; don't cut a second tag +- Desktop assets now come from the Electron package at `packages/desktop` ## Fixing a failed release build @@ -53,6 +54,7 @@ If the fix requires a code change (e.g. a broken build script), commit the fix t - `version:all:*` bumps root + syncs workspace versions and `@getpaseo/*` dependency versions - `release:prepare` refreshes workspace `node_modules` links to prevent stale types +- `npm run dev:desktop` and `npm run build:desktop` target the Electron desktop package in `packages/desktop` - If `release:publish` partially fails, re-run it — npm skips already-published versions - Website Mac download CTA URL derives from `packages/website/package.json` version at build time diff --git a/docs/SPLIT-PANES-PLAN.md b/docs/SPLIT-PANES-PLAN.md deleted file mode 100644 index 9896e8398..000000000 --- a/docs/SPLIT-PANES-PLAN.md +++ /dev/null @@ -1,275 +0,0 @@ -# Split Panes Plan - -**Goal:** VSCode-style split panes for the workspace screen. Users can drag tabs to edges to create horizontal/vertical splits, resize splits, and navigate between panes with keyboard shortcuts. Desktop/web only — mobile uses the same store but never creates splits (single pane). - -## Data Model - -### Core Types - -```typescript -interface SplitPane { - id: string; - tabIds: string[]; - focusedTabId: string | null; -} - -interface SplitGroup { - id: string; - direction: "horizontal" | "vertical"; - children: SplitNode[]; - sizes: number[]; // proportional, sum to 1, same length as children -} - -type SplitNode = - | { kind: "pane"; pane: SplitPane } - | { kind: "group"; group: SplitGroup }; - -interface WorkspaceLayout { - root: SplitNode; - focusedPaneId: string; -} -``` - -### Design Decisions - -- **Single store replaces the flat tab store.** The layout store owns tabs, tab order (per pane), and focused tab (per pane). No separate flat tab store. -- **Mobile is just a single-pane tree.** Same store, same code paths. Mobile never calls split operations, so the tree never grows beyond one pane. -- **Focused pane concept.** Common operations (`openTab`, `closeTab`, `focusTab`) route to the focused pane automatically. No `paneId` parameter needed for everyday use. -- **`PaneContext` doesn't need `paneId`.** Split-specific operations (drag-drop, resize) are wired directly in split UI components that know their pane ID from tree rendering. -- **Max depth: 4 levels.** -- **Proportional sizes** that sum to 1. Minimum proportion per child: 0.1 (10%). - -### Default State - -Every workspace starts with: - -```typescript -{ - root: { kind: "pane", pane: { id: "main", tabIds: [], focusedTabId: null } }, - focusedPaneId: "main", -} -``` - -### Migration - -Version 6 migration from the current flat tab store. Wraps existing `tabIds`, `tabOrder`, and `focusedTabId` into a single-pane tree. - -## Store Actions - -### Everyday Operations (pane-agnostic) - -These don't take a `paneId`. Mobile code only uses these. - -```typescript -openTab(workspaceKey: string, target: WorkspaceTabTarget): string | null; -closeTab(workspaceKey: string, tabId: string): void; -focusTab(workspaceKey: string, tabId: string): void; -retargetTab(workspaceKey: string, tabId: string, target: WorkspaceTabTarget): string | null; -reorderTabs(workspaceKey: string, tabIds: string[]): void; // within focused pane -getWorkspaceTabs(workspaceKey: string): WorkspaceTab[]; // all tabs across all panes -``` - -- `openTab` creates the tab and adds it to the focused pane. -- `closeTab` finds the tab in any pane, removes it. If that was the last tab in the pane, collapses the pane. -- `focusTab` finds the tab in any pane, focuses it and focuses that pane. - -### Split Operations (desktop only) - -```typescript -splitPane(workspaceKey: string, input: { - tabId: string; - targetPaneId: string; - position: "left" | "right" | "top" | "bottom"; -}): string | null; // new pane ID, or null if depth cap hit - -moveTabToPane(workspaceKey: string, tabId: string, toPaneId: string): void; -focusPane(workspaceKey: string, paneId: string): void; -resizeSplit(workspaceKey: string, groupId: string, sizes: number[]): void; -reorderTabsInPane(workspaceKey: string, paneId: string, tabIds: string[]): void; -``` - -## Tree Transformations - -### splitPane - -**Position mapping:** -- `left` / `right` → `horizontal` direction -- `top` / `bottom` → `vertical` direction -- `left` / `top` → new pane inserted before target -- `right` / `bottom` → new pane inserted after target - -**Optimization:** If the target pane's parent group has the same direction, insert as a sibling into that group instead of nesting. This keeps the tree flat. - -``` -Before: horizontal([A, B]) -Split B right with tab X - -Optimized: horizontal([A, B, C]) ← insert into existing group -Naive: horizontal([A, horizontal([B, C])]) ← wastes depth -``` - -**Steps:** -1. Check depth — reject if would exceed 4 levels -2. Remove `tabId` from source pane (could be same or different pane) -3. Create new pane: `{ id: generateId(), tabIds: [tabId], focusedTabId: tabId }` -4. If parent group has same direction → insert new pane adjacent to target in parent's children, split target's size proportion 50/50 between target and new pane -5. Else → replace target node with new group `{ direction, children: [target, newPane], sizes: [0.5, 0.5] }` (order based on position) -6. If source pane is now empty → collapse it -7. Set `focusedPaneId` to new pane - -### collapsePane - -Triggered when a pane's last tab is removed or moved out. - -``` -Before: horizontal([A, B, C]) sizes [0.3, 0.4, 0.3] -B loses last tab - -After: horizontal([A, C]) sizes [0.5, 0.5] (renormalized) -``` - -**Steps:** -1. Remove pane from parent group's children -2. Remove corresponding entry from parent's sizes -3. Renormalize sizes to sum to 1 -4. If parent group now has 1 child → unwrap: replace group with its single remaining child -5. Unwrap can cascade up the tree -6. Move focus to nearest sibling - -### moveTabToPane - -Tab dragged from one pane to another existing pane. - -1. Remove `tabId` from source pane's `tabIds` -2. Insert into target pane's `tabIds` at drop position (or end) -3. Set target pane's `focusedTabId` to the moved tab -4. If source pane is now empty → collapsePane -5. Set `focusedPaneId` to target pane - -### resizeSplit - -User drags a divider between panes. - -1. Find group by ID -2. Update the two adjacent sizes based on drag delta -3. Clamp each child to minimum proportion (0.1) -4. Renormalize so sizes sum to 1 - -## Keyboard Shortcuts - -| Action | Shortcut | -|---|---| -| Split right | `Cmd+\` | -| Split down | `Cmd+Shift+\` | -| Focus pane left | `Cmd+Shift+←` | -| Focus pane right | `Cmd+Shift+→` | -| Focus pane up | `Cmd+Shift+↑` | -| Focus pane down | `Cmd+Shift+↓` | -| Move tab to pane left | `Cmd+Shift+Alt+←` | -| Move tab to pane right | `Cmd+Shift+Alt+→` | -| Move tab to pane up | `Cmd+Shift+Alt+↑` | -| Move tab to pane down | `Cmd+Shift+Alt+↓` | -| Close pane | `Cmd+Shift+W` | - -Existing tab shortcuts unchanged — `Cmd+T`, `Cmd+W`, `Alt+Shift+[/]`, `Alt+1-9` — they operate on the focused pane's tabs. - -## Drag and Drop UX - -### Drop Zones - -When dragging a tab over a pane, the pane is divided into 5 drop zones: -- **Center** (inner 40%) — move tab to this pane (add to existing tab list) -- **Left edge** (leftmost 15%) — split left -- **Right edge** (rightmost 15%) — split right -- **Top edge** (topmost 15%) — split up -- **Bottom edge** (bottommost 15%) — split down - -### Overlay Preview - -On hover over a drop zone, show a semi-transparent overlay rectangle covering the half of the pane where the new split would appear. The overlay uses the theme's accent color at low opacity. - -### Cross-Pane Tab Drag - -Tabs can be dragged: -- Within a pane's tab bar → reorder (existing behavior via SortableInlineList) -- From one pane's tab bar to another pane's tab bar → move tab to that pane -- From a tab bar to a pane's drop zone → split - -When dragging the last tab out of a pane, the pane collapses after the drop completes. - -## Implementation Steps - -### Step 1: Layout Store - -Create `packages/app/src/stores/workspace-layout-store.ts`: -- `WorkspaceLayout`, `SplitNode`, `SplitPane`, `SplitGroup` types -- Zustand store with AsyncStorage persistence -- Everyday actions: `openTab`, `closeTab`, `focusTab`, `retargetTab`, `reorderTabs` -- Tree helpers: `findPaneById`, `findPaneContainingTab`, `getTreeDepth`, `collectAllTabs` -- Version 6 migration from flat tab store - -### Step 2: Migrate Workspace Screen to Layout Store - -Replace all `useWorkspaceTabsStore` usage in workspace-screen with the new layout store. Mobile and desktop both use the layout store — mobile just never splits. All existing behavior preserved. - -### Step 3: Split Tree Transformations - -Add to the layout store: -- `splitPane` with the parent-direction optimization and depth check -- `collapsePane` with unwrap cascading -- `moveTabToPane` -- `resizeSplit` - -Pure tree transformation functions, tested independently. - -### Step 4: Split Container Component - -Create `packages/app/src/components/split-container.tsx`: -- Recursive component that renders `SplitNode` -- Groups render as flex containers with direction from `SplitGroup.direction` -- Panes render tab bar + active panel content (using the panel registry) -- Resize handles between children of a group - -### Step 5: Drop Zones and Overlay - -Create `packages/app/src/components/split-drop-zone.tsx`: -- Overlay that appears during tab drag -- Divides pane into 5 zones (center + 4 edges) -- Shows preview rectangle on hover -- Calls `splitPane` or `moveTabToPane` on drop - -### Step 6: Cross-Pane Drag - -Extend the existing dnd-kit setup: -- Tab bar items remain draggable (existing) -- Pane drop zones become droppable targets -- Tab bar of other panes become droppable targets (move to pane) -- DndContext wraps the entire split container (not individual panes) - -### Step 7: Keyboard Shortcuts - -Register new actions in `keyboard/actions.ts`: -- `workspace.pane.split.right`, `workspace.pane.split.down` -- `workspace.pane.focus.left/right/up/down` -- `workspace.pane.move-tab.left/right/up/down` -- `workspace.pane.close` - -Add bindings in `keyboard-shortcuts.ts` and handlers in the workspace screen. - -### Step 8: Pane Focus Navigation - -Implement spatial navigation for `focus.left/right/up/down`: -- Walk the tree to find the focused pane's position in the layout -- Find the nearest pane in the requested direction -- Focus it - -Same logic for `move-tab` shortcuts — find adjacent pane, call `moveTabToPane`. - -## Constraints - -- Mobile stays single-pane — same store, no special casing -- Max 4 levels of nesting -- Minimum pane size: 10% of parent -- `PaneContext` interface unchanged — no `paneId` added -- Panel registry unchanged — panels don't know about splits -- Existing tab shortcuts work on focused pane, unchanged diff --git a/package-lock.json b/package-lock.json index 186649dac..60ced9e20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,6 @@ "@anthropic-ai/claude-agent-sdk": "^0.2.11" }, "devDependencies": { - "concurrently": "^9.2.1", "get-port-cli": "^3.0.0", "knip": "^5.82.1", "patch-package": "^8.0.1", @@ -2954,6 +2953,58 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "license": "MIT" }, + "node_modules/@develar/schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@develar/schema-utils/node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@develar/schema-utils/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/@develar/schema-utils/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/@dnd-kit/accessibility": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", @@ -3019,6 +3070,411 @@ "node": ">=0.8.0" } }, + "node_modules/@electron/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/asar/node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/@electron/asar/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/asar/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "electron-fuses": "dist/bin.js" + } + }, + "node_modules/@electron/fuses/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@electron/get/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@electron/get/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", + "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/rebuild": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.3.tgz", + "integrity": "sha512-u9vpTHRMkOYCs/1FLiSVAFZ7FbjsXK+bQuzviJZa+lG7BHZl1nz52/IcGvwa3sk80/fc3llutBkbCq10Vh8WQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.1.1", + "detect-libc": "^2.0.1", + "got": "^11.7.0", + "graceful-fs": "^4.2.11", + "node-abi": "^4.2.0", + "node-api-version": "^0.2.1", + "node-gyp": "^11.2.0", + "ora": "^5.1.0", + "read-binary-file-arch": "^1.0.6", + "semver": "^7.3.5", + "tar": "^7.5.6", + "yargs": "^17.0.1" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@electron/rebuild/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@electron/rebuild/node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@electron/rebuild/node_modules/tar": { + "version": "7.5.12", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.12.tgz", + "integrity": "sha512-9TsuLcdhOn4XztcQqhNyq1KOwOOED/3k58JAvtULiYqbO8B/0IBAAIE1hj0Svmm58k27TmcigyDI0deMlgG3uw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@electron/rebuild/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@electron/universal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", + "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.3.1", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/windows-sign": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", + "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "cross-dirname": "^0.1.0", + "debug": "^4.3.4", + "fs-extra": "^11.1.1", + "minimist": "^1.2.8", + "postject": "^1.0.0-alpha.6" + }, + "bin": { + "electron-windows-sign": "bin/electron-windows-sign.js" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/fs-extra": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/@emnapi/core": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", @@ -6191,6 +6647,33 @@ "react-native": "*" } }, + "node_modules/@hapi/address": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", + "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/address/node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/formula": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz", + "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", @@ -6198,6 +6681,23 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@hapi/pinpoint": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", + "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/tlds": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.6.tgz", + "integrity": "sha512-xdi7A/4NZokvV0ewovme3aUO5kQhW9pQ2YD1hRqZGhhSi5rBv4usHYidVocXSi9eihYsznZxLtAiEYYUL6VBGw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@hapi/topo": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", @@ -7346,6 +7846,61 @@ "@lezer/lr": "^1.4.0" } }, + "node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", @@ -7419,6 +7974,81 @@ "node": ">=12.4.0" } }, + "node_modules/@npmcli/agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", + "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@npmcli/agent/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@npmcli/agent/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@npmcli/fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", + "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/@oclif/core": { "version": "1.26.2", "resolved": "https://registry.npmjs.org/@oclif/core/-/core-1.26.2.tgz", @@ -9385,6 +10015,19 @@ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "license": "MIT" }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@tailwindcss/node": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", @@ -10138,242 +10781,6 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, - "node_modules/@tauri-apps/api": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz", - "integrity": "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==", - "license": "Apache-2.0 OR MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/tauri" - } - }, - "node_modules/@tauri-apps/cli": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.10.1.tgz", - "integrity": "sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g==", - "dev": true, - "license": "Apache-2.0 OR MIT", - "bin": { - "tauri": "tauri.js" - }, - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/tauri" - }, - "optionalDependencies": { - "@tauri-apps/cli-darwin-arm64": "2.10.1", - "@tauri-apps/cli-darwin-x64": "2.10.1", - "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.1", - "@tauri-apps/cli-linux-arm64-gnu": "2.10.1", - "@tauri-apps/cli-linux-arm64-musl": "2.10.1", - "@tauri-apps/cli-linux-riscv64-gnu": "2.10.1", - "@tauri-apps/cli-linux-x64-gnu": "2.10.1", - "@tauri-apps/cli-linux-x64-musl": "2.10.1", - "@tauri-apps/cli-win32-arm64-msvc": "2.10.1", - "@tauri-apps/cli-win32-ia32-msvc": "2.10.1", - "@tauri-apps/cli-win32-x64-msvc": "2.10.1" - } - }, - "node_modules/@tauri-apps/cli-darwin-arm64": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.10.1.tgz", - "integrity": "sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-darwin-x64": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.10.1.tgz", - "integrity": "sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.10.1.tgz", - "integrity": "sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm64-gnu": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.10.1.tgz", - "integrity": "sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm64-musl": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.10.1.tgz", - "integrity": "sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.10.1.tgz", - "integrity": "sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-x64-gnu": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.10.1.tgz", - "integrity": "sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-x64-musl": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.10.1.tgz", - "integrity": "sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-arm64-msvc": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.10.1.tgz", - "integrity": "sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-ia32-msvc": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.10.1.tgz", - "integrity": "sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-x64-msvc": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.10.1.tgz", - "integrity": "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/plugin-log": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-log/-/plugin-log-2.8.0.tgz", - "integrity": "sha512-a+7rOq3MJwpTOLLKbL8d0qGZ85hgHw5pNOWusA9o3cf7cEgtYHiGY/+O8fj8MvywQIGqFv0da2bYQDlrqLE7rw==", - "license": "MIT OR Apache-2.0", - "dependencies": { - "@tauri-apps/api": "^2.8.0" - } - }, "node_modules/@testing-library/react-hooks": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-7.0.2.tgz", @@ -10522,6 +10929,19 @@ "@types/node": "*" } }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -10610,6 +11030,16 @@ "@types/send": "*" } }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -10634,6 +11064,13 @@ "@types/unist": "*" } }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/http-errors": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", @@ -10702,6 +11139,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -10764,6 +11211,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/plist": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", @@ -10824,6 +11283,16 @@ "@types/react": "*" } }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/semver": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", @@ -10890,6 +11359,14 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/verror": { + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", + "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -10915,6 +11392,17 @@ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "license": "MIT" }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.56.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", @@ -11735,6 +12223,13 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/7zip-bin": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", + "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", + "dev": true, + "license": "MIT" + }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -11743,6 +12238,16 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/abbrev": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -12004,6 +12509,296 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/app-builder-bin": { + "version": "5.0.0-alpha.12", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz", + "integrity": "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/app-builder-lib": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.8.1.tgz", + "integrity": "sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@develar/schema-utils": "~2.6.5", + "@electron/asar": "3.4.1", + "@electron/fuses": "^1.8.0", + "@electron/get": "^3.0.0", + "@electron/notarize": "2.5.0", + "@electron/osx-sign": "1.3.3", + "@electron/rebuild": "^4.0.3", + "@electron/universal": "2.0.3", + "@malept/flatpak-bundler": "^0.4.0", + "@types/fs-extra": "9.0.13", + "async-exit-hook": "^2.0.1", + "builder-util": "26.8.1", + "builder-util-runtime": "9.5.1", + "chromium-pickle-js": "^0.2.0", + "ci-info": "4.3.1", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "ejs": "^3.1.8", + "electron-publish": "26.8.1", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "isbinaryfile": "^5.0.0", + "jiti": "^2.4.2", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "lazy-val": "^1.0.5", + "minimatch": "^10.0.3", + "plist": "3.1.0", + "proper-lockfile": "^4.1.2", + "resedit": "^1.7.0", + "semver": "~7.7.3", + "tar": "^7.5.7", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0", + "which": "^5.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "26.8.1", + "electron-builder-squirrel-windows": "26.8.1" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", + "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/app-builder-lib/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/app-builder-lib/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/app-builder-lib/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/app-builder-lib/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/app-builder-lib/node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/app-builder-lib/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/app-builder-lib/node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/app-builder-lib/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/app-builder-lib/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/app-builder-lib/node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/app-builder-lib/node_modules/tar": { + "version": "7.5.12", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.12.tgz", + "integrity": "sha512-9TsuLcdhOn4XztcQqhNyq1KOwOOED/3k58JAvtULiYqbO8B/0IBAAIE1hj0Svmm58k27TmcigyDI0deMlgG3uw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/app-builder-lib/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/app-builder-lib/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/app-root-path": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", @@ -12261,6 +13056,17 @@ "util": "^0.12.5" } }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -12300,6 +13106,16 @@ "dev": true, "license": "MIT" }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -12356,6 +13172,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/b4a": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", @@ -12958,6 +13786,16 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -12971,6 +13809,102 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, + "node_modules/builder-util": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.8.1.tgz", + "integrity": "sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "7zip-bin": "~5.2.0", + "app-builder-bin": "5.0.0-alpha.12", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.6", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "js-yaml": "^4.1.0", + "sanitize-filename": "^1.6.3", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", + "integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/builder-util/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/builder-util/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/builder-util/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/builder-util/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/builder-util/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/bunyan": { "version": "1.8.15", "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.15.tgz", @@ -13009,6 +13943,145 @@ "node": ">=8" } }, + "node_modules/cacache": { + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", + "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/cacache/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cacache/node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/cacache/node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/cacache/node_modules/tar": { + "version": "7.5.12", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.12.tgz", + "integrity": "sha512-9TsuLcdhOn4XztcQqhNyq1KOwOOED/3k58JAvtULiYqbO8B/0IBAAIE1hj0Svmm58k27TmcigyDI0deMlgG3uw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cacache/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -13373,6 +14446,13 @@ "rimraf": "^3.0.2" } }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "license": "MIT" + }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -13442,6 +14522,40 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -13486,6 +14600,19 @@ "node": ">=6" } }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/collect-v8-coverage": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", @@ -13601,6 +14728,16 @@ "dev": true, "license": "MIT" }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/component-type": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/component-type/-/component-type-1.2.2.tgz", @@ -13671,31 +14808,6 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, - "node_modules/concurrently": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", - "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "4.1.2", - "rxjs": "7.8.2", - "shell-quote": "1.8.3", - "supports-color": "8.1.1", - "tree-kill": "1.2.2", - "yargs": "17.7.2" - }, - "bin": { - "conc": "dist/bin/concurrently.js", - "concurrently": "dist/bin/concurrently.js" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" - } - }, "node_modules/connect": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", @@ -13787,6 +14899,14 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/cors": { "version": "2.8.6", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", @@ -13804,6 +14924,43 @@ "url": "https://opencollective.com/express" } }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/crc/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -13811,6 +14968,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cross-dirname": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", + "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/cross-fetch": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", @@ -14193,6 +15359,35 @@ "node": ">=0.10" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -14252,6 +15447,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -14402,6 +15607,41 @@ "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", "license": "MIT" }, + "node_modules/dir-compare": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -14415,6 +15655,96 @@ "node": ">=8" } }, + "node_modules/dmg-builder": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.8.1.tgz", + "integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.8.1", + "builder-util": "26.8.1", + "fs-extra": "^10.1.0", + "iconv-lite": "^0.6.2", + "js-yaml": "^4.1.0" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" + } + }, + "node_modules/dmg-builder/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/dmg-builder/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/dmg-license": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "@types/plist": "^3.0.1", + "@types/verror": "^1.10.3", + "ajv": "^6.10.0", + "crc": "^3.8.0", + "iconv-corefoundation": "^1.1.7", + "plist": "^3.0.4", + "smart-buffer": "^4.0.2", + "verror": "^1.10.0" + }, + "bin": { + "dmg-license": "bin/dmg-license.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dmg-license/node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/dmg-license/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -14873,12 +16203,320 @@ "node": ">=0.10.0" } }, + "node_modules/electron": { + "version": "41.0.3", + "resolved": "https://registry.npmjs.org/electron/-/electron-41.0.3.tgz", + "integrity": "sha512-IDjx8liW1q+r7+MOip5W1Eo1eMwJzVObmYrd9yz2dPCkS7XlgLq3qPVMR80TpiROFp73iY30kTzMdpA6fEVs3A==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^24.9.0", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-builder": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.8.1.tgz", + "integrity": "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.8.1", + "builder-util": "26.8.1", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "dmg-builder": "26.8.1", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.8.1.tgz", + "integrity": "sha512-o288fIdgPLHA76eDrFADHPoo7VyGkDCYbLV1GzndaMSAVBoZrGvM9m2IehdcVMzdAZJ2eV9bgyissQXHv5tGzA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "26.8.1", + "builder-util": "26.8.1", + "electron-winstaller": "5.4.0" + } + }, + "node_modules/electron-builder/node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-publish": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.8.1.tgz", + "integrity": "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "26.8.1", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "form-data": "^4.0.5", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-publish/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.307", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", "license": "ISC" }, + "node_modules/electron-updater": { + "version": "6.8.3", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.8.3.tgz", + "integrity": "sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ==", + "license": "MIT", + "dependencies": { + "builder-util-runtime": "9.5.1", + "fs-extra": "^10.1.0", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.5", + "lodash.escaperegexp": "^4.1.2", + "lodash.isequal": "^4.5.0", + "semver": "~7.7.3", + "tiny-typed-emitter": "^2.1.0" + } + }, + "node_modules/electron-updater/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/electron-updater/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/electron-winstaller": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", + "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@electron/asar": "^3.2.1", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "lodash": "^4.17.21", + "temp": "^0.9.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "@electron/windows-sign": "^1.1.2" + } + }, + "node_modules/electron-winstaller/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/electron-winstaller/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/electron-winstaller/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/electron-winstaller/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "peer": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-winstaller/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/electron-winstaller/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/electron-winstaller/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/electron-winstaller/node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/electron-winstaller/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/electron/node_modules/@types/node": { + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/electron/node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, "node_modules/emittery": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", @@ -14907,6 +16545,16 @@ "node": ">= 0.8" } }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, "node_modules/encoding-sniffer": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", @@ -18756,6 +20404,54 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true + }, "node_modules/fast-copy": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.2.tgz", @@ -18893,6 +20589,16 @@ "walk-up-path": "^4.0.0" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -19215,6 +20921,27 @@ "node": ">=0.4.0" } }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/fontfaceobserver": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/fontfaceobserver/-/fontfaceobserver-2.3.0.tgz", @@ -19362,7 +21089,6 @@ "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", @@ -19812,6 +21538,45 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/got/node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -20145,6 +21910,13 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/http-call": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/http-call/-/http-call-5.3.0.tgz", @@ -20212,6 +21984,20 @@ "node": ">= 6" } }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -20260,6 +22046,32 @@ "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==", "license": "BSD-3-Clause" }, + "node_modules/iconv-corefoundation": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "cli-truncate": "^2.1.0", + "node-addon-api": "^1.6.3" + }, + "engines": { + "node": "^8.11.2 || >=10" + } + }, + "node_modules/iconv-corefoundation/node_modules/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -20428,6 +22240,16 @@ "loose-envify": "^1.0.0" } }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -21051,6 +22873,19 @@ "dev": true, "license": "MIT" }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, "node_modules/isbot": { "version": "5.1.35", "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.35.tgz", @@ -22116,7 +23951,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, "license": "MIT", "dependencies": { "universalify": "^2.0.0" @@ -22289,6 +24123,12 @@ "lan-network": "dist/lan-network-cli.js" } }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "license": "MIT" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -22626,6 +24466,12 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "license": "MIT" }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "license": "MIT" + }, "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -22634,6 +24480,13 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -22700,6 +24553,16 @@ "dev": true, "license": "MIT" }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -22765,6 +24628,49 @@ "dev": true, "license": "ISC" }, + "node_modules/make-fetch-happen": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", + "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/make-fetch-happen/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/make-fetch-happen/node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -24004,6 +25910,16 @@ "node": ">=6" } }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/miniflare": { "version": "4.20260301.1", "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260301.1.tgz", @@ -24100,6 +26016,115 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", + "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/minizlib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.1.tgz", @@ -24397,12 +26422,35 @@ "integrity": "sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==", "license": "MIT" }, + "node_modules/node-abi": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.28.0.tgz", + "integrity": "sha512-Qfp5XZL1cJDOabOT8H5gnqMTmM4NjvYzHp4I/Kt/Sl76OVkOBBHRFlPspGV0hYvMoqQsypFjT/Yp7Km0beXW9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + }, + "engines": { + "node": ">=22.12.0" + } + }, "node_modules/node-addon-api": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "license": "MIT" }, + "node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, "node_modules/node-dir": { "version": "0.1.17", "resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz", @@ -24519,6 +26567,117 @@ "node": ">= 6.13.0" } }, + "node_modules/node-gyp": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", + "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "tar": "^7.4.3", + "tinyglobby": "^0.2.12", + "which": "^5.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/node-gyp/node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/node-gyp/node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/tar": { + "version": "7.5.12", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.12.tgz", + "integrity": "sha512-9TsuLcdhOn4XztcQqhNyq1KOwOOED/3k58JAvtULiYqbO8B/0IBAAIE1hj0Svmm58k27TmcigyDI0deMlgG3uw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -24565,6 +26724,22 @@ "url": "https://github.com/sponsors/antelle" } }, + "node_modules/nopt": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/normalize-package-data": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", @@ -24590,6 +26765,19 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/npm-package-arg": { "version": "11.0.3", "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.3.tgz", @@ -25046,6 +27234,16 @@ "@oxc-resolver/binding-win32-x64-msvc": "11.19.1" } }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -25077,6 +27275,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -25359,6 +27570,28 @@ "node": ">= 14.16" } }, + "node_modules/pe-library": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", + "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -25667,6 +27900,36 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/postject": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", + "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/postject/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -25843,6 +28106,25 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, "node_modules/property-information": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", @@ -25866,6 +28148,13 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, "node_modules/psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", @@ -26952,6 +29241,19 @@ "loose-envify": "^1.1.0" } }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, "node_modules/read-pkg": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-6.0.0.tgz", @@ -27283,6 +29585,24 @@ "dev": true, "license": "MIT" }, + "node_modules/resedit": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", + "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pe-library": "^0.4.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -27303,6 +29623,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, "node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", @@ -27336,6 +29663,19 @@ "node": ">=10" } }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -27653,6 +29993,16 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/sanitize-filename": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, "node_modules/sax": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz", @@ -28270,6 +30620,19 @@ "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", "license": "MIT" }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/sirv": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", @@ -28328,6 +30691,17 @@ "node": ">=8.0.0" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, "node_modules/smol-toml": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.0.tgz", @@ -28341,6 +30715,46 @@ "url": "https://github.com/sponsors/cyyynthia" } }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/sonic-boom": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", @@ -28469,6 +30883,19 @@ "node": ">=20.16.0" } }, + "node_modules/ssri": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", + "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -28574,6 +31001,16 @@ "node": ">=8" } }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -28975,6 +31412,19 @@ "node": ">= 6" } }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -29162,6 +31612,17 @@ "node": ">=8" } }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, "node_modules/temp/node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -29378,12 +31839,38 @@ "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", "license": "MIT" }, + "node_modules/tiny-async-pool": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", + "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.5.0" + } + }, + "node_modules/tiny-async-pool/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, + "node_modules/tiny-typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", + "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==", + "license": "MIT" + }, "node_modules/tiny-warning": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", @@ -29460,6 +31947,16 @@ "node": ">=14.14" } }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -29529,16 +32026,6 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true, - "license": "MIT", - "bin": { - "tree-kill": "cli.js" - } - }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -29582,6 +32069,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, "node_modules/ts-api-utils": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", @@ -30129,6 +32626,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/unique-filename": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", + "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/unique-slug": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", + "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/unique-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", @@ -30214,7 +32737,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 10.0.0" @@ -30400,6 +32922,13 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, "node_modules/util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", @@ -30484,6 +33013,22 @@ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -30768,6 +33313,62 @@ "node": ">=14" } }, + "node_modules/wait-on": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-8.0.5.tgz", + "integrity": "sha512-J3WlS0txVHkhLRb2FsmRg3dkMTCV1+M6Xra3Ho7HzZDHpE7DCOnoSoCJsZotrmW3uRMhvIJGSKUKrh/MeF4iag==", + "dev": true, + "license": "MIT", + "dependencies": { + "axios": "^1.12.1", + "joi": "^18.0.1", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "rxjs": "^7.8.2" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/wait-on/node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/wait-on/node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/wait-on/node_modules/joi": { + "version": "18.0.2", + "resolved": "https://registry.npmjs.org/joi/-/joi-18.0.2.tgz", + "integrity": "sha512-RuCOQMIt78LWnktPoeBL0GErkNaJPTBGcYuyaBvUOQSpcpcLfWrHPPihYdOGbV5pam9VTWbeoF7TsGiHugcjGA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/address": "^5.1.1", + "@hapi/formula": "^3.0.2", + "@hapi/hoek": "^11.0.7", + "@hapi/pinpoint": "^2.0.1", + "@hapi/tlds": "^1.1.1", + "@hapi/topo": "^6.0.2", + "@standard-schema/spec": "^1.0.0" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/walk-up-path": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz", @@ -31384,6 +33985,17 @@ "node": ">=12" } }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", @@ -31561,8 +34173,6 @@ "@react-navigation/native": "^7.1.8", "@tanstack/react-query": "^5.90.11", "@tanstack/react-virtual": "^3.13.21", - "@tauri-apps/api": "^2.9.1", - "@tauri-apps/plugin-log": "^2.8.0", "@xterm/addon-fit": "^0.11.0", "@xterm/addon-unicode11": "^0.9.0", "@xterm/addon-webgl": "^0.19.0", @@ -31702,10 +34312,38 @@ "packages/desktop": { "name": "@getpaseo/desktop", "version": "0.1.30", + "dependencies": { + "@getpaseo/cli": "0.1.30", + "@getpaseo/server": "0.1.30", + "electron-updater": "^6.6.2", + "ws": "^8.14.2" + }, "devDependencies": { - "@tauri-apps/cli": "^2.9.6" + "@types/node": "24.6.0", + "@types/ws": "^8.5.14", + "electron": "41.0.3", + "electron-builder": "26.8.1", + "typescript": "5.9.3", + "wait-on": "8.0.5" } }, + "packages/desktop/node_modules/@types/node": { + "version": "24.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.6.0.tgz", + "integrity": "sha512-F1CBxgqwOMc4GKJ7eY22hWhBVQuMYTtqI8L0FcszYcpYX0fzfDGpez22Xau8Mgm7O9fI+zA/TYIdq3tGWfweBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.13.0" + } + }, + "packages/desktop/node_modules/undici-types": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.13.0.tgz", + "integrity": "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==", + "dev": true, + "license": "MIT" + }, "packages/expo-two-way-audio": { "name": "@getpaseo/expo-two-way-audio", "version": "0.1.30", diff --git a/package.json b/package.json index 781e1de18..70e38d065 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,6 @@ "release:major": "npm run version:all:major && npm run release:check && npm run release:publish && npm run release:push" }, "devDependencies": { - "concurrently": "^9.2.1", "prettier": "^3.5.3", "get-port-cli": "^3.0.0", "knip": "^5.82.1", diff --git a/packages/app/index.ts b/packages/app/index.ts index 8ff193c14..060f848a7 100644 --- a/packages/app/index.ts +++ b/packages/app/index.ts @@ -2,17 +2,10 @@ import { polyfillCrypto } from "./src/polyfills/crypto"; polyfillCrypto(); -// Polyfill screen.orientation for WebKitGTK (Tauri Linux) which lacks the API +// Polyfill screen.orientation for WebKitGTK desktop runtimes that lack the API. import { polyfillScreenOrientation } from "./src/polyfills/screen-orientation"; polyfillScreenOrientation(); -// Bridge console.log/warn/error to Tauri's log plugin so JS output appears in app.log -if ((globalThis as { __TAURI__?: unknown }).__TAURI__) { - import("@tauri-apps/plugin-log").then(({ attachConsole }) => { - attachConsole(); - }); -} - // Configure Unistyles before Expo Router pulls in any components using StyleSheet. import "./src/styles/unistyles"; import "expo-router/entry"; diff --git a/packages/app/package.json b/packages/app/package.json index 39c119780..dc1a2e941 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -16,7 +16,6 @@ "ios": "expo run:ios", "ios:release": "expo run:ios --configuration Release", "web": "expo start --web", - "web:tauri": "PASEO_WEB_PLATFORM=tauri expo start --web", "lint": "expo lint", "typecheck": "tsc --noEmit", "test": "vitest run", @@ -24,7 +23,6 @@ "test:e2e:ui": "playwright test --ui", "build": "npm run build:web", "build:web": "expo export --platform web", - "build:web:tauri": "PASEO_WEB_PLATFORM=tauri expo export --platform web", "deploy:web": "npm run build:web && wrangler pages deploy dist --project-name paseo-app --branch main" }, "dependencies": { @@ -60,9 +58,7 @@ "@react-navigation/native": "^7.1.8", "@tanstack/react-query": "^5.90.11", "@tanstack/react-virtual": "^3.13.21", - "@tauri-apps/api": "^2.9.1", - "@tauri-apps/plugin-log": "^2.8.0", - "@xterm/addon-fit": "^0.11.0", +"@xterm/addon-fit": "^0.11.0", "@xterm/addon-unicode11": "^0.9.0", "@xterm/addon-webgl": "^0.19.0", "@xterm/xterm": "^6.0.0", diff --git a/packages/app/public/index.html b/packages/app/public/index.html new file mode 100644 index 000000000..12161df30 --- /dev/null +++ b/packages/app/public/index.html @@ -0,0 +1,54 @@ + + + + + + + %WEB_TITLE% + + + + + + +
+ + diff --git a/packages/app/src/app/_layout.tsx b/packages/app/src/app/_layout.tsx index 34cde2bd7..e9575c8d5 100644 --- a/packages/app/src/app/_layout.tsx +++ b/packages/app/src/app/_layout.tsx @@ -53,11 +53,10 @@ import { HorizontalScrollProvider, useHorizontalScrollOptional, } from "@/contexts/horizontal-scroll-context"; -import { getIsTauri } from "@/constants/layout"; +import { getIsDesktop } from "@/constants/layout"; import { CommandCenter } from "@/components/command-center"; import { ProjectPickerModal } from "@/components/project-picker-modal"; import { KeyboardShortcutsDialog } from "@/components/keyboard-shortcuts-dialog"; -import { listenToDesktopNotificationClicks } from "@/desktop/notifications/desktop-notifications"; import { useKeyboardShortcuts } from "@/hooks/use-keyboard-shortcuts"; import { queryClient } from "@/query/query-client"; import { @@ -65,19 +64,18 @@ import { type WebNotificationClickDetail, ensureOsNotificationPermission, } from "@/utils/os-notifications"; +import { getDesktopHost } from "@/desktop/host"; import { buildNotificationRoute } from "@/utils/notification-routing"; import { buildHostRootRoute, + mapPathnameToServer, parseServerIdFromPathname, parseHostAgentRouteFromPathname, parseWorkspaceOpenIntent, } from "@/utils/host-routes"; -import { getTauri } from "@/utils/tauri"; -import { attachConsole } from "@/utils/tauri-attach-console"; import { syncNavigationActiveWorkspace } from "@/stores/navigation-active-workspace-store"; polyfillCrypto(); -attachConsole(); const HostRuntimeBootstrapContext = createContext(false); function PushNotificationRouter() { @@ -86,33 +84,37 @@ function PushNotificationRouter() { useEffect(() => { if (Platform.OS === "web") { - if (getTauri()) { + let removeDesktopNotificationListener: (() => void) | null = null; + let cancelled = false; + + if (getIsDesktop()) { void ensureOsNotificationPermission(); - let disposed = false; - let unlisten: (() => void) | null = null; + const unlistenResult = getDesktopHost()?.events?.on?.( + "notification-click", + (payload: unknown) => { + const data = + typeof payload === "object" && + payload !== null && + "data" in payload && + typeof (payload as { data?: unknown }).data === "object" && + (payload as { data?: unknown }).data !== null + ? ((payload as { data: Record }).data) + : undefined; + router.push(buildNotificationRoute(data) as any); + } + ); - void listenToDesktopNotificationClicks((payload) => { - router.push(buildNotificationRoute(payload.data) as any); - }) - .then((cleanup) => { - if (disposed) { - cleanup(); - return; - } - unlisten = cleanup; - }) - .catch((error) => { - console.error( - "[OSNotifications][Desktop] Failed to register notification click listener", - error - ); - }); - - return () => { - disposed = true; - unlisten?.(); - }; + void Promise.resolve(unlistenResult).then((unlisten) => { + if (typeof unlisten !== "function") { + return; + } + if (cancelled) { + unlisten(); + return; + } + removeDesktopNotificationListener = unlisten; + }); } const target = globalThis as unknown as EventTarget; @@ -126,7 +128,10 @@ function PushNotificationRouter() { WEB_NOTIFICATION_CLICK_EVENT, openFromWebClick as EventListener ); + return () => { + cancelled = true; + removeDesktopNotificationListener?.(); target.removeEventListener( WEB_NOTIFICATION_CLICK_EVENT, openFromWebClick as EventListener @@ -476,12 +481,24 @@ function OfferLinkListener({ } function AppWithSidebar({ children }: { children: ReactNode }) { + const router = useRouter(); const pathname = usePathname(); const params = useGlobalSearchParams<{ open?: string | string[] }>(); + const hosts = useHosts(); useFaviconStatus(); const activeServerId = useMemo(() => parseServerIdFromPathname(pathname), [pathname]); const shouldShowAppChrome = activeServerId !== null; + useEffect(() => { + if (!activeServerId || hosts.length === 0) { + return; + } + if (hosts.some((host) => host.serverId === activeServerId)) { + return; + } + router.replace(mapPathnameToServer(pathname, hosts[0]!.serverId) as any); + }, [activeServerId, hosts, pathname, router]); + // Parse selectedAgentKey directly from pathname // useLocalSearchParams doesn't update when navigating between same-pattern routes const selectedAgentKey = useMemo(() => { diff --git a/packages/app/src/app/index.tsx b/packages/app/src/app/index.tsx index 45e3d983a..3cff19b69 100644 --- a/packages/app/src/app/index.tsx +++ b/packages/app/src/app/index.tsx @@ -1,7 +1,7 @@ import { useEffect, useSyncExternalStore, useState } from 'react' import { usePathname, useRouter } from 'expo-router' import { useHosts } from '@/runtime/host-runtime' -import { shouldUseManagedDesktopDaemon } from '@/desktop/managed-runtime/managed-runtime' +import { shouldUseDesktopDaemon } from '@/desktop/daemon/desktop-daemon' import { buildHostRootRoute } from '@/utils/host-routes' import { StartupSplashScreen } from '@/screens/startup-splash-screen' import { WelcomeScreen } from '@/components/welcome-screen' @@ -57,7 +57,7 @@ export default function Index() { const pathname = usePathname() const daemons = useHosts() const [hasTimedOut, setHasTimedOut] = useState(false) - const isDesktopStartupRace = shouldUseManagedDesktopDaemon() + const isDesktopStartupRace = shouldUseDesktopDaemon() const onlineServerId = useAnyHostOnline(daemons.map((daemon) => daemon.serverId)) useEffect(() => { const timer = setTimeout(() => { diff --git a/packages/app/src/attachments/store.ts b/packages/app/src/attachments/store.ts index 7e424f349..98a873f87 100644 --- a/packages/app/src/attachments/store.ts +++ b/packages/app/src/attachments/store.ts @@ -1,12 +1,12 @@ import { Platform } from "react-native"; -import { isTauriEnvironment } from "@/utils/tauri"; +import { isDesktop } from "@/desktop/host"; import type { AttachmentStore } from "@/attachments/types"; let attachmentStorePromise: Promise | null = null; async function createAttachmentStore(): Promise { if (Platform.OS === "web") { - if (isTauriEnvironment()) { + if (isDesktop()) { const { createDesktopAttachmentStore } = await import( "../desktop/attachments/desktop-attachment-store" ); diff --git a/packages/app/src/components/headers/screen-header.tsx b/packages/app/src/components/headers/screen-header.tsx index 2d3e8b16f..1d7cad66f 100644 --- a/packages/app/src/components/headers/screen-header.tsx +++ b/packages/app/src/components/headers/screen-header.tsx @@ -6,9 +6,9 @@ import { HEADER_INNER_HEIGHT, HEADER_INNER_HEIGHT_MOBILE, HEADER_TOP_PADDING_MOBILE, - getIsTauriMac, + getIsDesktopMac, } from "@/constants/layout"; -import { useTauriDragHandlers, useTrafficLightPadding } from "@/utils/tauri-window"; +import { useDesktopDragHandlers, useTrafficLightPadding } from "@/utils/desktop-window"; import { usePanelStore } from "@/stores/panel-store"; interface ScreenHeaderProps { @@ -37,18 +37,21 @@ export function ScreenHeader({ const topPadding = isMobile ? HEADER_TOP_PADDING_MOBILE : 0; const baseHorizontalPadding = theme.spacing[2]; const collapsedSidebarTrafficLightInset = - !isMobile && !desktopAgentListOpen && getIsTauriMac() + !isMobile && !desktopAgentListOpen && getIsDesktopMac() ? trafficLightPadding.left : 0; - // On Tauri macOS, enable window dragging and double-click to maximize - const dragHandlers = useTauriDragHandlers(); + const { style: dragRegionStyle, ...dragHandlers } = useDesktopDragHandlers(); return ( {left} diff --git a/packages/app/src/components/keyboard-shortcuts-dialog.tsx b/packages/app/src/components/keyboard-shortcuts-dialog.tsx index 1c7684d09..a2c7ebddd 100644 --- a/packages/app/src/components/keyboard-shortcuts-dialog.tsx +++ b/packages/app/src/components/keyboard-shortcuts-dialog.tsx @@ -1,7 +1,7 @@ import { useMemo } from "react"; import { Text, View } from "react-native"; import { StyleSheet } from "react-native-unistyles"; -import { getIsTauri } from "@/constants/layout"; +import { getIsDesktop } from "@/constants/layout"; import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet"; import { Shortcut } from "@/components/ui/shortcut"; import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store"; @@ -13,10 +13,10 @@ export function KeyboardShortcutsDialog() { const setOpen = useKeyboardShortcutsStore((s) => s.setShortcutsDialogOpen); const isMac = getShortcutOs() === "mac"; - const isTauri = getIsTauri(); + const isDesktopApp = getIsDesktop(); const sections = useMemo( - () => buildKeyboardShortcutHelpSections({ isMac, isTauri }), - [isMac, isTauri] + () => buildKeyboardShortcutHelpSections({ isMac, isDesktop: isDesktopApp }), + [isDesktopApp, isMac] ); return ( diff --git a/packages/app/src/components/left-sidebar.tsx b/packages/app/src/components/left-sidebar.tsx index 172959e9b..17be8b537 100644 --- a/packages/app/src/components/left-sidebar.tsx +++ b/packages/app/src/components/left-sidebar.tsx @@ -20,7 +20,7 @@ import { SidebarAgentListSkeleton } from './sidebar-agent-list-skeleton' import { useSidebarShortcutModel } from '@/hooks/use-sidebar-shortcut-model' import { useSidebarWorkspacesList } from '@/hooks/use-sidebar-workspaces-list' import { useSidebarAnimation } from '@/contexts/sidebar-animation-context' -import { useTauriDragHandlers, useTrafficLightPadding } from '@/utils/tauri-window' +import { useDesktopDragHandlers, useTrafficLightPadding } from '@/utils/desktop-window' import { Combobox } from '@/components/ui/combobox' import { getHostRuntimeStore, useHosts } from '@/runtime/host-runtime' import { formatConnectionStatus } from '@/utils/daemons' @@ -130,7 +130,7 @@ export function LeftSidebar({ selectedAgentId: _selectedAgentId }: LeftSidebarPr isGesturing, closeGestureRef, } = useSidebarAnimation() - const dragHandlers = useTauriDragHandlers() + const { style: dragRegionStyle, ...dragHandlers } = useDesktopDragHandlers() const trafficLightPadding = useTrafficLightPadding() const closeTouchStartX = useSharedValue(0) const closeTouchStartY = useSharedValue(0) @@ -448,9 +448,9 @@ export function LeftSidebar({ selectedAgentId: _selectedAgentId }: LeftSidebarPr return ( {trafficLightPadding.top > 0 ? ( - + ) : null} - + >>( new Map() ) - const isTauri = getIsTauri() + const isDesktopApp = getIsDesktop() const altDown = useKeyboardShortcutsStore((state) => state.altDown) const cmdOrCtrlDown = useKeyboardShortcutsStore((state) => state.cmdOrCtrlDown) - const showShortcutBadges = altDown || (isTauri && cmdOrCtrlDown) + const showShortcutBadges = altDown || (isDesktopApp && cmdOrCtrlDown) const getProjectOrder = useSidebarOrderStore((state) => state.getProjectOrder) const setProjectOrder = useSidebarOrderStore((state) => state.setProjectOrder) diff --git a/packages/app/src/constants/layout.ts b/packages/app/src/constants/layout.ts index 7a8ac7833..2bb8c8c19 100644 --- a/packages/app/src/constants/layout.ts +++ b/packages/app/src/constants/layout.ts @@ -1,5 +1,5 @@ import { Platform } from "react-native"; -import { getTauri } from "@/utils/tauri"; +import { isDesktop, isDesktopMac } from "@/desktop/host"; export const FOOTER_HEIGHT = 75; @@ -14,60 +14,56 @@ export const HEADER_TOP_PADDING_MOBILE = 8; // Max width for chat content (stream view, input area, new agent form) export const MAX_CONTENT_WIDTH = 820; -// Tauri desktop app constants for macOS traffic light buttons +// Desktop app constants for macOS traffic light buttons // These buttons (close/minimize/maximize) overlay the top-left corner -export const TAURI_TRAFFIC_LIGHT_WIDTH = 78; -export const TAURI_TRAFFIC_LIGHT_HEIGHT = 56; +export const DESKTOP_TRAFFIC_LIGHT_WIDTH = 78; +export const DESKTOP_TRAFFIC_LIGHT_HEIGHT = 45; -// Check if running in Tauri desktop app (any OS) -function isTauri(): boolean { +// Check if running in desktop app (any OS) +function isDesktopEnvironment(): boolean { if (Platform.OS !== "web") return false; - return getTauri() !== null; + return isDesktop(); } -// Check if running in Tauri desktop app on macOS -function isTauriMac(): boolean { +// Check if running in desktop host on macOS +function isDesktopEnvironmentMac(): boolean { if (Platform.OS !== "web") return false; - if (typeof window === "undefined") return false; - if (getTauri() === null) return false; - // Check for macOS via user agent - const ua = navigator.userAgent; - return ua.includes("Mac OS") || ua.includes("Macintosh"); + return isDesktopMac(); } -// Cached result - only cache true, keep checking if false (in case Tauri globals load later) -let _isTauriMacCached: boolean | null = null; -let _isTauriCached: boolean | null = null; +// Cached result - only cache true, keep checking if false (in case desktop globals load later) +let _isDesktopMacCached: boolean | null = null; +let _isDesktopCached: boolean | null = null; -export function getIsTauriMac(): boolean { - if (_isTauriMacCached === true) { +export function getIsDesktopMac(): boolean { + if (_isDesktopMacCached === true) { return true; } - const result = isTauriMac(); + const result = isDesktopEnvironmentMac(); if (result) { - _isTauriMacCached = true; + _isDesktopMacCached = true; } return result; } -export function getIsTauri(): boolean { - if (_isTauriCached === true) { +export function getIsDesktop(): boolean { + if (_isDesktopCached === true) { return true; } - const result = isTauri(); + const result = isDesktopEnvironment(); if (result) { - _isTauriCached = true; + _isDesktopCached = true; } return result; } -// Get traffic light padding values (only non-zero on Tauri macOS) +// Get traffic light padding values (only non-zero on desktop macOS) export function getTrafficLightPadding(): { left: number; top: number } { - if (!getIsTauriMac()) { + if (!getIsDesktopMac()) { return { left: 0, top: 0 }; } return { - left: TAURI_TRAFFIC_LIGHT_WIDTH, - top: TAURI_TRAFFIC_LIGHT_HEIGHT, + left: DESKTOP_TRAFFIC_LIGHT_WIDTH, + top: DESKTOP_TRAFFIC_LIGHT_HEIGHT, }; } diff --git a/packages/app/src/desktop/attachments/desktop-file-commands.ts b/packages/app/src/desktop/attachments/desktop-file-commands.ts index f8e28e20f..fe5e8642f 100644 --- a/packages/app/src/desktop/attachments/desktop-file-commands.ts +++ b/packages/app/src/desktop/attachments/desktop-file-commands.ts @@ -1,4 +1,4 @@ -import { invokeDesktopCommand } from "@/desktop/tauri/invoke-desktop-command"; +import { invokeDesktopCommand } from "@/desktop/electron/invoke"; interface AttachmentFileResult { path: string; diff --git a/packages/app/src/desktop/attachments/desktop-preview-url.test.ts b/packages/app/src/desktop/attachments/desktop-preview-url.test.ts index daaf8b429..a18cd4ca7 100644 --- a/packages/app/src/desktop/attachments/desktop-preview-url.test.ts +++ b/packages/app/src/desktop/attachments/desktop-preview-url.test.ts @@ -4,7 +4,7 @@ const { invokeDesktopCommandMock } = vi.hoisted(() => ({ invokeDesktopCommandMock: vi.fn(async () => "AAECAw=="), })); -vi.mock("@/desktop/tauri/invoke-desktop-command", () => ({ +vi.mock("@/desktop/electron/invoke", () => ({ invokeDesktopCommand: invokeDesktopCommandMock, })); diff --git a/packages/app/src/desktop/attachments/desktop-preview-url.ts b/packages/app/src/desktop/attachments/desktop-preview-url.ts index 14987149a..c9a383d19 100644 --- a/packages/app/src/desktop/attachments/desktop-preview-url.ts +++ b/packages/app/src/desktop/attachments/desktop-preview-url.ts @@ -1,6 +1,6 @@ import type { AttachmentMetadata } from "@/attachments/types"; import { fileUriToPath } from "@/attachments/utils"; -import { invokeDesktopCommand } from "@/desktop/tauri/invoke-desktop-command"; +import { invokeDesktopCommand } from "@/desktop/electron/invoke"; function base64ToUint8Array(base64: string): Uint8Array { const binary = atob(base64); diff --git a/packages/app/src/desktop/components/desktop-updates-section.tsx b/packages/app/src/desktop/components/desktop-updates-section.tsx index 01e530c50..d557fad82 100644 --- a/packages/app/src/desktop/components/desktop-updates-section.tsx +++ b/packages/app/src/desktop/components/desktop-updates-section.tsx @@ -20,21 +20,21 @@ import { Button } from '@/components/ui/button' import { useAppSettings } from '@/hooks/use-settings' import { confirmDialog } from '@/utils/confirm-dialog' import { openExternalUrl } from '@/utils/open-external-url' -import { formatVersionWithPrefix, isVersionMismatch } from '@/desktop/updates/desktop-updates' +import { getLocalDaemonVersion, isVersionMismatch } from '@/desktop/updates/desktop-updates' import { getCliSymlinkInstructions, - getManagedDaemonLogs, - getManagedDaemonPairing, - getManagedDaemonStatus, - restartManagedDaemon, - shouldUseManagedDesktopDaemon, - startManagedDaemon, - stopManagedDaemon, + getDesktopDaemonLogs, + getDesktopDaemonPairing, + getDesktopDaemonStatus, + restartDesktopDaemon, + shouldUseDesktopDaemon, + startDesktopDaemon, + stopDesktopDaemon, type CliSymlinkInstructions, - type ManagedDaemonLogs, - type ManagedPairingOffer, - type ManagedDaemonStatus, -} from '@/desktop/managed-runtime/managed-runtime' + type DesktopDaemonLogs, + type DesktopDaemonStatus, + type DesktopPairingOffer, +} from '@/desktop/daemon/desktop-daemon' export interface LocalDaemonSectionProps { appVersion: string | null @@ -46,33 +46,35 @@ export function LocalDaemonSection({ showLifecycleControls, }: LocalDaemonSectionProps) { const { theme } = useUnistyles() - const showSection = shouldUseManagedDesktopDaemon() + const showSection = shouldUseDesktopDaemon() const { settings, updateSettings } = useAppSettings() - const [managedStatus, setManagedStatus] = useState(null) + const [daemonStatus, setDaemonStatus] = useState(null) + const [daemonVersion, setDaemonVersion] = useState(null) const [statusError, setStatusError] = useState(null) const [isRestartingDaemon, setIsRestartingDaemon] = useState(false) const [isUpdatingDaemonManagement, setIsUpdatingDaemonManagement] = useState(false) const [isLoadingCliSymlinkInstructions, setIsLoadingCliSymlinkInstructions] = useState(false) const [statusMessage, setStatusMessage] = useState(null) const [cliStatusMessage, setCliStatusMessage] = useState(null) - const [managedLogs, setManagedLogs] = useState(null) + const [daemonLogs, setDaemonLogs] = useState(null) const [isLogsModalOpen, setIsLogsModalOpen] = useState(false) const [isPairingModalOpen, setIsPairingModalOpen] = useState(false) const [isCliSymlinkModalOpen, setIsCliSymlinkModalOpen] = useState(false) const [isLoadingPairing, setIsLoadingPairing] = useState(false) - const [pairingOffer, setPairingOffer] = useState(null) + const [pairingOffer, setPairingOffer] = useState(null) const [cliSymlinkInstructions, setCliSymlinkInstructions] = useState(null) const [pairingStatusMessage, setPairingStatusMessage] = useState(null) - const loadManagedStatus = useCallback(() => { + const loadDaemonData = useCallback(() => { if (!showSection) { return Promise.resolve() } - return Promise.all([getManagedDaemonStatus(), getManagedDaemonLogs()]) - .then(([status, logs]) => { - setManagedStatus(status) - setManagedLogs(logs) + return Promise.all([getDesktopDaemonStatus(), getDesktopDaemonLogs(), getLocalDaemonVersion()]) + .then(([status, logs, version]) => { + setDaemonStatus(status) + setDaemonLogs(logs) + setDaemonVersion(version.version) setStatusError(null) }) .catch((error) => { @@ -86,35 +88,31 @@ export function LocalDaemonSection({ if (!showSection) { return undefined } - void loadManagedStatus() + void loadDaemonData() return undefined - }, [loadManagedStatus, showSection]) + }, [loadDaemonData, showSection]) ) - const localDaemonVersionText = formatVersionWithPrefix(managedStatus?.runtimeVersion ?? null) - const daemonVersionMismatch = isVersionMismatch(appVersion, managedStatus?.runtimeVersion ?? null) + const daemonVersionMismatch = isVersionMismatch(appVersion, daemonVersion) const daemonStatusStateText = - statusError ?? (managedStatus?.status === 'running' ? managedStatus.status : 'not running') - const daemonStatusDetailText = `PID ${managedStatus?.pid ? managedStatus.pid : '—'}` + statusError ?? (daemonStatus?.status === 'running' ? daemonStatus.status : 'not running') + const daemonStatusDetailText = `PID ${daemonStatus?.pid ? daemonStatus.pid : '—'}` const isDaemonManagementPaused = !settings.manageBuiltInDaemon - const daemonActionLabel = managedStatus?.status === 'running' ? 'Restart daemon' : 'Start daemon' + const daemonActionLabel = daemonStatus?.status === 'running' ? 'Restart daemon' : 'Start daemon' const daemonActionMessage = - managedStatus?.status === 'running' + daemonStatus?.status === 'running' ? 'Restarts the built-in daemon.' : 'Starts the built-in daemon.' const handleUpdateLocalDaemon = useCallback(() => { - if (!showSection) { - return - } - if (isRestartingDaemon) { + if (!showSection || isRestartingDaemon) { return } void confirmDialog({ title: daemonActionLabel, message: - managedStatus?.status === 'running' + daemonStatus?.status === 'running' ? 'This will restart the built-in daemon. The app will reconnect automatically.' : 'This will start the built-in daemon.', confirmLabel: daemonActionLabel, @@ -128,19 +126,18 @@ export function LocalDaemonSection({ setIsRestartingDaemon(true) setStatusMessage(null) - const action = - managedStatus?.status === 'running' ? restartManagedDaemon : startManagedDaemon + const action = daemonStatus?.status === 'running' ? restartDesktopDaemon : startDesktopDaemon void action() .then((status) => { - setManagedStatus(status) + setDaemonStatus(status) setStatusMessage( - managedStatus?.status === 'running' ? 'Daemon restarted.' : 'Daemon started.' + daemonStatus?.status === 'running' ? 'Daemon restarted.' : 'Daemon started.' ) - return loadManagedStatus() + return loadDaemonData() }) .catch((error) => { - console.error('[Settings] Failed to change managed daemon state', error) + console.error('[Settings] Failed to change desktop daemon state', error) const message = error instanceof Error ? error.message : String(error) setStatusMessage(`${daemonActionLabel} failed: ${message}`) }) @@ -149,10 +146,10 @@ export function LocalDaemonSection({ }) }) .catch((error) => { - console.error('[Settings] Failed to open managed daemon action confirmation', error) + console.error('[Settings] Failed to open desktop daemon action confirmation', error) Alert.alert('Error', 'Unable to open the daemon confirmation dialog.') }) - }, [daemonActionLabel, isRestartingDaemon, loadManagedStatus, managedStatus?.status, showSection]) + }, [daemonActionLabel, daemonStatus?.status, isRestartingDaemon, loadDaemonData, showSection]) const handleToggleDaemonManagement = useCallback(() => { if (isUpdatingDaemonManagement) { @@ -193,13 +190,13 @@ export function LocalDaemonSection({ setStatusMessage(null) const stopPromise = - managedStatus?.status === 'running' - ? stopManagedDaemon() - : Promise.resolve(managedStatus ?? null) + daemonStatus?.status === 'running' + ? stopDesktopDaemon() + : Promise.resolve(daemonStatus ?? null) void stopPromise .then(() => updateSettings({ manageBuiltInDaemon: false })) - .then(() => loadManagedStatus()) + .then(() => loadDaemonData()) .then(() => { setStatusMessage('Built-in daemon paused and stopped.') }) @@ -216,9 +213,9 @@ export function LocalDaemonSection({ Alert.alert('Error', 'Unable to open the daemon confirmation dialog.') }) }, [ + daemonStatus, isUpdatingDaemonManagement, - loadManagedStatus, - managedStatus, + loadDaemonData, settings.manageBuiltInDaemon, updateSettings, ]) @@ -258,7 +255,7 @@ export function LocalDaemonSection({ }, [cliSymlinkInstructions?.commands]) const handleCopyLogPath = useCallback(() => { - const logPath = managedLogs?.logPath + const logPath = daemonLogs?.logPath if (!logPath) { return } @@ -271,14 +268,14 @@ export function LocalDaemonSection({ console.error('[Settings] Failed to copy log path', error) Alert.alert('Error', 'Unable to copy log path.') }) - }, [managedLogs?.logPath]) + }, [daemonLogs?.logPath]) const handleOpenLogs = useCallback(() => { - if (!managedLogs) { + if (!daemonLogs) { return } setIsLogsModalOpen(true) - }, [managedLogs]) + }, [daemonLogs]) const handleOpenPairingModal = useCallback(() => { if (isLoadingPairing) { @@ -289,7 +286,7 @@ export function LocalDaemonSection({ setIsLoadingPairing(true) setPairingStatusMessage(null) - void getManagedDaemonPairing() + void getDesktopDaemonPairing() .then((pairing) => { setPairingOffer(pairing) if (!pairing.relayEnabled || !pairing.url) { @@ -344,7 +341,7 @@ export function LocalDaemonSection({ Status - Only the built-in managed daemon is shown here. + Only the built-in desktop daemon is shown here. {daemonStatusStateText} @@ -398,7 +395,7 @@ export function LocalDaemonSection({ disabled={isRestartingDaemon} > {isRestartingDaemon - ? managedStatus?.status === 'running' + ? daemonStatus?.status === 'running' ? 'Restarting...' : 'Starting...' : daemonActionLabel} @@ -425,10 +422,10 @@ export function LocalDaemonSection({ Log file - {managedLogs?.logPath ?? 'Log path unavailable.'} + {daemonLogs?.logPath ?? 'Log path unavailable.'} - {managedLogs?.logPath ? ( + {daemonLogs?.logPath ? ( @@ -523,9 +520,9 @@ export function LocalDaemonSection({ snapPoints={['70%', '92%']} > - {managedLogs?.logPath ?? 'Log path unavailable.'} + {daemonLogs?.logPath ?? 'Log path unavailable.'} - {managedLogs?.contents.length ? managedLogs.contents : '(log file is empty)'} + {daemonLogs?.contents.length ? daemonLogs.contents : '(log file is empty)'} @@ -537,7 +534,7 @@ const ADVANCED_DAEMON_SETTINGS_URL = 'https://paseo.sh/docs/configuration' function PairingOfferDialogContent(input: { isLoading: boolean - pairingOffer: ManagedPairingOffer | null + pairingOffer: DesktopPairingOffer | null statusMessage: string | null onCopyLink: () => void }) { diff --git a/packages/app/src/utils/managed-tauri-daemon-transport.test.ts b/packages/app/src/desktop/daemon/desktop-daemon-transport.test.ts similarity index 67% rename from packages/app/src/utils/managed-tauri-daemon-transport.test.ts rename to packages/app/src/desktop/daemon/desktop-daemon-transport.test.ts index 978b03295..80153d3f8 100644 --- a/packages/app/src/utils/managed-tauri-daemon-transport.test.ts +++ b/packages/app/src/desktop/daemon/desktop-daemon-transport.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi, beforeEach } from "vitest"; -const managedRuntimeMock = vi.hoisted(() => { +const desktopDaemonMock = vi.hoisted(() => { let eventHandler: ((payload: { sessionId: string; kind: "open" | "message" | "close" | "error"; @@ -40,32 +40,32 @@ const managedRuntimeMock = vi.hoisted(() => { }; }); -vi.mock("@/desktop/managed-runtime/managed-runtime", () => ({ - openLocalTransportSession: managedRuntimeMock.openLocalTransportSession, - listenToLocalTransportEvents: managedRuntimeMock.listenToLocalTransportEvents, - sendLocalTransportMessage: managedRuntimeMock.sendLocalTransportMessage, - closeLocalTransportSession: managedRuntimeMock.closeLocalTransportSession, +vi.mock("./desktop-daemon", () => ({ + openLocalTransportSession: desktopDaemonMock.openLocalTransportSession, + listenToLocalTransportEvents: desktopDaemonMock.listenToLocalTransportEvents, + sendLocalTransportMessage: desktopDaemonMock.sendLocalTransportMessage, + closeLocalTransportSession: desktopDaemonMock.closeLocalTransportSession, })); -describe("managed-tauri-daemon-transport", () => { +describe("desktop-daemon-transport", () => { beforeEach(() => { - managedRuntimeMock.openLocalTransportSession.mockReset(); - managedRuntimeMock.listenToLocalTransportEvents.mockClear(); - managedRuntimeMock.sendLocalTransportMessage.mockClear(); - managedRuntimeMock.closeLocalTransportSession.mockClear(); + desktopDaemonMock.openLocalTransportSession.mockReset(); + desktopDaemonMock.listenToLocalTransportEvents.mockClear(); + desktopDaemonMock.sendLocalTransportMessage.mockClear(); + desktopDaemonMock.closeLocalTransportSession.mockClear(); }); it("emits open after the session resolves even if the rust open event raced earlier", async () => { let resolveSession!: (sessionId: string) => void; - managedRuntimeMock.openLocalTransportSession.mockImplementation( + desktopDaemonMock.openLocalTransportSession.mockImplementation( () => new Promise((resolve) => { resolveSession = resolve; }) ); - const mod = await import("./managed-tauri-daemon-transport"); - const transportFactory = mod.createTauriLocalDaemonTransportFactory(); + const mod = await import("./desktop-daemon-transport"); + const transportFactory = mod.createDesktopLocalDaemonTransportFactory(); expect(transportFactory).not.toBeNull(); const transport = transportFactory!({ @@ -75,7 +75,7 @@ describe("managed-tauri-daemon-transport", () => { const onOpen = vi.fn(); transport.onOpen(onOpen); - managedRuntimeMock.emitEvent({ + desktopDaemonMock.emitEvent({ sessionId: "local-session-1", kind: "open", }); @@ -93,21 +93,21 @@ describe("managed-tauri-daemon-transport", () => { let resolveListen!: (cleanup: () => void) => void; const cleanup = vi.fn(); - managedRuntimeMock.openLocalTransportSession.mockImplementation( + desktopDaemonMock.openLocalTransportSession.mockImplementation( () => new Promise((resolve) => { resolveSession = resolve; }) ); - managedRuntimeMock.listenToLocalTransportEvents.mockImplementation( + desktopDaemonMock.listenToLocalTransportEvents.mockImplementation( () => new Promise<() => void>((resolve) => { resolveListen = resolve; }) ); - const mod = await import("./managed-tauri-daemon-transport"); - const transportFactory = mod.createTauriLocalDaemonTransportFactory(); + const mod = await import("./desktop-daemon-transport"); + const transportFactory = mod.createDesktopLocalDaemonTransportFactory(); expect(transportFactory).not.toBeNull(); const transport = transportFactory!({ @@ -121,7 +121,7 @@ describe("managed-tauri-daemon-transport", () => { await Promise.resolve(); await Promise.resolve(); - expect(managedRuntimeMock.closeLocalTransportSession).toHaveBeenCalledWith("local-session-2"); + expect(desktopDaemonMock.closeLocalTransportSession).toHaveBeenCalledWith("local-session-2"); expect(cleanup).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/app/src/utils/managed-tauri-daemon-transport.ts b/packages/app/src/desktop/daemon/desktop-daemon-transport.ts similarity index 97% rename from packages/app/src/utils/managed-tauri-daemon-transport.ts rename to packages/app/src/desktop/daemon/desktop-daemon-transport.ts index cc663f7d6..890fac2d3 100644 --- a/packages/app/src/utils/managed-tauri-daemon-transport.ts +++ b/packages/app/src/desktop/daemon/desktop-daemon-transport.ts @@ -5,7 +5,7 @@ import { openLocalTransportSession, sendLocalTransportMessage, type LocalTransportTarget, -} from "@/desktop/managed-runtime/managed-runtime"; +} from "./desktop-daemon"; const LOCAL_TRANSPORT_SCHEME = "paseo+local:"; @@ -49,7 +49,7 @@ function parseLocalDaemonTransportUrl(url: string): LocalTransportTarget { }; } -export function createTauriLocalDaemonTransportFactory(): DaemonTransportFactory | null { +export function createDesktopLocalDaemonTransportFactory(): DaemonTransportFactory | null { return ({ url }) => { const target = parseLocalDaemonTransportUrl(url); let sessionId: string | null = null; diff --git a/packages/app/src/desktop/managed-runtime/managed-runtime.ts b/packages/app/src/desktop/daemon/desktop-daemon.ts similarity index 58% rename from packages/app/src/desktop/managed-runtime/managed-runtime.ts rename to packages/app/src/desktop/daemon/desktop-daemon.ts index 49cb0739e..6ec353408 100644 --- a/packages/app/src/desktop/managed-runtime/managed-runtime.ts +++ b/packages/app/src/desktop/daemon/desktop-daemon.ts @@ -1,29 +1,24 @@ -import { invokeDesktopCommand } from '@/desktop/tauri/invoke-desktop-command' -import { getTauri, isTauriEnvironment } from '@/utils/tauri' +import { getDesktopHost, isDesktop } from '@/desktop/host' +import { invokeDesktopCommand } from '@/desktop/electron/invoke' -export type ManagedRuntimeStatus = { - runtimeId: string - runtimeVersion: string - runtimeRoot: string -} +export type DesktopDaemonState = 'starting' | 'running' | 'stopped' | 'errored' -export type ManagedDaemonStatus = { - runtimeId: string - runtimeVersion: string +export type DesktopDaemonStatus = { serverId: string - status: string + status: DesktopDaemonState listen: string hostname: string | null pid: number | null home: string + error: string | null } -export type ManagedDaemonLogs = { +export type DesktopDaemonLogs = { logPath: string contents: string } -export type ManagedPairingOffer = { +export type DesktopPairingOffer = { relayEnabled: boolean url: string | null qr: string | null @@ -35,12 +30,6 @@ export type CliSymlinkInstructions = { commands: string } -export type ManagedTcpSettings = { - enabled: boolean - host: string - port: number -} - export type LocalTransportTarget = { transportType: 'socket' | 'pipe' transportPath: string @@ -68,36 +57,42 @@ function toNumberOrNull(value: unknown): number | null { return typeof value === 'number' && Number.isFinite(value) ? value : null } -function parseManagedRuntimeStatus(raw: unknown): ManagedRuntimeStatus { - if (!isRecord(raw)) { - throw new Error('Unexpected managed runtime status response.') - } - return { - runtimeId: toStringOrNull(raw.runtimeId) ?? '', - runtimeVersion: toStringOrNull(raw.runtimeVersion) ?? '', - runtimeRoot: toStringOrNull(raw.runtimeRoot) ?? '', +function parseDesktopDaemonState(value: unknown): DesktopDaemonState { + const normalized = toStringOrNull(value)?.toLowerCase() + switch (normalized) { + case 'starting': + return 'starting' + case 'running': + return 'running' + case 'errored': + case 'error': + return 'errored' + case 'stopped': + case 'stopping': + case 'unknown': + default: + return 'stopped' } } -function parseManagedDaemonStatus(raw: unknown): ManagedDaemonStatus { +function parseDesktopDaemonStatus(raw: unknown): DesktopDaemonStatus { if (!isRecord(raw)) { - throw new Error('Unexpected managed daemon status response.') + throw new Error('Unexpected desktop daemon status response.') } return { - runtimeId: toStringOrNull(raw.runtimeId) ?? '', - runtimeVersion: toStringOrNull(raw.runtimeVersion) ?? '', serverId: toStringOrNull(raw.serverId) ?? '', - status: toStringOrNull(raw.status) ?? 'unknown', + status: parseDesktopDaemonState(raw.status), listen: toStringOrNull(raw.listen) ?? '', hostname: toStringOrNull(raw.hostname), pid: toNumberOrNull(raw.pid), home: toStringOrNull(raw.home) ?? '', + error: toStringOrNull(raw.error), } } -function parseManagedDaemonLogs(raw: unknown): ManagedDaemonLogs { +function parseDesktopDaemonLogs(raw: unknown): DesktopDaemonLogs { if (!isRecord(raw)) { - throw new Error('Unexpected managed daemon logs response.') + throw new Error('Unexpected desktop daemon logs response.') } return { logPath: toStringOrNull(raw.logPath) ?? '', @@ -105,9 +100,9 @@ function parseManagedDaemonLogs(raw: unknown): ManagedDaemonLogs { } } -function parseManagedPairingOffer(raw: unknown): ManagedPairingOffer { +function parseDesktopPairingOffer(raw: unknown): DesktopPairingOffer { if (!isRecord(raw)) { - throw new Error('Unexpected managed daemon pairing response.') + throw new Error('Unexpected desktop daemon pairing response.') } return { relayEnabled: raw.relayEnabled === true, @@ -127,36 +122,32 @@ function parseCliSymlinkInstructionsInternal(raw: unknown): CliSymlinkInstructio } } -export function shouldUseManagedDesktopDaemon(): boolean { - return isTauriEnvironment() && getTauri() !== null +export function shouldUseDesktopDaemon(): boolean { + return isDesktop() } -export async function getManagedRuntimeStatus(): Promise { - return parseManagedRuntimeStatus(await invokeDesktopCommand('managed_runtime_status')) +export async function getDesktopDaemonStatus(): Promise { + return parseDesktopDaemonStatus(await invokeDesktopCommand('desktop_daemon_status')) } -export async function getManagedDaemonStatus(): Promise { - return parseManagedDaemonStatus(await invokeDesktopCommand('managed_daemon_status')) +export async function startDesktopDaemon(): Promise { + return parseDesktopDaemonStatus(await invokeDesktopCommand('start_desktop_daemon')) } -export async function startManagedDaemon(): Promise { - return parseManagedDaemonStatus(await invokeDesktopCommand('start_managed_daemon')) +export async function stopDesktopDaemon(): Promise { + return parseDesktopDaemonStatus(await invokeDesktopCommand('stop_desktop_daemon')) } -export async function stopManagedDaemon(): Promise { - return parseManagedDaemonStatus(await invokeDesktopCommand('stop_managed_daemon')) +export async function restartDesktopDaemon(): Promise { + return parseDesktopDaemonStatus(await invokeDesktopCommand('restart_desktop_daemon')) } -export async function restartManagedDaemon(): Promise { - return parseManagedDaemonStatus(await invokeDesktopCommand('restart_managed_daemon')) +export async function getDesktopDaemonLogs(): Promise { + return parseDesktopDaemonLogs(await invokeDesktopCommand('desktop_daemon_logs')) } -export async function getManagedDaemonLogs(): Promise { - return parseManagedDaemonLogs(await invokeDesktopCommand('managed_daemon_logs')) -} - -export async function getManagedDaemonPairing(): Promise { - return parseManagedPairingOffer(await invokeDesktopCommand('managed_daemon_pairing')) +export async function getDesktopDaemonPairing(): Promise { + return parseDesktopPairingOffer(await invokeDesktopCommand('desktop_daemon_pairing')) } export function parseCliSymlinkInstructions(raw: unknown): CliSymlinkInstructions { @@ -171,27 +162,18 @@ export async function getCliSymlinkInstructions(): Promise { - return parseManagedDaemonStatus( - await invokeDesktopCommand('update_managed_daemon_tcp_settings', { settings }) - ) -} - export type LocalTransportEventUnlisten = () => void export type LocalTransportEventHandler = (payload: LocalTransportEventPayload) => void export async function listenToLocalTransportEvents( handler: LocalTransportEventHandler ): Promise { - const listen = getTauri()?.event?.listen + const listen = getDesktopHost()?.events?.on if (typeof listen !== 'function') { - throw new Error('Tauri event API is unavailable.') + throw new Error('Desktop events API is unavailable.') } - const unlisten = await listen('local-daemon-transport-event', (event: unknown) => { - const payload = isRecord(event) && isRecord(event.payload) ? event.payload : null - if (!payload) { + const unlisten = await listen('local-daemon-transport-event', (payload: unknown) => { + if (!isRecord(payload)) { return } handler({ diff --git a/packages/app/src/desktop/electron/events.ts b/packages/app/src/desktop/electron/events.ts new file mode 100644 index 000000000..3ab5743d1 --- /dev/null +++ b/packages/app/src/desktop/electron/events.ts @@ -0,0 +1,27 @@ +import { getDesktopHost } from "@/desktop/host"; + +export type DesktopEventUnlisten = () => void; + +type EventEnvelope = { + payload?: unknown; +}; + +export async function listenToDesktopEvent( + event: string, + handler: (payload: TPayload) => void +): Promise { + const listen = getDesktopHost()?.events?.on; + if (typeof listen !== "function") { + throw new Error("Desktop event API is unavailable."); + } + + const unlisten = await listen(event, (rawEvent: unknown) => { + const payload = + typeof rawEvent === "object" && rawEvent !== null && "payload" in rawEvent + ? (rawEvent as EventEnvelope).payload + : rawEvent; + handler(payload as TPayload); + }); + + return typeof unlisten === "function" ? unlisten : () => {}; +} diff --git a/packages/app/src/desktop/electron/host.ts b/packages/app/src/desktop/electron/host.ts new file mode 100644 index 000000000..e03cda5a0 --- /dev/null +++ b/packages/app/src/desktop/electron/host.ts @@ -0,0 +1,12 @@ +import type { DesktopHostBridge } from "@/desktop/host"; + +export function getElectronHost(): DesktopHostBridge | null { + if (typeof window === "undefined") { + return null; + } + const host = window.paseoDesktop; + if (!host || typeof host !== "object") { + return null; + } + return host; +} diff --git a/packages/app/src/desktop/tauri/invoke-desktop-command.ts b/packages/app/src/desktop/electron/invoke.ts similarity index 54% rename from packages/app/src/desktop/tauri/invoke-desktop-command.ts rename to packages/app/src/desktop/electron/invoke.ts index 04e07d776..7b4d88b22 100644 --- a/packages/app/src/desktop/tauri/invoke-desktop-command.ts +++ b/packages/app/src/desktop/electron/invoke.ts @@ -1,13 +1,12 @@ -import { getTauri } from "@/utils/tauri"; +import { getDesktopHost } from "@/desktop/host"; export async function invokeDesktopCommand( command: string, args?: Record ): Promise { - const invoke = getTauri()?.core?.invoke; + const invoke = getDesktopHost()?.invoke; if (typeof invoke !== "function") { - throw new Error("Tauri invoke() is unavailable in this environment."); + throw new Error("Desktop invoke() is unavailable in this environment."); } - return (await invoke(command, args)) as T; } diff --git a/packages/app/src/desktop/electron/window.ts b/packages/app/src/desktop/electron/window.ts new file mode 100644 index 000000000..96565a144 --- /dev/null +++ b/packages/app/src/desktop/electron/window.ts @@ -0,0 +1,37 @@ +import { getDesktopHost, type DesktopWindowBridge } from "@/desktop/host"; + +export function getDesktopWindow(): DesktopWindowBridge | null { + const getter = getDesktopHost()?.window?.getCurrentWindow; + if (typeof getter !== "function") { + return null; + } + try { + return getter() ?? null; + } catch { + return null; + } +} + +export async function startDesktopDragging(): Promise { + const win = getDesktopWindow(); + if (!win || typeof win.startDragging !== "function") { + return; + } + await win.startDragging(); +} + +export async function toggleDesktopMaximize(): Promise { + const win = getDesktopWindow(); + if (!win || typeof win.toggleMaximize !== "function") { + return; + } + await win.toggleMaximize(); +} + +export async function isDesktopFullscreen(): Promise { + const win = getDesktopWindow(); + if (!win || typeof win.isFullscreen !== "function") { + return false; + } + return await win.isFullscreen(); +} diff --git a/packages/app/src/desktop/host.ts b/packages/app/src/desktop/host.ts new file mode 100644 index 000000000..8711fd176 --- /dev/null +++ b/packages/app/src/desktop/host.ts @@ -0,0 +1,111 @@ +import { Platform } from "react-native"; +import { getElectronHost } from "@/desktop/electron/host"; + +export type DesktopNotificationPermission = "granted" | "denied" | "default"; + +export interface DesktopDialogAskOptions { + title?: string; + okLabel?: string; + cancelLabel?: string; + kind?: "info" | "warning" | "error"; +} + +export interface DesktopDialogOpenOptions { + title?: string; + defaultPath?: string; + directory?: boolean; + multiple?: boolean; + filters?: Array<{ + name: string; + extensions: string[]; + }>; +} + +export interface DesktopDialogBridge { + ask?: (message: string, options?: DesktopDialogAskOptions) => Promise; + open?: ( + options?: DesktopDialogOpenOptions + ) => Promise; +} + +export interface DesktopNotificationBridge { + isSupported?: () => Promise; + sendNotification?: ( + payload: string | { title: string; body?: string; data?: Record } + ) => Promise; +} + +export interface DesktopOpenerBridge { + openUrl?: (url: string) => Promise; +} + +export interface DesktopWindowBridge { + label?: string; + startDragging?: () => Promise; + toggleMaximize?: () => Promise; + isFullscreen?: () => Promise; + onResized?: ( + handler: (event: TEvent) => void + ) => Promise<() => void> | (() => void); + setBadgeCount?: (count?: number) => Promise; + onDragDropEvent?: ( + handler: (event: TEvent) => void + ) => Promise<() => void> | (() => void); +} + +export interface DesktopWindowModuleBridge { + getCurrentWindow?: () => DesktopWindowBridge; +} + +export interface DesktopEventsBridge { + on?: ( + event: string, + handler: (payload: unknown) => void + ) => Promise<() => void> | (() => void); +} + +export interface DesktopInvokeBridge { + invoke?: (command: string, args?: Record) => Promise; +} + +export interface DesktopHostBridge { + platform?: string; + invoke?: DesktopInvokeBridge["invoke"]; + events?: DesktopEventsBridge; + window?: DesktopWindowModuleBridge; + dialog?: DesktopDialogBridge; + notification?: DesktopNotificationBridge; + opener?: DesktopOpenerBridge; +} + +declare global { + interface Window { + paseoDesktop?: DesktopHostBridge; + } +} + +export function getDesktopHost(): DesktopHostBridge | null { + if (Platform.OS !== "web") { + return null; + } + return getElectronHost(); +} + +export function isDesktop(): boolean { + return getDesktopHost() !== null; +} + +export function isDesktopMac(): boolean { + if (!isDesktop()) { + return false; + } + if (typeof navigator === "undefined") { + return false; + } + const hostPlatform = getDesktopHost()?.platform?.toLowerCase(); + if (hostPlatform === "darwin" || hostPlatform === "mac" || hostPlatform === "macos") { + return true; + } + const ua = navigator.userAgent; + return ua.includes("Mac OS") || ua.includes("Macintosh"); +} diff --git a/packages/app/src/desktop/managed-runtime/managed-runtime.test.ts b/packages/app/src/desktop/managed-runtime/managed-runtime.test.ts deleted file mode 100644 index bb64d8771..000000000 --- a/packages/app/src/desktop/managed-runtime/managed-runtime.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { parseCliSymlinkInstructions } from "./managed-runtime"; - -describe("parseCliSymlinkInstructions", () => { - it("parses CLI symlink instructions from the desktop backend", () => { - expect( - parseCliSymlinkInstructions({ - title: "Add paseo to your shell", - detail: "Create a symlink to the Paseo desktop executable.", - commands: "sudo ln -sf /Applications/Paseo.app/Contents/MacOS/Paseo /usr/local/bin/paseo", - }) - ).toEqual({ - title: "Add paseo to your shell", - detail: "Create a symlink to the Paseo desktop executable.", - commands: "sudo ln -sf /Applications/Paseo.app/Contents/MacOS/Paseo /usr/local/bin/paseo", - }); - }); - - it("rejects non-object payloads", () => { - expect(() => parseCliSymlinkInstructions(null)).toThrow( - "Unexpected CLI symlink instructions response." - ); - }); -}); diff --git a/packages/app/src/desktop/notifications/desktop-notifications.ts b/packages/app/src/desktop/notifications/desktop-notifications.ts deleted file mode 100644 index 8d15d5304..000000000 --- a/packages/app/src/desktop/notifications/desktop-notifications.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { invokeDesktopCommand } from "@/desktop/tauri/invoke-desktop-command"; -import { getTauri } from "@/utils/tauri"; - -export const DESKTOP_NOTIFICATION_CLICK_EVENT = "desktop-notification-click"; - -export interface DesktopNotificationInput { - title: string; - body?: string; - data?: Record; -} - -export interface DesktopNotificationClickPayload { - data?: Record; -} - -export type DesktopNotificationClickHandler = ( - payload: DesktopNotificationClickPayload -) => void; - -export type DesktopNotificationClickUnlisten = () => void; - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -export async function sendDesktopNotification( - input: DesktopNotificationInput -): Promise { - try { - await invokeDesktopCommand("send_desktop_notification", { input }); - return true; - } catch (error) { - console.warn("[OSNotifications][Desktop] Failed to send desktop notification", error); - return false; - } -} - -export async function listenToDesktopNotificationClicks( - handler: DesktopNotificationClickHandler -): Promise { - const listen = getTauri()?.event?.listen; - if (typeof listen !== "function") { - throw new Error("Tauri event API is unavailable."); - } - - const unlisten = await listen(DESKTOP_NOTIFICATION_CLICK_EVENT, (event: unknown) => { - const payload = isRecord(event) && isRecord(event.payload) ? event.payload : null; - if (!payload) { - return; - } - - handler({ - data: isRecord(payload.data) ? payload.data : undefined, - }); - }); - - return typeof unlisten === "function" ? unlisten : () => {}; -} diff --git a/packages/app/src/desktop/permissions/desktop-permissions.test.ts b/packages/app/src/desktop/permissions/desktop-permissions.test.ts index a036f179b..532b2b347 100644 --- a/packages/app/src/desktop/permissions/desktop-permissions.test.ts +++ b/packages/app/src/desktop/permissions/desktop-permissions.test.ts @@ -4,14 +4,17 @@ type MockPlatform = 'web' | 'ios' | 'android' type GlobalSnapshot = { Notification: unknown - __TAURI__: unknown navigatorDescriptor?: PropertyDescriptor + paseoDesktop: unknown } const originalGlobals: GlobalSnapshot = { Notification: (globalThis as { Notification?: unknown }).Notification, - __TAURI__: (globalThis as { __TAURI__?: unknown }).__TAURI__, navigatorDescriptor: Object.getOwnPropertyDescriptor(globalThis, 'navigator'), + paseoDesktop: + typeof window === 'undefined' + ? undefined + : (window as { paseoDesktop?: unknown }).paseoDesktop, } function setNavigator(value: unknown): void { @@ -24,13 +27,16 @@ function setNavigator(value: unknown): void { function restoreGlobals(): void { ;(globalThis as { Notification?: unknown }).Notification = originalGlobals.Notification - ;(globalThis as { __TAURI__?: unknown }).__TAURI__ = originalGlobals.__TAURI__ if (originalGlobals.navigatorDescriptor) { Object.defineProperty(globalThis, 'navigator', originalGlobals.navigatorDescriptor) } else { delete (globalThis as { navigator?: unknown }).navigator } + + if (typeof window !== 'undefined') { + ;(window as { paseoDesktop?: unknown }).paseoDesktop = originalGlobals.paseoDesktop + } } async function loadModuleForPlatform(platform: MockPlatform) { @@ -47,20 +53,20 @@ describe('desktop-permissions', () => { restoreGlobals() }) - it('shows section only in Tauri web runtime', async () => { + it('shows section only in desktop web runtime', async () => { const { shouldShowDesktopPermissionSection } = await loadModuleForPlatform('web') expect(shouldShowDesktopPermissionSection()).toBe(false) - ;(globalThis as { __TAURI__?: unknown }).__TAURI__ = { notification: {} } + ;(window as { paseoDesktop?: unknown }).paseoDesktop = {} expect(shouldShowDesktopPermissionSection()).toBe(true) }) it('reads notification and microphone status', async () => { - const isPermissionGranted = vi.fn(async () => false) - ;(globalThis as { __TAURI__?: unknown }).__TAURI__ = { - notification: { isPermissionGranted }, + class MockNotification { + static permission = 'default' } + ;(globalThis as { Notification?: unknown }).Notification = MockNotification setNavigator({ permissions: { query: vi.fn(async () => ({ state: 'granted' })), @@ -73,9 +79,8 @@ describe('desktop-permissions', () => { const { getDesktopPermissionSnapshot } = await loadModuleForPlatform('web') const snapshot = await getDesktopPermissionSnapshot() - expect(snapshot.notifications.state).toBe('not-granted') + expect(snapshot.notifications.state).toBe('prompt') expect(snapshot.microphone.state).toBe('granted') - expect(isPermissionGranted).toHaveBeenCalledTimes(1) expect(snapshot.checkedAt).toBeTypeOf('number') }) @@ -127,20 +132,21 @@ describe('desktop-permissions', () => { ) }) - it('requests notification permission via Tauri', async () => { - const requestPermission = vi.fn(async () => 'granted') - ;(globalThis as { __TAURI__?: unknown }).__TAURI__ = { - notification: { requestPermission }, + it('requests notification permission via the browser Notification API', async () => { + class MockNotification { + static permission = 'default' + static requestPermission = vi.fn(async () => 'granted') } + ;(globalThis as { Notification?: unknown }).Notification = MockNotification const { requestDesktopPermission } = await loadModuleForPlatform('web') const result = await requestDesktopPermission({ kind: 'notifications' }) expect(result.state).toBe('granted') - expect(requestPermission).toHaveBeenCalledTimes(1) + expect(MockNotification.requestPermission).toHaveBeenCalledTimes(1) }) - it('falls back to browser Notification permission when Tauri API is unavailable', async () => { + it('reads browser Notification permission when available', async () => { class MockNotification { static permission = 'denied' } diff --git a/packages/app/src/desktop/permissions/desktop-permissions.ts b/packages/app/src/desktop/permissions/desktop-permissions.ts index 7632fa89c..2c4926ab9 100644 --- a/packages/app/src/desktop/permissions/desktop-permissions.ts +++ b/packages/app/src/desktop/permissions/desktop-permissions.ts @@ -1,5 +1,5 @@ import { Platform } from 'react-native' -import { getTauri, type TauriNotificationPermission } from '@/utils/tauri' +import { getDesktopHost } from '@/desktop/host' export type DesktopPermissionKind = 'notifications' | 'microphone' @@ -45,7 +45,7 @@ type NavigatorLike = { } export function shouldShowDesktopPermissionSection(): boolean { - return Platform.OS === 'web' && getTauri() !== null + return Platform.OS === 'web' && getDesktopHost() !== null } function status(input: DesktopPermissionStatus): DesktopPermissionStatus { @@ -132,27 +132,6 @@ function mapNotificationPermissionString(permission: string): DesktopPermissionS }) } -function mapTauriNotificationPermissionResult( - permission: TauriNotificationPermission -): DesktopPermissionStatus { - if (permission === 'granted') { - return status({ - state: 'granted', - detail: 'Notifications are allowed by the OS.', - }) - } - if (permission === 'denied') { - return status({ - state: 'denied', - detail: 'Notifications are denied in system settings.', - }) - } - return status({ - state: 'prompt', - detail: 'Notifications have not been granted yet.', - }) -} - async function getNotificationPermissionStatus(): Promise { if (Platform.OS !== 'web') { return status({ @@ -161,44 +140,30 @@ async function getNotificationPermissionStatus(): Promise { @@ -279,18 +244,11 @@ async function requestNotificationPermissionStatus(): Promise { diff --git a/packages/app/src/desktop/permissions/use-desktop-permissions.ts b/packages/app/src/desktop/permissions/use-desktop-permissions.ts index 8845b2834..782f005d6 100644 --- a/packages/app/src/desktop/permissions/use-desktop-permissions.ts +++ b/packages/app/src/desktop/permissions/use-desktop-permissions.ts @@ -122,7 +122,7 @@ export function useDesktopPermissions(): UseDesktopPermissionsReturn { try { const sent = await sendOsNotification({ title: "Paseo notification test", - body: "If you can see this, delivery works. Click it to verify the open flow.", + body: "If you can see this, desktop notifications work.", }); if (!sent) { console.warn("[Settings] Desktop test notification was not delivered"); diff --git a/packages/app/src/desktop/pick-directory.ts b/packages/app/src/desktop/pick-directory.ts index 9ce554411..1f52fc400 100644 --- a/packages/app/src/desktop/pick-directory.ts +++ b/packages/app/src/desktop/pick-directory.ts @@ -1,9 +1,9 @@ -import { getTauri } from '@/utils/tauri' +import { getDesktopHost } from '@/desktop/host' export async function pickDirectory(): Promise { - const open = getTauri()?.dialog?.open + const open = getDesktopHost()?.dialog?.open if (typeof open !== 'function') { - throw new Error('Tauri dialog open() is unavailable in this environment.') + throw new Error('Desktop dialog open() is unavailable in this environment.') } const selection = await open({ diff --git a/packages/app/src/desktop/updates/desktop-updates.ts b/packages/app/src/desktop/updates/desktop-updates.ts index 28546c68f..a7a11b7e1 100644 --- a/packages/app/src/desktop/updates/desktop-updates.ts +++ b/packages/app/src/desktop/updates/desktop-updates.ts @@ -1,6 +1,6 @@ import { Platform } from 'react-native' -import { getTauri } from '@/utils/tauri' -import { invokeDesktopCommand } from '@/desktop/tauri/invoke-desktop-command' +import { isDesktop } from '@/desktop/host' +import { invokeDesktopCommand } from '@/desktop/electron/invoke' export interface DesktopAppUpdateCheckResult { hasUpdate: boolean @@ -49,7 +49,7 @@ function toNumberOr(defaultValue: number, value: unknown): number { } export function shouldShowDesktopUpdateSection(): boolean { - return Platform.OS === 'web' && getTauri() !== null + return Platform.OS === 'web' && isDesktop() } export function parseLocalDaemonVersionResult(raw: unknown): LocalDaemonVersionResult { diff --git a/packages/app/src/hooks/image-attachment-picker.test.ts b/packages/app/src/hooks/image-attachment-picker.test.ts index 78a8ce0b1..6ccbfe28c 100644 --- a/packages/app/src/hooks/image-attachment-picker.test.ts +++ b/packages/app/src/hooks/image-attachment-picker.test.ts @@ -1,21 +1,21 @@ import { describe, expect, it, vi, beforeEach } from "vitest"; -const tauriState = vi.hoisted(() => ({ +const desktopHostState = vi.hoisted(() => ({ api: null as any, })); -vi.mock("@/utils/tauri", () => ({ - getTauri: () => tauriState.api, +vi.mock("@/desktop/host", () => ({ + getDesktopHost: () => desktopHostState.api, })); import { normalizePickedImageAssets, - openImagePathsWithTauriDialog, + openImagePathsWithDesktopDialog, } from "./image-attachment-picker"; describe("image-attachment-picker", () => { beforeEach(() => { - tauriState.api = null; + desktopHostState.api = null; }); it("normalizes a picked File into a blob source", async () => { @@ -69,13 +69,13 @@ describe("image-attachment-picker", () => { expect(result[0]?.mimeType).toBe("image/png"); }); - it("uses the tauri dialog api when available", async () => { + it("uses the desktop dialog api when available", async () => { const open = vi.fn().mockResolvedValue(["/tmp/one.png", "/tmp/two.jpg"]); - tauriState.api = { + desktopHostState.api = { dialog: { open }, }; - const result = await openImagePathsWithTauriDialog(); + const result = await openImagePathsWithDesktopDialog(); expect(open).toHaveBeenCalledWith( expect.objectContaining({ @@ -87,21 +87,11 @@ describe("image-attachment-picker", () => { expect(result).toEqual(["/tmp/one.png", "/tmp/two.jpg"]); }); - it("falls back to core invoke for the tauri dialog plugin", async () => { - const invoke = vi.fn().mockResolvedValue("/tmp/one.png"); - tauriState.api = { - core: { invoke }, - }; + it("throws when desktop dialog API is not available", async () => { + desktopHostState.api = {}; - const result = await openImagePathsWithTauriDialog(); - - expect(invoke).toHaveBeenCalledWith("plugin:dialog|open", { - options: expect.objectContaining({ - multiple: true, - directory: false, - title: "Attach images", - }), - }); - expect(result).toEqual(["/tmp/one.png"]); + await expect(openImagePathsWithDesktopDialog()).rejects.toThrow( + "Desktop dialog API is not available." + ); }); }); diff --git a/packages/app/src/hooks/image-attachment-picker.ts b/packages/app/src/hooks/image-attachment-picker.ts index a46e261de..bf28e2294 100644 --- a/packages/app/src/hooks/image-attachment-picker.ts +++ b/packages/app/src/hooks/image-attachment-picker.ts @@ -1,4 +1,4 @@ -import { getTauri } from "@/utils/tauri"; +import { getDesktopHost } from "@/desktop/host"; export type PickedImageSource = | { kind: "file_uri"; uri: string } @@ -77,15 +77,15 @@ export async function normalizePickedImageAssets( ); } -function normalizeTauriDialogSelection(selection: string | string[] | null): string[] { +function normalizeDesktopDialogSelection(selection: string | string[] | null): string[] { if (!selection) { return []; } return Array.isArray(selection) ? selection : [selection]; } -export async function openImagePathsWithTauriDialog(): Promise { - const tauri = getTauri(); +export async function openImagePathsWithDesktopDialog(): Promise { + const desktop = getDesktopHost(); const options = { directory: false, multiple: true, @@ -93,18 +93,10 @@ export async function openImagePathsWithTauriDialog(): Promise { title: "Attach images", }; - const dialogOpen = tauri?.dialog?.open; - if (typeof dialogOpen === "function") { - return normalizeTauriDialogSelection(await dialogOpen(options)); + const dialogOpen = desktop?.dialog?.open; + if (typeof dialogOpen !== "function") { + throw new Error("Desktop dialog API is not available."); } - const invoke = tauri?.core?.invoke; - if (typeof invoke !== "function") { - throw new Error("Tauri dialog API is not available."); - } - - const result = await invoke("plugin:dialog|open", { options }); - return normalizeTauriDialogSelection( - Array.isArray(result) || typeof result === "string" || result === null ? result : null - ); + return normalizeDesktopDialogSelection(await dialogOpen(options)); } diff --git a/packages/app/src/hooks/use-audio-recorder.web.ts b/packages/app/src/hooks/use-audio-recorder.web.ts index 353bbd830..9c8ab3f86 100644 --- a/packages/app/src/hooks/use-audio-recorder.web.ts +++ b/packages/app/src/hooks/use-audio-recorder.web.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { AttemptCancelledError, AttemptGuard } from "@/utils/attempt-guard"; -import { getTauri } from "@/utils/tauri"; +import { isDesktop } from "@/desktop/host"; export interface AudioCaptureConfig { sampleRate?: number; @@ -157,20 +157,20 @@ export function useAudioRecorder(config?: AudioCaptureConfig) { : true; const currentOrigin = typeof window !== "undefined" && window.location ? window.location.origin : "unknown"; - const isTauri = getTauri() !== null; + const isDesktopApp = isDesktop(); if (missingNavigator) { throw new Error("Microphone capture is not supported in this environment"); } - if (!secureContext && !isTauri) { + if (!secureContext && !isDesktopApp) { throw new Error( `Microphone access requires HTTPS or localhost. Current origin: ${currentOrigin}` ); } - if (!secureContext && isTauri) { + if (!secureContext && isDesktopApp) { console.warn( - "[AudioRecorder][Web] Insecure context reported under Tauri; attempting getUserMedia anyway", + "[AudioRecorder][Web] Insecure context reported under Desktop; attempting getUserMedia anyway", { currentOrigin } ); } diff --git a/packages/app/src/hooks/use-dictation-audio-source.web.ts b/packages/app/src/hooks/use-dictation-audio-source.web.ts index 64c2b99b2..c17707cb0 100644 --- a/packages/app/src/hooks/use-dictation-audio-source.web.ts +++ b/packages/app/src/hooks/use-dictation-audio-source.web.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { parsePcm16Wav } from "@/utils/pcm16-wav"; -import { getTauri } from "@/utils/tauri"; +import { isDesktop } from "@/desktop/host"; import type { DictationAudioSource, DictationAudioSourceConfig } from "./use-dictation-audio-source.types"; @@ -158,17 +158,17 @@ export function useDictationAudioSource(config: DictationAudioSourceConfig): Dic : true; const currentOrigin = typeof window !== "undefined" && window.location ? window.location.origin : "unknown"; - const isTauri = getTauri() !== null; + const isDesktopApp = isDesktop(); if (missingNavigator) { throw new Error("Microphone capture is not supported in this environment"); } - if (!secureContext && !isTauri) { + if (!secureContext && !isDesktopApp) { throw new Error(`Microphone access requires HTTPS or localhost. Current origin: ${currentOrigin}`); } - if (!secureContext && isTauri) { + if (!secureContext && isDesktopApp) { console.warn( - "[DictationAudio][Web] Insecure context reported under Tauri; attempting getUserMedia anyway", + "[DictationAudio][Web] Insecure context reported under Desktop; attempting getUserMedia anyway", { currentOrigin } ); } diff --git a/packages/app/src/hooks/use-favicon-status.ts b/packages/app/src/hooks/use-favicon-status.ts index 006867216..9fd9e736d 100644 --- a/packages/app/src/hooks/use-favicon-status.ts +++ b/packages/app/src/hooks/use-favicon-status.ts @@ -1,8 +1,8 @@ import { useEffect, useRef, useState } from "react"; import { Platform } from "react-native"; -import { getIsTauriMac } from "@/constants/layout"; +import { getIsDesktopMac } from "@/constants/layout"; import { useAggregatedAgents } from "./use-aggregated-agents"; -import { getCurrentTauriWindow } from "@/utils/tauri"; +import { getDesktopHost } from "@/desktop/host"; type FaviconStatus = "none" | "running" | "attention"; type ColorScheme = "dark" | "light"; @@ -92,15 +92,15 @@ function getSystemColorScheme(): ColorScheme { } async function updateMacDockBadge(count?: number) { - if (Platform.OS !== "web" || !getIsTauriMac()) return; + if (Platform.OS !== "web" || !getIsDesktopMac()) return; - const tauriWindow = getCurrentTauriWindow(); - if (!tauriWindow || typeof tauriWindow.setBadgeCount !== "function") { + const desktopWindow = getDesktopHost()?.window?.getCurrentWindow?.(); + if (!desktopWindow || typeof desktopWindow.setBadgeCount !== "function") { return; } try { - await tauriWindow.setBadgeCount(count); + await desktopWindow.setBadgeCount(count); } catch (error) { console.warn("[useFaviconStatus] Failed to update macOS dock badge", error); } diff --git a/packages/app/src/hooks/use-file-drop-zone.ts b/packages/app/src/hooks/use-file-drop-zone.ts index b5e7ccbea..d712f79e5 100644 --- a/packages/app/src/hooks/use-file-drop-zone.ts +++ b/packages/app/src/hooks/use-file-drop-zone.ts @@ -1,7 +1,7 @@ import { useState, useRef, useEffect } from "react"; import { Platform } from "react-native"; import type { ImageAttachment } from "@/components/message-input"; -import { getCurrentTauriWindow, getTauri } from "@/utils/tauri"; +import { getDesktopHost } from "@/desktop/host"; import { persistAttachmentFromBlob, persistAttachmentFromFileUri, @@ -33,7 +33,7 @@ const IMAGE_MIME_BY_EXTENSION: Record = { ".tiff": "image/tiff", }; -type TauriDragDropPayload = +type DesktopDragDropPayload = | { type: "enter"; paths: string[]; @@ -49,8 +49,8 @@ type TauriDragDropPayload = type: "leave"; }; -type TauriDragDropEvent = { - payload: TauriDragDropPayload; +type DesktopDragDropEvent = { + payload: DesktopDragDropPayload; }; function isImageFile(file: File): boolean { @@ -121,26 +121,27 @@ export function useFileDropZone({ didCleanup = true; try { void Promise.resolve(cleanupFn()).catch((error) => { - console.warn("[useFileDropZone] Failed to remove Tauri drag-drop listener:", error); + console.warn("[useFileDropZone] Failed to remove desktop drag-drop listener:", error); }); } catch (error) { - console.warn("[useFileDropZone] Failed to remove Tauri drag-drop listener:", error); + console.warn("[useFileDropZone] Failed to remove desktop drag-drop listener:", error); } } - async function setupTauriDragDrop(): Promise { - if (getTauri() === null) { + async function setupDesktopDragDrop(): Promise { + const desktopHost = getDesktopHost(); + if (desktopHost === null) { return false; } - const tauriWindow = getCurrentTauriWindow(); - if (!tauriWindow || typeof tauriWindow.onDragDropEvent !== "function") { + const desktopWindow = desktopHost.window?.getCurrentWindow?.(); + if (!desktopWindow || typeof desktopWindow.onDragDropEvent !== "function") { return false; } try { - const unlisten = await tauriWindow.onDragDropEvent( - (event: TauriDragDropEvent) => { + const unlisten = await desktopWindow.onDragDropEvent( + (event: DesktopDragDropEvent) => { const payload = event.payload; if (payload.type === "leave") { setIsDragging(false); @@ -185,7 +186,7 @@ export function useFileDropZone({ cleanup = unlisten; return true; } catch (error) { - console.warn("[useFileDropZone] Failed to listen for Tauri drag-drop:", error); + console.warn("[useFileDropZone] Failed to listen for desktop drag-drop:", error); return false; } } @@ -269,8 +270,8 @@ export function useFileDropZone({ } void (async () => { - const tauriListenersAttached = await setupTauriDragDrop(); - if (disposed || tauriListenersAttached) { + const desktopListenersAttached = await setupDesktopDragDrop(); + if (disposed || desktopListenersAttached) { return; } setupDomDragDrop(); diff --git a/packages/app/src/hooks/use-image-attachment-picker.ts b/packages/app/src/hooks/use-image-attachment-picker.ts index 89a344aa6..b1c268bfb 100644 --- a/packages/app/src/hooks/use-image-attachment-picker.ts +++ b/packages/app/src/hooks/use-image-attachment-picker.ts @@ -2,10 +2,10 @@ import { useCallback, useRef } from "react"; import { Alert } from "react-native"; import { Platform } from "react-native"; import * as ImagePicker from "expo-image-picker"; -import { isTauriEnvironment } from "@/utils/tauri"; +import { isDesktop } from "@/desktop/host"; import { normalizePickedImageAssets, - openImagePathsWithTauriDialog, + openImagePathsWithDesktopDialog, type PickedImageAttachmentInput, } from "@/hooks/image-attachment-picker"; @@ -42,8 +42,8 @@ export function useImageAttachmentPicker(): UseImageAttachmentPickerResult { isPickingRef.current = true; try { - if (Platform.OS === "web" && isTauriEnvironment()) { - const selectedPaths = await openImagePathsWithTauriDialog(); + if (Platform.OS === "web" && isDesktop()) { + const selectedPaths = await openImagePathsWithDesktopDialog(); if (selectedPaths.length === 0) { return null; } diff --git a/packages/app/src/hooks/use-is-local-daemon.ts b/packages/app/src/hooks/use-is-local-daemon.ts index 634c0d9ce..90744804d 100644 --- a/packages/app/src/hooks/use-is-local-daemon.ts +++ b/packages/app/src/hooks/use-is-local-daemon.ts @@ -1,15 +1,14 @@ import { useQuery } from '@tanstack/react-query' -import { getManagedDaemonStatus } from '@/desktop/managed-runtime/managed-runtime' -import { isTauriEnvironment } from '@/utils/tauri' +import { getDesktopDaemonStatus, shouldUseDesktopDaemon } from '@/desktop/daemon/desktop-daemon' -const MANAGED_DAEMON_SERVER_ID_QUERY_KEY = ['managed-daemon-server-id'] as const +const DESKTOP_DAEMON_SERVER_ID_QUERY_KEY = ['desktop-daemon-server-id'] as const -interface ManagedDaemonServerIdResult { +interface DesktopDaemonServerIdResult { serverId: string | null } -async function loadManagedDaemonServerId(): Promise { - const status = await getManagedDaemonStatus() +async function loadDesktopDaemonServerId(): Promise { + const status = await getDesktopDaemonStatus() const serverId = status.serverId.trim() return { serverId: serverId.length > 0 ? serverId : null, @@ -18,14 +17,15 @@ async function loadManagedDaemonServerId(): Promise export function useIsLocalDaemon(serverId: string): boolean { const normalizedServerId = serverId.trim() - const isDesktop = isTauriEnvironment() + const isDesktop = shouldUseDesktopDaemon() const query = useQuery({ - queryKey: MANAGED_DAEMON_SERVER_ID_QUERY_KEY, - queryFn: loadManagedDaemonServerId, + queryKey: DESKTOP_DAEMON_SERVER_ID_QUERY_KEY, + queryFn: loadDesktopDaemonServerId, enabled: isDesktop, staleTime: Infinity, gcTime: Infinity, + refetchInterval: (query) => (query.state.data?.serverId ? false : 1000), refetchOnMount: false, refetchOnReconnect: false, refetchOnWindowFocus: false, diff --git a/packages/app/src/hooks/use-keyboard-shortcuts.ts b/packages/app/src/hooks/use-keyboard-shortcuts.ts index a5b2ccea7..cd121f7e9 100644 --- a/packages/app/src/hooks/use-keyboard-shortcuts.ts +++ b/packages/app/src/hooks/use-keyboard-shortcuts.ts @@ -1,7 +1,7 @@ import { useEffect } from "react"; import { Platform } from "react-native"; import { usePathname } from "expo-router"; -import { getIsTauri } from "@/constants/layout"; +import { getIsDesktop } from "@/constants/layout"; import { useHosts } from "@/runtime/host-runtime"; import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store"; import { setCommandCenterFocusRestoreElement } from "@/utils/command-center-focus-restore"; @@ -50,7 +50,7 @@ export function useKeyboardShortcuts({ if (Platform.OS !== "web") return; if (isMobile) return; - const isTauri = getIsTauri(); + const isDesktopApp = getIsDesktop(); const isMac = getShortcutOs() === "mac"; const shouldHandle = () => { @@ -259,7 +259,7 @@ export function useKeyboardShortcuts({ if (key === "Alt" && !event.shiftKey) { useKeyboardShortcutsStore.getState().setAltDown(true); } - if (isTauri && (key === "Meta" || key === "Control") && !event.shiftKey) { + if (isDesktopApp && (key === "Meta" || key === "Control") && !event.shiftKey) { useKeyboardShortcutsStore.getState().setCmdOrCtrlDown(true); } if (key === "Shift") { @@ -278,7 +278,7 @@ export function useKeyboardShortcuts({ event, context: { isMac, - isTauri, + isDesktop: isDesktopApp, focusScope, commandCenterOpen: store.commandCenterOpen, hasSelectedAgent: canToggleFileExplorerShortcut({ @@ -314,7 +314,7 @@ export function useKeyboardShortcuts({ if (key === "Alt") { useKeyboardShortcutsStore.getState().setAltDown(false); } - if (isTauri && (key === "Meta" || key === "Control")) { + if (isDesktopApp && (key === "Meta" || key === "Control")) { useKeyboardShortcutsStore.getState().setCmdOrCtrlDown(false); } }; diff --git a/packages/app/src/keyboard/keyboard-shortcuts.test.ts b/packages/app/src/keyboard/keyboard-shortcuts.test.ts index f7a38a1b9..ed1afbd86 100644 --- a/packages/app/src/keyboard/keyboard-shortcuts.test.ts +++ b/packages/app/src/keyboard/keyboard-shortcuts.test.ts @@ -23,7 +23,7 @@ function shortcutContext( ): KeyboardShortcutContext { return { isMac: false, - isTauri: false, + isDesktop: false, focusScope: "other", commandCenterOpen: false, hasSelectedAgent: true, @@ -84,7 +84,7 @@ type HelpSectionCase = { name: string; context: { isMac: boolean; - isTauri: boolean; + isDesktop: boolean; }; expectedKeys: Record; }; @@ -106,42 +106,42 @@ describe("keyboard-shortcuts", () => { { name: "matches workspace index jump on web via Alt+digit", event: { key: "2", code: "Digit2", altKey: true }, - context: { isTauri: false }, + context: { isDesktop: false }, action: "workspace.navigate.index", payload: { index: 2 }, }, { - name: "matches workspace index jump on tauri via Mod+digit", + name: "matches workspace index jump on desktop via Mod+digit", event: { key: "2", code: "Digit2", metaKey: true }, - context: { isMac: true, isTauri: true }, + context: { isMac: true, isDesktop: true }, action: "workspace.navigate.index", payload: { index: 2 }, }, { - name: "matches tab index jump on tauri via Alt+digit", + name: "matches tab index jump on desktop via Alt+digit", event: { key: "2", code: "Digit2", altKey: true }, - context: { isTauri: true }, + context: { isDesktop: true }, action: "workspace.tab.navigate.index", payload: { index: 2 }, }, { name: "matches tab index jump on web via Alt+Shift+digit", event: { key: "@", code: "Digit2", altKey: true, shiftKey: true }, - context: { isTauri: false }, + context: { isDesktop: false }, action: "workspace.tab.navigate.index", payload: { index: 2 }, }, { name: "matches workspace relative navigation on web via Alt+[", event: { key: "[", code: "BracketLeft", altKey: true }, - context: { isTauri: false }, + context: { isDesktop: false }, action: "workspace.navigate.relative", payload: { delta: -1 }, }, { - name: "matches workspace relative navigation on tauri via Mod+]", + name: "matches workspace relative navigation on desktop via Mod+]", event: { key: "]", code: "BracketRight", ctrlKey: true }, - context: { isTauri: true }, + context: { isDesktop: true }, action: "workspace.navigate.relative", payload: { delta: 1 }, }, @@ -160,13 +160,13 @@ describe("keyboard-shortcuts", () => { { name: "matches Alt+Shift+W to close current tab on web", event: { key: "W", code: "KeyW", altKey: true, shiftKey: true }, - context: { isTauri: false }, + context: { isDesktop: false }, action: "workspace.tab.close.current", }, { - name: "matches Mod+W to close current tab on tauri", + name: "matches Mod+W to close current tab on desktop", event: { key: "w", code: "KeyW", metaKey: true }, - context: { isMac: true, isTauri: true }, + context: { isMac: true, isDesktop: true }, action: "workspace.tab.close.current", }, { @@ -312,7 +312,7 @@ describe("keyboard-shortcut help sections", () => { const helpCases: HelpSectionCase[] = [ { name: "uses web defaults for workspace and tab jump", - context: { isMac: true, isTauri: false }, + context: { isMac: true, isDesktop: false }, expectedKeys: { "new-agent": ["mod", "shift", "O"], "workspace-tab-new": ["mod", "T"], @@ -324,8 +324,8 @@ describe("keyboard-shortcut help sections", () => { }, }, { - name: "uses tauri defaults for workspace and tab jump", - context: { isMac: true, isTauri: true }, + name: "uses desktop defaults for workspace and tab jump", + context: { isMac: true, isDesktop: true }, expectedKeys: { "new-agent": ["mod", "shift", "O"], "workspace-tab-new": ["mod", "T"], @@ -338,7 +338,7 @@ describe("keyboard-shortcut help sections", () => { }, { name: "uses mod+period as non-mac left sidebar shortcut", - context: { isMac: false, isTauri: false }, + context: { isMac: false, isDesktop: false }, expectedKeys: { "toggle-left-sidebar": ["mod", "."], }, diff --git a/packages/app/src/keyboard/keyboard-shortcuts.ts b/packages/app/src/keyboard/keyboard-shortcuts.ts index 09bc44e41..399df6723 100644 --- a/packages/app/src/keyboard/keyboard-shortcuts.ts +++ b/packages/app/src/keyboard/keyboard-shortcuts.ts @@ -8,7 +8,7 @@ import type { export type KeyboardShortcutContext = { isMac: boolean; - isTauri: boolean; + isDesktop: boolean; focusScope: KeyboardFocusScope; commandCenterOpen: boolean; hasSelectedAgent: boolean; @@ -36,7 +36,7 @@ export type KeyboardShortcutHelpSection = { type KeyboardShortcutPlatformContext = { isMac: boolean; - isTauri: boolean; + isDesktop: boolean; }; type KeyboardShortcutHelpEntry = { @@ -158,20 +158,20 @@ const SHORTCUT_BINDINGS: readonly KeyboardShortcutBinding[] = [ }, }, { - id: "workspace-tab-close-current-mod-w-tauri", + id: "workspace-tab-close-current-mod-w-desktop", action: "workspace.tab.close.current", matches: (event) => isMod(event) && !event.altKey && !event.shiftKey && (event.code === "KeyW" || event.key.toLowerCase() === "w"), - when: (context) => context.isTauri && !context.commandCenterOpen, + when: (context) => context.isDesktop && !context.commandCenterOpen, help: { id: "workspace-tab-close-current", section: "global", label: "Close current tab", keys: ["mod", "W"], - when: (context) => context.isTauri, + when: (context) => context.isDesktop, }, }, { @@ -183,17 +183,17 @@ const SHORTCUT_BINDINGS: readonly KeyboardShortcutBinding[] = [ event.altKey && event.shiftKey && (event.code === "KeyW" || event.key.toLowerCase() === "w"), - when: (context) => !context.isTauri && !context.commandCenterOpen, + when: (context) => !context.isDesktop && !context.commandCenterOpen, help: { id: "workspace-tab-close-current", section: "global", label: "Close current tab", keys: ["alt", "shift", "W"], - when: (context) => !context.isTauri, + when: (context) => !context.isDesktop, }, }, { - id: "workspace-navigate-index-mod-digit-tauri", + id: "workspace-navigate-index-mod-digit-desktop", action: "workspace.navigate.index", matches: (event) => isMod(event) && @@ -201,13 +201,13 @@ const SHORTCUT_BINDINGS: readonly KeyboardShortcutBinding[] = [ !event.shiftKey && hasDigit(event), payload: withIndexPayload, - when: (context) => context.isTauri && !context.commandCenterOpen, + when: (context) => context.isDesktop && !context.commandCenterOpen, help: { id: "workspace-jump-index", section: "global", label: "Jump to workspace", keys: ["mod", "1-9"], - when: (context) => context.isTauri, + when: (context) => context.isDesktop, }, }, { @@ -220,17 +220,17 @@ const SHORTCUT_BINDINGS: readonly KeyboardShortcutBinding[] = [ !event.shiftKey && hasDigit(event), payload: withIndexPayload, - when: (context) => !context.isTauri && !context.commandCenterOpen, + when: (context) => !context.isDesktop && !context.commandCenterOpen, help: { id: "workspace-jump-index", section: "global", label: "Jump to workspace", keys: ["alt", "1-9"], - when: (context) => !context.isTauri, + when: (context) => !context.isDesktop, }, }, { - id: "workspace-tab-navigate-index-alt-digit-tauri", + id: "workspace-tab-navigate-index-alt-digit-desktop", action: "workspace.tab.navigate.index", matches: (event) => !event.metaKey && @@ -239,13 +239,13 @@ const SHORTCUT_BINDINGS: readonly KeyboardShortcutBinding[] = [ !event.shiftKey && hasDigit(event), payload: withIndexPayload, - when: (context) => context.isTauri && !context.commandCenterOpen, + when: (context) => context.isDesktop && !context.commandCenterOpen, help: { id: "workspace-tab-jump-index", section: "global", label: "Jump to tab", keys: ["alt", "1-9"], - when: (context) => context.isTauri, + when: (context) => context.isDesktop, }, }, { @@ -258,13 +258,13 @@ const SHORTCUT_BINDINGS: readonly KeyboardShortcutBinding[] = [ event.shiftKey && hasDigit(event), payload: withIndexPayload, - when: (context) => !context.isTauri && !context.commandCenterOpen, + when: (context) => !context.isDesktop && !context.commandCenterOpen, help: { id: "workspace-tab-jump-index", section: "global", label: "Jump to tab", keys: ["alt", "shift", "1-9"], - when: (context) => !context.isTauri, + when: (context) => !context.isDesktop, }, }, { @@ -276,13 +276,13 @@ const SHORTCUT_BINDINGS: readonly KeyboardShortcutBinding[] = [ !event.shiftKey && (event.code === "BracketLeft" || event.key === "["), payload: withRelativeDelta(-1), - when: (context) => context.isTauri && !context.commandCenterOpen, + when: (context) => context.isDesktop && !context.commandCenterOpen, help: { id: "workspace-prev", section: "global", label: "Previous workspace", keys: ["mod", "["], - when: (context) => context.isTauri, + when: (context) => context.isDesktop, }, }, { @@ -294,13 +294,13 @@ const SHORTCUT_BINDINGS: readonly KeyboardShortcutBinding[] = [ !event.shiftKey && (event.code === "BracketRight" || event.key === "]"), payload: withRelativeDelta(1), - when: (context) => context.isTauri && !context.commandCenterOpen, + when: (context) => context.isDesktop && !context.commandCenterOpen, help: { id: "workspace-next", section: "global", label: "Next workspace", keys: ["mod", "]"], - when: (context) => context.isTauri, + when: (context) => context.isDesktop, }, }, { @@ -313,13 +313,13 @@ const SHORTCUT_BINDINGS: readonly KeyboardShortcutBinding[] = [ !event.shiftKey && (event.code === "BracketLeft" || event.key === "["), payload: withRelativeDelta(-1), - when: (context) => !context.isTauri && !context.commandCenterOpen, + when: (context) => !context.isDesktop && !context.commandCenterOpen, help: { id: "workspace-prev", section: "global", label: "Previous workspace", keys: ["alt", "["], - when: (context) => !context.isTauri, + when: (context) => !context.isDesktop, }, }, { @@ -332,13 +332,13 @@ const SHORTCUT_BINDINGS: readonly KeyboardShortcutBinding[] = [ !event.shiftKey && (event.code === "BracketRight" || event.key === "]"), payload: withRelativeDelta(1), - when: (context) => !context.isTauri && !context.commandCenterOpen, + when: (context) => !context.isDesktop && !context.commandCenterOpen, help: { id: "workspace-next", section: "global", label: "Next workspace", keys: ["alt", "]"], - when: (context) => !context.isTauri, + when: (context) => !context.isDesktop, }, }, { diff --git a/packages/app/src/runtime/host-runtime.ts b/packages/app/src/runtime/host-runtime.ts index fb31126f7..082407c45 100644 --- a/packages/app/src/runtime/host-runtime.ts +++ b/packages/app/src/runtime/host-runtime.ts @@ -16,9 +16,9 @@ import { import { decodeOfferFragmentPayload, normalizeHostPort } from "@/utils/daemon-endpoints"; import { ConnectionOfferSchema, type ConnectionOffer } from "@server/shared/connection-offer"; import { - shouldUseManagedDesktopDaemon, - startManagedDaemon, -} from "@/desktop/managed-runtime/managed-runtime"; + shouldUseDesktopDaemon, + startDesktopDaemon, +} from "@/desktop/daemon/desktop-daemon"; import { connectToDaemon } from "@/utils/test-daemon-connection"; import { buildDaemonWebSocketUrl, @@ -32,8 +32,8 @@ import { } from "@/utils/connection-selection"; import { buildLocalDaemonTransportUrl, - createTauriLocalDaemonTransportFactory, -} from "@/utils/managed-tauri-daemon-transport"; + createDesktopLocalDaemonTransportFactory, +} from "@/desktop/daemon/desktop-daemon-transport"; import { applyFetchedAgentDirectory } from "@/utils/agent-directory-sync"; import { useSessionStore, type Agent } from "@/stores/session-store"; @@ -432,7 +432,7 @@ function probeIntervalForConnection( function createDefaultDeps(): HostRuntimeControllerDeps { return { createClient: ({ host, connection, clientId, runtimeGeneration }) => { - const localTransportFactory = createTauriLocalDaemonTransportFactory(); + const localTransportFactory = createDesktopLocalDaemonTransportFactory(); const base = { suppressSendErrors: true, clientId, @@ -1188,7 +1188,7 @@ export class HostRuntimeStore { return; } - if (shouldUseManagedDesktopDaemon()) { + if (shouldUseDesktopDaemon()) { await this.bootstrapDesktop(); } else { await this.bootstrapLocalhost(); @@ -1197,43 +1197,22 @@ export class HostRuntimeStore { private async bootstrapDesktop(): Promise { try { - await Promise.allSettled([ - (async () => { - const daemon = await startManagedDaemon(); - if (!daemon.serverId) { - return; - } - const connection = connectionFromListen(daemon.listen); - if (!connection) { - return; - } - await this.upsertHostConnection({ - serverId: daemon.serverId, - label: daemon.hostname ?? undefined, - connection, - }); - })().catch((error) => { - console.warn("[HostRuntime] Failed to bootstrap desktop daemon connection", error); - }), - (async () => { - const { client, serverId, hostname } = await connectToDaemon( - { - id: `bootstrap:${DEFAULT_LOCALHOST_ENDPOINT}`, - type: "directTcp", - endpoint: DEFAULT_LOCALHOST_ENDPOINT, - }, - { timeoutMs: DEFAULT_LOCALHOST_BOOTSTRAP_TIMEOUT_MS } - ); - await this.upsertDirectConnection({ - serverId, - endpoint: DEFAULT_LOCALHOST_ENDPOINT, - label: hostname ?? undefined, - existingClient: client, - }); - })().catch(() => undefined), - ]); + const daemon = await startDesktopDaemon(); + const connection = connectionFromListen(daemon.listen); + if (!connection) { + return; + } + const { client, serverId, hostname } = await connectToDaemon(connection, { + timeoutMs: DEFAULT_LOCALHOST_BOOTSTRAP_TIMEOUT_MS, + }); + await this.upsertHostConnection({ + serverId, + label: hostname ?? daemon.hostname ?? undefined, + connection, + existingClient: client, + }); } catch (error) { - console.warn("[HostRuntime] Failed to bootstrap desktop startup host connections", error); + console.warn("[HostRuntime] Failed to bootstrap desktop daemon connection", error); } } diff --git a/packages/app/src/screens/agent/draft-agent-screen.tsx b/packages/app/src/screens/agent/draft-agent-screen.tsx index 01e042361..80e9dd327 100644 --- a/packages/app/src/screens/agent/draft-agent-screen.tsx +++ b/packages/app/src/screens/agent/draft-agent-screen.tsx @@ -47,7 +47,7 @@ import type { } from '@server/server/agent/agent-sdk-types' import { AGENT_PROVIDER_DEFINITIONS } from '@server/server/agent/provider-manifest' import { buildHostWorkspaceAgentRoute } from '@/utils/host-routes' -import { useTauriDragHandlers } from '@/utils/tauri-window' +import { useDesktopDragHandlers } from '@/utils/desktop-window' import { useKeyboardShiftStyle } from '@/hooks/use-keyboard-shift-style' import { normalizeAgentSnapshot } from '@/utils/agent-snapshots' import { useDraftAgentCreateFlow } from '@/hooks/use-draft-agent-create-flow' @@ -238,7 +238,7 @@ function DraftAgentScreenContent({ const activateExplorerTabForCheckout = usePanelStore( (state) => state.activateExplorerTabForCheckout ) - const tauriDragHandlers = useTauriDragHandlers() + const { style: dragRegionStyle, ...dragHandlers } = useDesktopDragHandlers() const isExplorerOpen = isMobile ? mobileView === 'file-explorer' : desktopFileExplorerOpen const draftIdRef = useRef(generateDraftId()) const draftAgentIdRef = useRef(generateDraftId()) @@ -974,7 +974,7 @@ function DraftAgentScreenContent({ const explorerServerId = draftExplorerCheckout?.serverId ?? null const explorerIsGit = draftExplorerCheckout?.isGit ?? false const mainContent = ( - + { if (!isMobile) { @@ -30,7 +30,7 @@ export function OpenProjectScreen({ serverId }: { serverId: string }) { }, [isMobile, openAgentList]); return ( - + diff --git a/packages/app/src/screens/settings-screen.tsx b/packages/app/src/screens/settings-screen.tsx index 9d04fdd9b..0759cdab2 100644 --- a/packages/app/src/screens/settings-screen.tsx +++ b/packages/app/src/screens/settings-screen.tsx @@ -41,6 +41,7 @@ import { import { AdaptiveModalSheet, AdaptiveTextInput } from "@/components/adaptive-modal-sheet"; import { DesktopPermissionsSection } from "@/desktop/components/desktop-permissions-section"; import { LocalDaemonSection } from "@/desktop/components/desktop-updates-section"; +import { isDesktop as isDesktopHost } from "@/desktop/host"; import { useDesktopAppUpdater } from "@/desktop/updates/use-desktop-app-updater"; import { formatVersionWithPrefix } from "@/desktop/updates/desktop-updates"; import { resolveAppVersion } from "@/utils/app-version"; @@ -513,7 +514,7 @@ export default function SettingsScreen() { const isLoading = settingsLoading; const isMountedRef = useRef(true); const lastHandledEditHostRef = useRef(null); - const isDesktop = Platform.OS === "web"; + const isDesktop = isDesktopHost(); const isLocalDaemon = useIsLocalDaemon(routeServerId); const appVersion = resolveAppVersion(); const appVersionText = formatVersionWithPrefix(appVersion); diff --git a/packages/app/src/screens/startup-splash-screen.tsx b/packages/app/src/screens/startup-splash-screen.tsx index 1468c593c..41a40987f 100644 --- a/packages/app/src/screens/startup-splash-screen.tsx +++ b/packages/app/src/screens/startup-splash-screen.tsx @@ -1,7 +1,7 @@ import { Text, View } from "react-native"; import { StyleSheet } from "react-native-unistyles"; import { PaseoLogo } from "@/components/icons/paseo-logo"; -import { useTauriDragHandlers } from "@/utils/tauri-window"; +import { useDesktopDragHandlers } from "@/utils/desktop-window"; const styles = StyleSheet.create((theme) => ({ container: { @@ -18,10 +18,10 @@ const styles = StyleSheet.create((theme) => ({ })); export function StartupSplashScreen() { - const dragHandlers = useTauriDragHandlers(); + const { style: dragRegionStyle, ...dragHandlers } = useDesktopDragHandlers(); return ( - + Starting up… diff --git a/packages/app/src/types/host-connection.ts b/packages/app/src/types/host-connection.ts index a815c78a8..97639a963 100644 --- a/packages/app/src/types/host-connection.ts +++ b/packages/app/src/types/host-connection.ts @@ -80,6 +80,17 @@ function hostLifecycleEquals(left: HostLifecycle, right: HostLifecycle): boolean return JSON.stringify(left) === JSON.stringify(right) } +function dedupeHostConnections(connections: HostConnection[]): HostConnection[] { + const next: HostConnection[] = [] + for (const connection of connections) { + if (next.some((existing) => hostConnectionEquals(existing, connection))) { + continue + } + next.push(connection) + } + return next +} + export function upsertHostConnectionInProfiles(input: { profiles: HostProfile[] serverId: string @@ -96,9 +107,17 @@ export function upsertHostConnectionInProfiles(input: { const labelTrimmed = input.label?.trim() ?? '' const derivedLabel = labelTrimmed || serverId const existing = input.profiles - const idx = existing.findIndex((daemon) => daemon.serverId === serverId) + const matchingIndexes = existing.reduce((matches, daemon, index) => { + if ( + daemon.serverId === serverId || + daemon.connections.some((connection) => hostConnectionEquals(connection, input.connection)) + ) { + matches.push(index) + } + return matches + }, []) - if (idx === -1) { + if (matchingIndexes.length === 0) { const profile: HostProfile = { serverId, label: derivedLabel, @@ -111,31 +130,37 @@ export function upsertHostConnectionInProfiles(input: { return [...existing, profile] } - const prev = existing[idx]! - const connectionIdx = prev.connections.findIndex((connection) => connection.id === input.connection.id) - const hadConnection = connectionIdx !== -1 - const connectionChanged = - connectionIdx === -1 - ? true - : !hostConnectionEquals(prev.connections[connectionIdx]!, input.connection) - const nextConnections = - connectionIdx === -1 - ? [...prev.connections, input.connection] - : connectionChanged - ? prev.connections.map((connection, index) => - index === connectionIdx ? input.connection : connection - ) - : prev.connections - + const matchedProfiles = matchingIndexes.map((index) => existing[index]!) + const prev = + matchedProfiles.find((daemon) => daemon.serverId === serverId) ?? matchedProfiles[0]! + const nextConnections = dedupeHostConnections([ + ...matchedProfiles.flatMap((daemon) => daemon.connections), + input.connection, + ]) const nextLifecycle = prev.lifecycle - const nextLabel = labelTrimmed ? labelTrimmed : prev.label - const nextPreferredConnectionId = prev.preferredConnectionId ?? input.connection.id + const nextLabel = + labelTrimmed || (prev.label === prev.serverId ? derivedLabel : prev.label) + const nextPreferredConnectionId = + prev.preferredConnectionId && + nextConnections.some((connection) => connection.id === prev.preferredConnectionId) + ? prev.preferredConnectionId + : input.connection.id + const nextCreatedAt = matchedProfiles.reduce( + (earliest, daemon) => (daemon.createdAt < earliest ? daemon.createdAt : earliest), + prev.createdAt + ) const changed = + matchingIndexes.length > 1 || + prev.serverId !== serverId || + nextCreatedAt !== prev.createdAt || nextLabel !== prev.label || nextPreferredConnectionId !== prev.preferredConnectionId || !hostLifecycleEquals(prev.lifecycle, nextLifecycle) || - !hadConnection || - connectionChanged + nextConnections.length !== prev.connections.length || + nextConnections.some((connection, index) => { + const previousConnection = prev.connections[index] + return !previousConnection || !hostConnectionEquals(connection, previousConnection) + }) if (!changed) { return existing @@ -143,15 +168,19 @@ export function upsertHostConnectionInProfiles(input: { const nextProfile: HostProfile = { ...prev, + serverId, label: nextLabel, lifecycle: nextLifecycle, connections: nextConnections, preferredConnectionId: nextPreferredConnectionId, + createdAt: nextCreatedAt, updatedAt: now, } - const next = [...existing] - next[idx] = nextProfile + const firstIndex = matchingIndexes[0]! + const matchingIndexSet = new Set(matchingIndexes) + const next = existing.filter((_daemon, index) => !matchingIndexSet.has(index)) + next.splice(firstIndex, 0, nextProfile) return next } diff --git a/packages/app/src/utils/confirm-dialog.test.ts b/packages/app/src/utils/confirm-dialog.test.ts index 8876611b1..682ebb893 100644 --- a/packages/app/src/utils/confirm-dialog.test.ts +++ b/packages/app/src/utils/confirm-dialog.test.ts @@ -1,5 +1,13 @@ import { afterEach, describe, expect, it, vi } from "vitest"; +const desktopHostState = { + api: null as { + dialog?: { + ask?: (message: string, options?: Record) => Promise; + }; + } | null, +}; + type MockPlatform = "web" | "ios" | "android"; type AlertButton = { @@ -19,13 +27,16 @@ async function loadModuleForPlatform(platform: MockPlatform): Promise<{ }, Platform: { OS: platform }, })); + vi.doMock("@/desktop/host", () => ({ + getDesktopHost: () => desktopHostState.api, + })); const module = await import("./confirm-dialog"); return { confirmDialog: module.confirmDialog, alertMock }; } function clearDialogGlobals(): void { - delete (globalThis as { __TAURI__?: unknown }).__TAURI__; + desktopHostState.api = null; delete (globalThis as { confirm?: unknown }).confirm; } @@ -37,13 +48,13 @@ describe("confirmDialog", () => { clearDialogGlobals(); }); - it("uses Tauri dialog.ask on web when available", async () => { + it("uses the desktop dialog bridge on web when available", async () => { const askMock = vi.fn(async () => true); const blurMock = vi.fn(); (globalThis as { document?: unknown }).document = { activeElement: { blur: blurMock }, } as unknown as Document; - (globalThis as { __TAURI__?: unknown }).__TAURI__ = { + desktopHostState.api = { dialog: { ask: askMock }, }; @@ -67,7 +78,7 @@ describe("confirmDialog", () => { }); }); - it("falls back to browser confirm on web when Tauri APIs are unavailable", async () => { + it("falls back to browser confirm on web when desktop APIs are unavailable", async () => { const browserConfirm = vi.fn(() => true); const blurMock = vi.fn(); (globalThis as { document?: unknown }).document = { diff --git a/packages/app/src/utils/confirm-dialog.ts b/packages/app/src/utils/confirm-dialog.ts index 4297b6574..1b950b458 100644 --- a/packages/app/src/utils/confirm-dialog.ts +++ b/packages/app/src/utils/confirm-dialog.ts @@ -1,5 +1,5 @@ import { Alert, Platform } from "react-native"; -import { getTauri, type TauriDialogAskOptions } from "@/utils/tauri"; +import { getDesktopHost, type DesktopDialogAskOptions } from "@/desktop/host"; export interface ConfirmDialogInput { title: string; @@ -48,14 +48,14 @@ async function showNativeConfirmDialog(input: ConfirmDialogInput): Promise { - const tauriApi = getTauriApi(); - if (!tauriApi) { +async function showDesktopConfirmDialog(input: ConfirmDialogInput): Promise { + const desktopApi = getDesktopApi(); + if (!desktopApi) { return null; } blurActiveWebElement(); - const options = buildTauriAskOptions(input); - const tauriAsk = tauriApi.dialog?.ask; + const options = buildDesktopAskOptions(input); + const desktopAsk = desktopApi.dialog?.ask; - if (typeof tauriAsk === "function") { - try { - return Boolean(await tauriAsk(input.message, options)); - } catch (error) { - console.warn("[ConfirmDialog] Tauri dialog.ask failed", error); - } - } - - const tauriInvoke = tauriApi.core?.invoke; - if (typeof tauriInvoke === "function") { - try { - const result = await tauriInvoke("plugin:dialog|ask", { - message: input.message, - ...options, - }); - return result === true; - } catch (error) { - console.warn("[ConfirmDialog] Tauri plugin:dialog|ask failed", error); - } + if (typeof desktopAsk === "function") { + return Boolean(await desktopAsk(input.message, options)); } return null; @@ -124,9 +107,9 @@ export async function confirmDialog(input: ConfirmDialogInput): Promise return showNativeConfirmDialog(input); } - const tauriResult = await showTauriConfirmDialog(input); - if (tauriResult !== null) { - return tauriResult; + const desktopResult = await showDesktopConfirmDialog(input); + if (desktopResult !== null) { + return desktopResult; } return showWebConfirmDialog(input); @@ -134,5 +117,5 @@ export async function confirmDialog(input: ConfirmDialogInput): Promise export const __private__ = { blurActiveWebElement, - buildTauriAskOptions, + buildDesktopAskOptions, }; diff --git a/packages/app/src/utils/desktop-window.ts b/packages/app/src/utils/desktop-window.ts new file mode 100644 index 000000000..94c53f233 --- /dev/null +++ b/packages/app/src/utils/desktop-window.ts @@ -0,0 +1,117 @@ +import type { CSSProperties } from "react"; +import { useState, useEffect } from "react"; +import { Platform, type ViewStyle } from "react-native"; +import { + getIsDesktopMac, + DESKTOP_TRAFFIC_LIGHT_WIDTH, + DESKTOP_TRAFFIC_LIGHT_HEIGHT, +} from "@/constants/layout"; +import { getDesktopWindow } from "@/desktop/electron/window"; +import { isDesktop } from "@/desktop/host"; + +type DesktopDragHandlers = { + style?: CSSProperties & ViewStyle; +}; + +const DESKTOP_DRAG_REGION_STYLE = { + WebkitAppRegion: "drag", +} as CSSProperties & ViewStyle; + +export async function startDragging() { + const win = getDesktopWindow(); + if (win && typeof win.startDragging === "function") { + try { + await win.startDragging(); + } catch (error) { + console.warn("[DesktopWindow] startDragging failed", error); + } + } +} + +export async function toggleMaximize() { + const win = getDesktopWindow(); + if (win && typeof win.toggleMaximize === "function") { + try { + await win.toggleMaximize(); + } catch (error) { + console.warn("[DesktopWindow] toggleMaximize failed", error); + } + } +} + +export function useDesktopDragHandlers(): DesktopDragHandlers { + if (Platform.OS !== "web" || !isDesktop()) { + return {}; + } + + return { + style: DESKTOP_DRAG_REGION_STYLE, + }; +} + +export function useTrafficLightPadding(): { left: number; top: number } { + const [isFullscreen, setIsFullscreen] = useState(false); + + useEffect(() => { + if (Platform.OS !== "web" || !getIsDesktopMac()) return; + + let disposed = false; + let cleanup: (() => void) | undefined; + let didCleanup = false; + + function runCleanup() { + if (!cleanup || didCleanup) return; + didCleanup = true; + try { + void Promise.resolve(cleanup()).catch((error) => { + console.warn("[DesktopWindow] Failed to remove resize listener", error); + }); + } catch (error) { + console.warn("[DesktopWindow] Failed to remove resize listener", error); + } + } + + async function setup() { + const win = getDesktopWindow(); + if (!win) return; + + const fullscreen = + typeof win.isFullscreen === "function" ? await win.isFullscreen() : false; + if (disposed) return; + setIsFullscreen(fullscreen); + + if (typeof win.onResized !== "function") { + return; + } + + const unlisten = await win.onResized(async () => { + if (disposed) return; + const fs = + typeof win.isFullscreen === "function" ? await win.isFullscreen() : false; + if (disposed) return; + setIsFullscreen(fs); + }); + + cleanup = unlisten; + if (disposed) { + runCleanup(); + } + } + + void setup(); + + return () => { + disposed = true; + runCleanup(); + }; + }, []); + + if (!getIsDesktopMac() || isFullscreen) { + return { left: 0, top: 0 }; + } + + return { + left: DESKTOP_TRAFFIC_LIGHT_WIDTH, + top: DESKTOP_TRAFFIC_LIGHT_HEIGHT, + }; +} diff --git a/packages/app/src/utils/open-external-url.ts b/packages/app/src/utils/open-external-url.ts index f27307a70..2de4c5f5a 100644 --- a/packages/app/src/utils/open-external-url.ts +++ b/packages/app/src/utils/open-external-url.ts @@ -1,10 +1,10 @@ import * as Linking from "expo-linking"; import { Platform } from "react-native"; -import { getTauri } from "@/utils/tauri"; +import { getDesktopHost } from "@/desktop/host"; export async function openExternalUrl(url: string): Promise { if (Platform.OS === "web") { - const opener = getTauri()?.opener?.openUrl; + const opener = getDesktopHost()?.opener?.openUrl; if (typeof opener === "function") { await opener(url); return; diff --git a/packages/app/src/utils/os-notifications.test.ts b/packages/app/src/utils/os-notifications.test.ts index 43edf530f..d63ee230d 100644 --- a/packages/app/src/utils/os-notifications.test.ts +++ b/packages/app/src/utils/os-notifications.test.ts @@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; type MockNotificationOptions = { body?: string; data?: Record; + icon?: string; }; type MockNotificationInstance = { @@ -18,7 +19,6 @@ type GlobalSnapshot = { dispatchEvent: unknown; focus: unknown; location: unknown; - __TAURI__: unknown; }; const originalGlobals: GlobalSnapshot = { @@ -27,12 +27,34 @@ const originalGlobals: GlobalSnapshot = { dispatchEvent: (globalThis as { dispatchEvent?: unknown }).dispatchEvent, focus: (globalThis as { focus?: unknown }).focus, location: (globalThis as { location?: unknown }).location, - __TAURI__: (globalThis as { __TAURI__?: unknown }).__TAURI__, }; -async function loadModuleForPlatform(platform: "web" | "ios" | "android") { +async function loadModuleForPlatform( + platform: "web" | "ios" | "android", + options?: { + desktopHost?: { + notification?: { + sendNotification?: (payload: { + title: string; + body?: string; + data?: Record; + }) => Promise; + }; + } | null; + } +) { vi.resetModules(); vi.doMock("react-native", () => ({ Platform: { OS: platform } })); + vi.doMock("@/desktop/host", () => ({ + getDesktopHost: () => options?.desktopHost ?? null, + })); + vi.doMock("expo-asset", () => ({ + Asset: { + fromModule: vi.fn(() => ({ + uri: "http://localhost:8081/packages/app/assets/images/notification-icon.png", + })), + }, + })); return import("./os-notifications"); } @@ -42,7 +64,6 @@ function restoreGlobals(): void { (globalThis as { dispatchEvent?: unknown }).dispatchEvent = originalGlobals.dispatchEvent; (globalThis as { focus?: unknown }).focus = originalGlobals.focus; (globalThis as { location?: unknown }).location = originalGlobals.location; - (globalThis as { __TAURI__?: unknown }).__TAURI__ = originalGlobals.__TAURI__; } describe("sendOsNotification", () => { @@ -71,7 +92,6 @@ describe("sendOsNotification", () => { }); afterEach(() => { - vi.doUnmock("@/desktop/notifications/desktop-notifications"); vi.doUnmock("react-native"); vi.restoreAllMocks(); vi.resetModules(); @@ -168,14 +188,8 @@ describe("sendOsNotification", () => { expect(assign).toHaveBeenCalledWith("/h/srv%20with%20space/agent/agent%2F1"); }); - it("uses the desktop notification bridge when Tauri is available", async () => { - const sendDesktopNotification = vi.fn(async () => true); - vi.doMock("@/desktop/notifications/desktop-notifications", () => ({ - sendDesktopNotification, - })); - - (globalThis as { __TAURI__?: unknown }).__TAURI__ = {}; - + it("returns false when the Notification API is unavailable", async () => { + (globalThis as { Notification?: unknown }).Notification = undefined; const { sendOsNotification } = await loadModuleForPlatform("web"); const sent = await sendOsNotification({ title: "Agent finished", @@ -183,11 +197,61 @@ describe("sendOsNotification", () => { data: { serverId: "srv-1", agentId: "agent-1" }, }); + expect(sent).toBe(false); + }); + + it("does not attach a click handler when there is no route target", async () => { + const created: MockNotificationInstance[] = []; + + class MockNotification implements MockNotificationInstance { + static permission = "granted"; + static requestPermission = vi.fn(async () => "granted"); + onclick: ((event: Event) => void) | null = null; + close = vi.fn(); + + constructor(public title: string, public options?: MockNotificationOptions) { + created.push(this); + } + } + + (globalThis as { Notification?: unknown }).Notification = MockNotification; + (globalThis as { dispatchEvent?: unknown }).dispatchEvent = vi.fn(); + (globalThis as { location?: unknown }).location = { assign: vi.fn() }; + + const { sendOsNotification } = await loadModuleForPlatform("web"); + + const sent = await sendOsNotification({ + title: "Paseo notification test", + body: "If you can see this, desktop notifications work.", + }); + expect(sent).toBe(true); - expect(sendDesktopNotification).toHaveBeenCalledWith({ - title: "Agent finished", - body: "Done", - data: { serverId: "srv-1", agentId: "agent-1" }, + expect(created).toHaveLength(1); + expect(created[0]?.onclick).toBeNull(); + }); + + it("uses the desktop notification bridge when available", async () => { + const sendNotification = vi.fn(async () => true); + + const { sendOsNotification } = await loadModuleForPlatform("web", { + desktopHost: { + notification: { + sendNotification, + }, + }, + }); + + const sent = await sendOsNotification({ + title: "Paseo notification test", + body: "If you can see this, desktop notifications work.", + data: { serverId: "srv-1" }, + }); + + expect(sent).toBe(true); + expect(sendNotification).toHaveBeenCalledWith({ + title: "Paseo notification test", + body: "If you can see this, desktop notifications work.", + data: { serverId: "srv-1" }, }); }); }); diff --git a/packages/app/src/utils/os-notifications.ts b/packages/app/src/utils/os-notifications.ts index 6697bc83e..98f8b14e0 100644 --- a/packages/app/src/utils/os-notifications.ts +++ b/packages/app/src/utils/os-notifications.ts @@ -1,7 +1,10 @@ +import { Asset } from "expo-asset"; import { Platform } from "react-native"; -import { sendDesktopNotification } from "@/desktop/notifications/desktop-notifications"; -import { buildNotificationRoute } from "./notification-routing"; -import { getTauri, type TauriNotificationApi } from "@/utils/tauri"; +import { getDesktopHost } from "@/desktop/host"; +import { + buildNotificationRoute, + resolveNotificationTarget, +} from "./notification-routing"; type OsNotificationPayload = { title: string; @@ -15,28 +18,41 @@ export type WebNotificationClickDetail = { type WebNotificationInstance = { onclick?: ((event: Event) => void) | null; - onclose?: ((event: Event) => void) | null; - addEventListener?: (type: string, listener: (event: Event) => void) => void; - removeEventListener?: (type: string, listener: (event: Event) => void) => void; - close?: () => void; }; export const WEB_NOTIFICATION_CLICK_EVENT = "paseo:web-notification-click"; let permissionRequest: Promise | null = null; -const activeWebNotifications = new Set(); +let notificationIconUrl: string | null | undefined; -function getTauriNotificationModule(): TauriNotificationApi | null { - if (Platform.OS !== "web") { - return null; - } - return getTauri()?.notification ?? null; +function getDesktopNotificationSender(): + | ((payload: { + title: string; + body?: string; + data?: Record; + }) => Promise) + | null { + const sendNotification = getDesktopHost()?.notification?.sendNotification; + return typeof sendNotification === "function" + ? (sendNotification as (payload: { + title: string; + body?: string; + data?: Record; + }) => Promise) + : null; } function getWebNotificationConstructor(): { permission: string; requestPermission?: () => Promise; - new (title: string, options?: { body?: string; data?: Record }): unknown; + new ( + title: string, + options?: { + body?: string; + data?: Record; + icon?: string; + } + ): unknown; } | null { const NotificationConstructor = (globalThis as { Notification?: any }).Notification; return NotificationConstructor ?? null; @@ -70,49 +86,31 @@ export async function ensureOsNotificationPermission(): Promise { if (Platform.OS !== "web") { return false; } - - const tauriNotification = getTauriNotificationModule(); - if (tauriNotification) { - return await ensureTauriNotificationPermission(tauriNotification); - } - return await ensureNotificationPermission(); } -async function ensureTauriNotificationPermission( - notificationModule: TauriNotificationApi -): Promise { - if (typeof notificationModule.isPermissionGranted === "function") { - try { - const granted = await notificationModule.isPermissionGranted(); - if (granted) { - return true; - } - } catch (error) { - console.warn( - "[OSNotifications][Tauri] Failed to check notification permission", - error - ); - } - } +function hasNotificationClickTarget( + data: Record | undefined +): boolean { + const target = resolveNotificationTarget(data); + return target.serverId !== null || target.agentId !== null || target.workspaceId !== null; +} - if (typeof notificationModule.requestPermission !== "function") { - console.warn( - "[OSNotifications][Tauri] notification.requestPermission is unavailable" - ); - return false; +function getWebNotificationIconUrl(): string | undefined { + if (notificationIconUrl !== undefined) { + return notificationIconUrl ?? undefined; } try { - const result = await notificationModule.requestPermission(); - return result === "granted"; - } catch (error) { - console.warn( - "[OSNotifications][Tauri] Failed to request notification permission", - error + const asset = Asset.fromModule( + require("../../assets/images/notification-icon.png") ); - return false; + notificationIconUrl = asset.uri ?? null; + } catch { + notificationIconUrl = null; } + + return notificationIconUrl ?? undefined; } function dispatchWebNotificationClick(detail: WebNotificationClickDetail): boolean { @@ -156,41 +154,12 @@ function attachWebClickHandler( notification: WebNotificationInstance, data: Record | undefined ): void { - activeWebNotifications.add(notification); - - const cleanup = () => { - activeWebNotifications.delete(notification); - }; - - const onClick = () => { - const focus = (globalThis as { focus?: () => void }).focus; - if (typeof focus === "function") { - focus(); - } - + notification.onclick = () => { const handledByApp = dispatchWebNotificationClick({ data }); if (!handledByApp) { fallbackNavigateToNotificationTarget(data); } - - cleanup(); - if (typeof notification.close === "function") { - notification.close(); - } }; - - const onClose = () => { - cleanup(); - }; - - if (typeof notification.addEventListener === "function") { - notification.addEventListener("click", onClick); - notification.addEventListener("close", onClose); - return; - } - - notification.onclick = onClick; - notification.onclose = onClose; } export async function sendOsNotification( @@ -201,8 +170,9 @@ export async function sendOsNotification( return false; } - if (getTauri()) { - return await sendDesktopNotification(payload); + const desktopNotificationSender = getDesktopNotificationSender(); + if (desktopNotificationSender) { + return await desktopNotificationSender(payload); } const NotificationConstructor = getWebNotificationConstructor(); @@ -212,8 +182,11 @@ export async function sendOsNotification( const notification = new NotificationConstructor(payload.title, { body: payload.body, data: payload.data, + icon: getWebNotificationIconUrl(), }) as WebNotificationInstance; - attachWebClickHandler(notification, payload.data); + if (hasNotificationClickTarget(payload.data)) { + attachWebClickHandler(notification, payload.data); + } return true; } } diff --git a/packages/app/src/utils/shortcut-platform.ts b/packages/app/src/utils/shortcut-platform.ts index a54e2550f..3185f27f2 100644 --- a/packages/app/src/utils/shortcut-platform.ts +++ b/packages/app/src/utils/shortcut-platform.ts @@ -1,12 +1,12 @@ import { Platform } from "react-native"; -import { getIsTauriMac } from "@/constants/layout"; +import { getIsDesktopMac } from "@/constants/layout"; import type { ShortcutOs } from "@/utils/format-shortcut"; export function getShortcutOs(): ShortcutOs { if (Platform.OS !== "web") { return Platform.OS === "ios" ? "mac" : "non-mac"; } - if (getIsTauriMac()) return "mac"; + if (getIsDesktopMac()) return "mac"; if (typeof navigator === "undefined") return "non-mac"; const ua = navigator.userAgent ?? ""; const platform = (navigator as any).platform ?? ""; @@ -15,4 +15,3 @@ export function getShortcutOs(): ShortcutOs { /Mac|iPhone|iPad|iPod/i.test(platform); return isApple ? "mac" : "non-mac"; } - diff --git a/packages/app/src/utils/tauri-attach-console.ts b/packages/app/src/utils/tauri-attach-console.ts deleted file mode 100644 index c6faffee8..000000000 --- a/packages/app/src/utils/tauri-attach-console.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { getTauri } from "@/utils/tauri"; - -let attached = false; - -export function attachConsole(): void { - if (attached || !getTauri()) { - return; - } - attached = true; - - const originalLog = console.log; - const originalInfo = console.info; - const originalWarn = console.warn; - const originalError = console.error; - const originalDebug = console.debug; - - function formatArgs(args: unknown[]): string { - return args - .map((a) => - typeof a === "string" ? a : JSON.stringify(a, null, 0) ?? String(a) - ) - .join(" "); - } - - function forwardToRust(level: number, args: unknown[]): void { - const invoke = getTauri()?.core?.invoke; - if (typeof invoke !== "function") return; - try { - void invoke("webview_log", { level, message: formatArgs(args) }); - } catch { - // silently ignore IPC errors - } - } - - console.log = (...args: unknown[]) => { - originalLog.apply(console, args); - forwardToRust(1, args); // info - }; - console.info = (...args: unknown[]) => { - originalInfo.apply(console, args); - forwardToRust(1, args); // info - }; - console.warn = (...args: unknown[]) => { - originalWarn.apply(console, args); - forwardToRust(2, args); // warn - }; - console.error = (...args: unknown[]) => { - originalError.apply(console, args); - forwardToRust(3, args); // error - }; - console.debug = (...args: unknown[]) => { - originalDebug.apply(console, args); - forwardToRust(0, args); // debug - }; -} diff --git a/packages/app/src/utils/tauri-window.ts b/packages/app/src/utils/tauri-window.ts deleted file mode 100644 index bfa64c97f..000000000 --- a/packages/app/src/utils/tauri-window.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { Platform } from "react-native"; -import { useState, useEffect } from "react"; -import { getIsTauriMac, TAURI_TRAFFIC_LIGHT_WIDTH, TAURI_TRAFFIC_LIGHT_HEIGHT } from "@/constants/layout"; -import { getCurrentTauriWindow, getTauri, isTauriEnvironment } from "@/utils/tauri"; - -let tauriWindow: any = null; -const NON_DRAGGABLE_SELECTOR = [ - "button", - "a", - "input", - "textarea", - "select", - "[role='button']", - "[role='link']", - "[role='textbox']", - "[role='combobox']", - "[contenteditable='true']", -].join(", "); - -async function getTauriWindow() { - if (tauriWindow) return tauriWindow; - - // Double-check: both environment check AND platform check - if (!isTauriEnvironment()) { - return null; - } - - try { - // When `app.withGlobalTauri` is enabled, Tauri exposes its JS APIs via getTauri(). - // We must use that here because importing `@tauri-apps/api/*` would be bundled into - // the native (Hermes) builds and break parsing/runtime on mobile. - const tauri = getTauri(); - if (!tauri) { - return null; - } - - // Prefer the public global window module. - const publicWindow = getCurrentTauriWindow(); - if (publicWindow) { - tauriWindow = publicWindow; - return tauriWindow; - } - - // Fallback: core invoke (should exist when withGlobalTauri is enabled). - // We assume the main window label is "main" (default in Tauri when not specified). - const invoke = tauri?.core?.invoke; - if (typeof invoke === "function") { - tauriWindow = { - label: "main", - startDragging: () => invoke("plugin:window|start_dragging", { label: "main" }), - toggleMaximize: () => invoke("plugin:window|toggle_maximize", { label: "main" }), - isFullscreen: () => invoke("plugin:window|is_fullscreen", { label: "main" }), - onResized: (handler: (e: unknown) => void) => tauri?.event?.listen?.("tauri://resize", handler), - }; - return tauriWindow; - } - - return null; - } catch { - return null; - } -} - -export async function startDragging() { - const win = await getTauriWindow(); - if (win) { - try { - await win.startDragging(); - } catch (error) { - console.warn("[TauriWindow] startDragging failed", error); - } - } -} - -export async function toggleMaximize() { - const win = await getTauriWindow(); - if (win) { - try { - await win.toggleMaximize(); - } catch (error) { - console.warn("[TauriWindow] toggleMaximize failed", error); - } - } -} - -// Returns event handlers for drag region behavior -export function useTauriDragHandlers() { - // Dragging should work on any desktop OS when running under Tauri. - if (Platform.OS !== "web" || !isTauriEnvironment()) { - return {}; - } - - return { - onMouseDown: (e: React.MouseEvent) => { - // Only handle primary button, ignore if clicking on interactive elements. - // Tauri docs recommend using `e.detail` on mousedown for double-click maximize. - if (e.defaultPrevented) return; - if (e.button !== 0) return; - const target = e.target instanceof Element ? e.target : null; - if (target?.closest(NON_DRAGGABLE_SELECTOR)) return; - - // Prevent text selection when dragging - e.preventDefault(); - - // Double click to maximize, otherwise drag. - if (e.detail === 2) { - toggleMaximize(); - } else { - startDragging(); - } - }, - }; -} - -// Hook that returns traffic light padding, accounting for fullscreen state -// In fullscreen, traffic lights are hidden so no padding is needed -export function useTrafficLightPadding(): { left: number; top: number } { - const [isFullscreen, setIsFullscreen] = useState(false); - - useEffect(() => { - if (Platform.OS !== "web" || !getIsTauriMac()) return; - - let disposed = false; - let cleanup: (() => void) | undefined; - let didCleanup = false; - - function runCleanup() { - if (!cleanup || didCleanup) return; - didCleanup = true; - try { - void Promise.resolve(cleanup()).catch((error) => { - console.warn("[TauriWindow] Failed to remove resize listener", error); - }); - } catch (error) { - console.warn("[TauriWindow] Failed to remove resize listener", error); - } - } - - async function setup() { - const win = await getTauriWindow(); - if (!win) return; - - // Check initial fullscreen state - const fullscreen = await win.isFullscreen(); - if (disposed) return; - setIsFullscreen(fullscreen); - - // Listen for resize events which include fullscreen changes - const unlisten = await win.onResized(async () => { - if (disposed) return; - const fs = await win.isFullscreen(); - if (disposed) return; - setIsFullscreen(fs); - }); - - cleanup = unlisten; - if (disposed) { - runCleanup(); - } - } - - void setup(); - - return () => { - disposed = true; - runCleanup(); - }; - }, []); - - if (!getIsTauriMac() || isFullscreen) { - return { left: 0, top: 0 }; - } - - return { - left: TAURI_TRAFFIC_LIGHT_WIDTH, - top: TAURI_TRAFFIC_LIGHT_HEIGHT, - }; -} diff --git a/packages/app/src/utils/tauri.ts b/packages/app/src/utils/tauri.ts deleted file mode 100644 index 438541ad1..000000000 --- a/packages/app/src/utils/tauri.ts +++ /dev/null @@ -1,106 +0,0 @@ -export type TauriNotificationPermission = "granted" | "denied" | "default"; - -export interface TauriDialogAskOptions { - title?: string; - okLabel?: string; - cancelLabel?: string; - kind?: "info" | "warning" | "error"; -} - -export interface TauriDialogOpenOptions { - title?: string; - defaultPath?: string; - directory?: boolean; - multiple?: boolean; - filters?: Array<{ - name: string; - extensions: string[]; - }>; -} - -export interface TauriDialogApi { - ask?: (message: string, options?: TauriDialogAskOptions) => Promise; - open?: ( - options?: TauriDialogOpenOptions - ) => Promise; -} - -export interface TauriCoreApi { - invoke?: (command: string, args?: Record) => Promise; - convertFileSrc?: (path: string) => string; -} - -export interface TauriEventApi { - listen?: ( - event: string, - handler: (event: unknown) => void - ) => Promise<() => void> | (() => void); -} - -export interface TauriWindowApi { - label?: string; - startDragging?: () => Promise; - toggleMaximize?: () => Promise; - isFullscreen?: () => Promise; - onResized?: ( - handler: (event: TEvent) => void - ) => Promise<() => void> | (() => void); - setBadgeCount?: (count?: number) => Promise; - onDragDropEvent?: ( - handler: (event: TEvent) => void - ) => Promise<() => void> | (() => void); -} - -export interface TauriWindowModule { - getCurrentWindow?: () => TauriWindowApi; -} - -export interface TauriOpenerApi { - openUrl?: (url: string) => Promise; -} - -export interface TauriNotificationApi { - isPermissionGranted?: () => Promise; - requestPermission?: () => Promise; - sendNotification?: ( - payload: string | { title: string; body?: string } - ) => Promise; -} - -export interface TauriWebSocketApi { - connect?: (url: string, config?: unknown) => Promise; -} - -export interface TauriApi { - core?: TauriCoreApi; - dialog?: TauriDialogApi; - event?: TauriEventApi; - notification?: TauriNotificationApi; - opener?: TauriOpenerApi; - websocket?: TauriWebSocketApi; - window?: TauriWindowModule; -} - -export function getTauri(): TauriApi | null { - const tauri = (globalThis as { __TAURI__?: unknown }).__TAURI__; - if (!tauri || typeof tauri !== "object") { - return null; - } - return tauri as TauriApi; -} - -export function isTauriEnvironment(): boolean { - return getTauri() !== null; -} - -export function getCurrentTauriWindow(): TauriWindowApi | null { - const getter = getTauri()?.window?.getCurrentWindow; - if (typeof getter !== "function") { - return null; - } - try { - return getter() ?? null; - } catch { - return null; - } -} diff --git a/packages/app/src/utils/test-daemon-connection.ts b/packages/app/src/utils/test-daemon-connection.ts index 3d2ae5a79..3e271f648 100644 --- a/packages/app/src/utils/test-daemon-connection.ts +++ b/packages/app/src/utils/test-daemon-connection.ts @@ -5,8 +5,8 @@ import { getOrCreateClientId } from "./client-id"; import { buildDaemonWebSocketUrl, buildRelayWebSocketUrl } from "./daemon-endpoints"; import { buildLocalDaemonTransportUrl, - createTauriLocalDaemonTransportFactory, -} from "./managed-tauri-daemon-transport"; + createDesktopLocalDaemonTransportFactory, +} from "@/desktop/daemon/desktop-daemon-transport"; function normalizeNonEmptyString(value: unknown): string | null { if (typeof value !== "string") return null; @@ -49,7 +49,7 @@ export async function buildClientConfig( serverId?: string ): Promise { const clientId = await getOrCreateClientId(); - const localTransportFactory = createTauriLocalDaemonTransportFactory(); + const localTransportFactory = createDesktopLocalDaemonTransportFactory(); const base = { clientId, clientType: "mobile" as const, diff --git a/packages/app/src/voice/audio-engine.web.ts b/packages/app/src/voice/audio-engine.web.ts index ddb9916b8..cbffbeb65 100644 --- a/packages/app/src/voice/audio-engine.web.ts +++ b/packages/app/src/voice/audio-engine.web.ts @@ -1,4 +1,4 @@ -import { getTauri } from "@/utils/tauri"; +import { isDesktop } from "@/desktop/host"; import type { AudioEngine, AudioEngineCallbacks, @@ -300,12 +300,12 @@ export function createAudioEngine( typeof window !== "undefined" && window.location ? window.location.origin : "unknown"; - const isTauri = getTauri() !== null; + const isDesktopApp = isDesktop(); if (missingNavigator) { throw new Error("Microphone capture is not supported in this environment"); } - if (!secureContext && !isTauri) { + if (!secureContext && !isDesktopApp) { throw new Error( `Microphone access requires HTTPS or localhost. Current origin: ${currentOrigin}` ); diff --git a/packages/desktop/.gitignore b/packages/desktop/.gitignore new file mode 100644 index 000000000..ce59b2b70 --- /dev/null +++ b/packages/desktop/.gitignore @@ -0,0 +1,9 @@ +# Build output +dist/ +release/ + +# Dependencies +node_modules/ + +# TypeScript +*.tsbuildinfo diff --git a/packages/desktop/README.md b/packages/desktop/README.md new file mode 100644 index 000000000..b7ae3e132 --- /dev/null +++ b/packages/desktop/README.md @@ -0,0 +1,3 @@ +# desktop + +Electron desktop app for Paseo. diff --git a/packages/desktop/src-tauri/icons/128x128.png b/packages/desktop/assets/128x128.png similarity index 100% rename from packages/desktop/src-tauri/icons/128x128.png rename to packages/desktop/assets/128x128.png diff --git a/packages/desktop/src-tauri/icons/128x128@2x.png b/packages/desktop/assets/128x128@2x.png similarity index 100% rename from packages/desktop/src-tauri/icons/128x128@2x.png rename to packages/desktop/assets/128x128@2x.png diff --git a/packages/desktop/src-tauri/icons/32x32.png b/packages/desktop/assets/32x32.png similarity index 100% rename from packages/desktop/src-tauri/icons/32x32.png rename to packages/desktop/assets/32x32.png diff --git a/packages/desktop/src-tauri/icons/64x64.png b/packages/desktop/assets/64x64.png similarity index 100% rename from packages/desktop/src-tauri/icons/64x64.png rename to packages/desktop/assets/64x64.png diff --git a/packages/desktop/src-tauri/icons/icon.icns b/packages/desktop/assets/icon.icns similarity index 100% rename from packages/desktop/src-tauri/icons/icon.icns rename to packages/desktop/assets/icon.icns diff --git a/packages/desktop/src-tauri/icons/icon.ico b/packages/desktop/assets/icon.ico similarity index 100% rename from packages/desktop/src-tauri/icons/icon.ico rename to packages/desktop/assets/icon.ico diff --git a/packages/desktop/src-tauri/icons/icon.png b/packages/desktop/assets/icon.png similarity index 100% rename from packages/desktop/src-tauri/icons/icon.png rename to packages/desktop/assets/icon.png diff --git a/packages/desktop/electron-builder.yml b/packages/desktop/electron-builder.yml new file mode 100644 index 000000000..2630eadba --- /dev/null +++ b/packages/desktop/electron-builder.yml @@ -0,0 +1,44 @@ +appId: sh.paseo.desktop +productName: Paseo +executableName: Paseo +directories: + output: release +files: + - dist/**/* +extraResources: + - from: ../app/dist + to: app-dist +publish: + provider: github + owner: anthropics + repo: paseo +mac: + category: public.app-category.developer-tools + icon: assets/icon.icns + extraFiles: + - from: scripts/paseo + to: MacOS/paseo + target: + - dmg + - zip +linux: + category: Development + icon: assets + extraFiles: + - from: scripts/paseo + to: paseo + target: + - AppImage + - tar.gz +win: + icon: assets/icon.ico + extraFiles: + - from: scripts/paseo.cmd + to: paseo.cmd + target: + - nsis + - zip +nsis: + oneClick: false + perMachine: false + allowToChangeInstallationDirectory: true diff --git a/packages/desktop/package-lock.json b/packages/desktop/package-lock.json deleted file mode 100644 index 19a7223b0..000000000 --- a/packages/desktop/package-lock.json +++ /dev/null @@ -1,233 +0,0 @@ -{ - "name": "desktop", - "version": "0.1.3", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "desktop", - "version": "0.1.3", - "license": "ISC", - "devDependencies": { - "@tauri-apps/cli": "^2.9.6" - } - }, - "node_modules/@tauri-apps/cli": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.9.6.tgz", - "integrity": "sha512-3xDdXL5omQ3sPfBfdC8fCtDKcnyV7OqyzQgfyT5P3+zY6lcPqIYKQBvUasNvppi21RSdfhy44ttvJmftb0PCDw==", - "dev": true, - "license": "Apache-2.0 OR MIT", - "bin": { - "tauri": "tauri.js" - }, - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/tauri" - }, - "optionalDependencies": { - "@tauri-apps/cli-darwin-arm64": "2.9.6", - "@tauri-apps/cli-darwin-x64": "2.9.6", - "@tauri-apps/cli-linux-arm-gnueabihf": "2.9.6", - "@tauri-apps/cli-linux-arm64-gnu": "2.9.6", - "@tauri-apps/cli-linux-arm64-musl": "2.9.6", - "@tauri-apps/cli-linux-riscv64-gnu": "2.9.6", - "@tauri-apps/cli-linux-x64-gnu": "2.9.6", - "@tauri-apps/cli-linux-x64-musl": "2.9.6", - "@tauri-apps/cli-win32-arm64-msvc": "2.9.6", - "@tauri-apps/cli-win32-ia32-msvc": "2.9.6", - "@tauri-apps/cli-win32-x64-msvc": "2.9.6" - } - }, - "node_modules/@tauri-apps/cli-darwin-arm64": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.9.6.tgz", - "integrity": "sha512-gf5no6N9FCk1qMrti4lfwP77JHP5haASZgVbBgpZG7BUepB3fhiLCXGUK8LvuOjP36HivXewjg72LTnPDScnQQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-darwin-x64": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.9.6.tgz", - "integrity": "sha512-oWh74WmqbERwwrwcueJyY6HYhgCksUc6NT7WKeXyrlY/FPmNgdyQAgcLuTSkhRFuQ6zh4Np1HZpOqCTpeZBDcw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.9.6.tgz", - "integrity": "sha512-/zde3bFroFsNXOHN204DC2qUxAcAanUjVXXSdEGmhwMUZeAQalNj5cz2Qli2elsRjKN/hVbZOJj0gQ5zaYUjSg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm64-gnu": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.9.6.tgz", - "integrity": "sha512-pvbljdhp9VOo4RnID5ywSxgBs7qiylTPlK56cTk7InR3kYSTJKYMqv/4Q/4rGo/mG8cVppesKIeBMH42fw6wjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm64-musl": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.9.6.tgz", - "integrity": "sha512-02TKUndpodXBCR0oP//6dZWGYcc22Upf2eP27NvC6z0DIqvkBBFziQUcvi2n6SrwTRL0yGgQjkm9K5NIn8s6jw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.9.6.tgz", - "integrity": "sha512-fmp1hnulbqzl1GkXl4aTX9fV+ubHw2LqlLH1PE3BxZ11EQk+l/TmiEongjnxF0ie4kV8DQfDNJ1KGiIdWe1GvQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-x64-gnu": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.9.6.tgz", - "integrity": "sha512-vY0le8ad2KaV1PJr+jCd8fUF9VOjwwQP/uBuTJvhvKTloEwxYA/kAjKK9OpIslGA9m/zcnSo74czI6bBrm2sYA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-x64-musl": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.9.6.tgz", - "integrity": "sha512-TOEuB8YCFZTWVDzsO2yW0+zGcoMiPPwcUgdnW1ODnmgfwccpnihDRoks+ABT1e3fHb1ol8QQWsHSCovb3o2ENQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-arm64-msvc": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.9.6.tgz", - "integrity": "sha512-ujmDGMRc4qRLAnj8nNG26Rlz9klJ0I0jmZs2BPpmNNf0gM/rcVHhqbEkAaHPTBVIrtUdf7bGvQAD2pyIiUrBHQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-ia32-msvc": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.9.6.tgz", - "integrity": "sha512-S4pT0yAJgFX8QRCyKA1iKjZ9Q/oPjCZf66A/VlG5Yw54Nnr88J1uBpmenINbXxzyhduWrIXBaUbEY1K80ZbpMg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-x64-msvc": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.9.6.tgz", - "integrity": "sha512-ldWuWSSkWbKOPjQMJoYVj9wLHcOniv7diyI5UAJ4XsBdtaFB0pKHQsqw/ItUma0VXGC7vB4E9fZjivmxur60aw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - } - } -} diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 8a2d8cf27..b000a066c 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -2,17 +2,26 @@ "name": "@getpaseo/desktop", "version": "0.1.30", "private": true, - "description": "Paseo desktop app (Tauri wrapper)", + "description": "Paseo desktop app (Electron wrapper)", + "main": "dist/main.js", "scripts": { - "build:managed-runtime": "node ./scripts/build-managed-runtime.mjs", - "prepare:managed-runtime": "npm --prefix ../.. run build:daemon && npm run build:managed-runtime && npm run validate:managed-runtime", - "dev": "tauri dev", - "build": "npm --prefix ../.. run build:web --workspace=@getpaseo/app && npm run prepare:managed-runtime && tauri build", - "validate:managed-runtime": "node ./scripts/validate-managed-runtime.mjs", - "smoke:managed-daemon": "node ./scripts/managed-daemon-smoke.mjs", - "tauri": "tauri" + "build": "npm --prefix ../.. run build:daemon && npm run build:main && CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --config electron-builder.yml", + "build:main": "tsc -p tsconfig.json", + "dev": "npm run build:main && wait-on tcp:8081 && electron .", + "typecheck": "tsc --noEmit -p tsconfig.json" + }, + "dependencies": { + "@getpaseo/cli": "0.1.30", + "@getpaseo/server": "0.1.30", + "electron-updater": "^6.6.2", + "ws": "^8.14.2" }, "devDependencies": { - "@tauri-apps/cli": "^2.9.6" + "@types/node": "24.6.0", + "@types/ws": "^8.5.14", + "electron": "41.0.3", + "electron-builder": "26.8.1", + "typescript": "5.9.3", + "wait-on": "8.0.5" } } diff --git a/packages/desktop/scripts/build-managed-runtime.mjs b/packages/desktop/scripts/build-managed-runtime.mjs deleted file mode 100644 index 01e3fa9af..000000000 --- a/packages/desktop/scripts/build-managed-runtime.mjs +++ /dev/null @@ -1,590 +0,0 @@ -import { createHash } from "node:crypto"; -import { spawnSync } from "node:child_process"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import process from "node:process"; -import { fileURLToPath } from "node:url"; - -const repoRoot = path.resolve(fileURLToPath(new URL("../../..", import.meta.url))); -const desktopRoot = path.join(repoRoot, "packages", "desktop"); -const resourcesRoot = path.join(desktopRoot, "src-tauri", "resources", "managed-runtime"); -const cacheRoot = path.join(desktopRoot, ".cache", "managed-runtime"); -const packageVersion = JSON.parse( - await fs.readFile(path.join(desktopRoot, "package.json"), "utf8") -).version; - -const workspaces = [ - { name: "@getpaseo/relay", root: path.join(repoRoot, "packages", "relay") }, - { name: "@getpaseo/server", root: path.join(repoRoot, "packages", "server") }, - { name: "@getpaseo/cli", root: path.join(repoRoot, "packages", "cli") }, -]; - -const ripgrepPlatformDirMap = { - darwin: { - arm64: "arm64-darwin", - x64: "x64-darwin", - }, - linux: { - arm64: "arm64-linux", - x64: "x64-linux", - }, - win32: { - arm64: "arm64-win32", - x64: "x64-win32", - }, -}; - -async function rmSafe(target) { - await fs.rm(target, { recursive: true, force: true }); -} - -async function ensureDir(target) { - await fs.mkdir(target, { recursive: true }); -} - -async function copyDir(source, target) { - await ensureDir(path.dirname(target)); - await fs.cp(source, target, { recursive: true, dereference: true, force: true }); -} - -async function copyFile(source, target, mode) { - await ensureDir(path.dirname(target)); - await fs.copyFile(source, target); - if (mode != null) { - await fs.chmod(target, mode); - } -} - -async function pathExists(target) { - try { - await fs.access(target); - return true; - } catch { - return false; - } -} - -async function removeIfExists(target) { - if (await pathExists(target)) { - await rmSafe(target); - } -} - -async function readToolVersions() { - const raw = await fs.readFile(path.join(repoRoot, ".tool-versions"), "utf8"); - const entries = new Map(); - for (const line of raw.split(/\r?\n/)) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith("#")) { - continue; - } - const [tool, ...rest] = trimmed.split(/\s+/); - if (!tool || rest.length === 0) { - continue; - } - entries.set(tool, rest.join(" ")); - } - return entries; -} - -function resolveNodeVersion(toolVersions) { - const raw = toolVersions.get("nodejs"); - if (!raw) { - throw new Error("Missing nodejs entry in .tool-versions."); - } - const version = raw.trim().replace(/^v/, ""); - if (!/^\d+\.\d+\.\d+$/.test(version)) { - throw new Error(`Unsupported nodejs version in .tool-versions: ${raw}`); - } - return version; -} - -function resolveNodeArtifact(version) { - const platformMap = { - darwin: "darwin", - linux: "linux", - win32: "win", - }; - const archMap = { - arm64: "arm64", - x64: "x64", - }; - const platform = platformMap[process.platform]; - const arch = archMap[process.arch]; - if (!platform || !arch) { - throw new Error(`Managed runtime is not implemented for ${process.platform}-${process.arch}.`); - } - const baseName = `node-v${version}-${platform}-${arch}`; - const extension = process.platform === "win32" ? "zip" : process.platform === "linux" ? "tar.xz" : "tar.gz"; - return { - version, - baseName, - archiveName: `${baseName}.${extension}`, - extension, - downloadUrl: `https://nodejs.org/dist/v${version}/${baseName}.${extension}`, - checksumsUrl: `https://nodejs.org/dist/v${version}/SHASUMS256.txt`, - }; -} - -async function downloadFile(url, target) { - await ensureDir(path.dirname(target)); - runCommand("curl", [ - "--fail", - "--location", - "--retry", - "3", - "--silent", - "--show-error", - "--output", - target, - url, - ]); -} - -async function sha256File(target) { - const hash = createHash("sha256"); - hash.update(await fs.readFile(target)); - return hash.digest("hex"); -} - -async function computeRuntimeContentHash(input) { - const hash = createHash("sha256"); - hash.update(`packageVersion:${packageVersion}\n`); - hash.update(`nodeVersion:${input.nodeVersion}\n`); - hash.update(`platform:${process.platform}\n`); - hash.update(`arch:${process.arch}\n`); - for (const tarball of input.tarballs) { - hash.update(`workspace:${tarball.name}\n`); - hash.update(`filename:${path.basename(tarball.path)}\n`); - hash.update(`sha256:${await sha256File(tarball.path)}\n`); - } - return hash.digest("hex").slice(0, 12); -} - -async function ensureNodeArchive(nodeArtifact) { - const versionCacheRoot = path.join(cacheRoot, `node-v${nodeArtifact.version}`); - const archivePath = path.join(versionCacheRoot, nodeArtifact.archiveName); - const checksumsPath = path.join(versionCacheRoot, "SHASUMS256.txt"); - if (!(await pathExists(archivePath))) { - await downloadFile(nodeArtifact.downloadUrl, archivePath); - } - if (!(await pathExists(checksumsPath))) { - await downloadFile(nodeArtifact.checksumsUrl, checksumsPath); - } - const checksums = await fs.readFile(checksumsPath, "utf8"); - const expectedLine = checksums - .split(/\r?\n/) - .map((line) => line.trim()) - .find((line) => line.endsWith(` ${nodeArtifact.archiveName}`)); - if (!expectedLine) { - throw new Error(`Missing checksum for ${nodeArtifact.archiveName} in SHASUMS256.txt`); - } - const expectedSha = expectedLine.split(/\s+/)[0]; - const actualSha = await sha256File(archivePath); - if (actualSha !== expectedSha) { - throw new Error( - `Checksum mismatch for ${nodeArtifact.archiveName}: expected ${expectedSha}, got ${actualSha}` - ); - } - return archivePath; -} - -function runCommand(command, args, options = {}) { - const result = spawnSync(command, args, { - stdio: "pipe", - encoding: "utf8", - ...options, - }); - if (result.status !== 0 || result.error) { - throw new Error( - [ - `Command failed: ${command} ${args.join(" ")}`, - result.error ? String(result.error) : null, - result.signal ? `signal: ${result.signal}` : null, - result.status != null ? `exit code: ${result.status}` : null, - result.stdout?.trim(), - result.stderr?.trim(), - ] - .filter(Boolean) - .join("\n") - ); - } - return result; -} - -function withManagedRuntimeNodeOptions(env = process.env) { - const existing = env.NODE_OPTIONS?.trim() ?? ""; - if (existing.includes("--max-old-space-size")) { - return env; - } - const nodeOptions = existing ? `${existing} --max-old-space-size=8192` : "--max-old-space-size=8192"; - return { - ...env, - NODE_OPTIONS: nodeOptions, - }; -} - -async function extractNodeDistribution(archivePath, nodeArtifact) { - const extractionRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paseo-managed-runtime-node-")); - if (nodeArtifact.extension === "zip") { - runCommand("powershell", [ - "-NoProfile", - "-Command", - `Expand-Archive -LiteralPath '${archivePath.replace(/'/g, "''")}' -DestinationPath '${extractionRoot.replace(/'/g, "''")}' -Force`, - ]); - } else { - const tarFlag = nodeArtifact.extension === "tar.xz" ? "-xJf" : "-xzf"; - runCommand("tar", [tarFlag, archivePath, "-C", extractionRoot]); - } - return { - extractionRoot, - extractedRoot: path.join(extractionRoot, nodeArtifact.baseName), - }; -} - -async function ensureWorkspaceBuilds() { - const requiredPaths = [ - path.join(repoRoot, "packages", "relay", "dist"), - path.join(repoRoot, "packages", "server", "dist"), - path.join(repoRoot, "packages", "cli", "dist"), - ]; - for (const requiredPath of requiredPaths) { - if (!(await pathExists(requiredPath))) { - throw new Error( - `Managed runtime build is missing required path: ${requiredPath}. Run the daemon build first.` - ); - } - } -} - -async function packWorkspace(packageRoot, tarballRoot) { - await ensureDir(tarballRoot); - const npmExecPath = process.env.npm_execpath; - if (!npmExecPath) { - throw new Error("Missing npm_execpath while building managed runtime."); - } - const result = runCommand(process.execPath, [ - npmExecPath, - "pack", - "--json", - "--pack-destination", - tarballRoot, - ], { - cwd: packageRoot, - env: withManagedRuntimeNodeOptions(process.env), - }); - const [{ filename }] = JSON.parse(result.stdout.trim()); - if (!filename) { - throw new Error(`npm pack did not produce a filename for ${packageRoot}`); - } - return path.join(tarballRoot, filename); -} - -async function installPackedWorkspaces(runtimeRoot, bundledNodeRoot, tarballs) { - const nodeExecutable = process.platform === "win32" - ? path.join(bundledNodeRoot, "node.exe") - : path.join(bundledNodeRoot, "bin", "node"); - const npmCli = process.platform === "win32" - ? path.join(bundledNodeRoot, "node_modules", "npm", "bin", "npm-cli.js") - : path.join(bundledNodeRoot, "lib", "node_modules", "npm", "bin", "npm-cli.js"); - const install = spawnSync(nodeExecutable, [ - npmCli, - "install", - "--include=optional", - "--omit=dev", - "--no-package-lock", - "--no-save", - ...tarballs, - ], { - cwd: runtimeRoot, - stdio: "inherit", - env: { - ...process.env, - npm_config_audit: "false", - npm_config_fund: "false", - }, - }); - if (install.status !== 0) { - throw new Error( - `Managed runtime dependency install failed with exit code ${install.status ?? 1}.` - ); - } -} - -function resolveSherpaNativePackageName() { - const sherpaPlatformMap = { - darwin: "darwin", - linux: "linux", - win32: "win", - }; - const sherpaPlatform = sherpaPlatformMap[process.platform]; - if (!sherpaPlatform) { - throw new Error(`Managed runtime sherpa package is not implemented for ${process.platform}.`); - } - return `sherpa-onnx-${sherpaPlatform}-${process.arch}`; -} - -async function ensureSherpaNativePackage(runtimeRoot, bundledNodeRoot) { - const packageName = resolveSherpaNativePackageName(); - const packageDir = path.join(runtimeRoot, "node_modules", packageName); - if (await pathExists(packageDir)) { - return; - } - - const sherpaNodePackageJson = JSON.parse( - await fs.readFile( - path.join(runtimeRoot, "node_modules", "sherpa-onnx-node", "package.json"), - "utf8" - ) - ); - const packageVersion = sherpaNodePackageJson.version; - if (!packageVersion) { - throw new Error("Missing version in sherpa-onnx-node package.json."); - } - - const nodeExecutable = process.platform === "win32" - ? path.join(bundledNodeRoot, "node.exe") - : path.join(bundledNodeRoot, "bin", "node"); - const npmCli = process.platform === "win32" - ? path.join(bundledNodeRoot, "node_modules", "npm", "bin", "npm-cli.js") - : path.join(bundledNodeRoot, "lib", "node_modules", "npm", "bin", "npm-cli.js"); - const install = spawnSync(nodeExecutable, [ - npmCli, - "install", - "--include=optional", - "--omit=dev", - "--no-package-lock", - "--no-save", - `${packageName}@${packageVersion}`, - ], { - cwd: runtimeRoot, - stdio: "inherit", - env: { - ...process.env, - npm_config_audit: "false", - npm_config_fund: "false", - }, - }); - if (install.status !== 0) { - throw new Error( - `Managed runtime sherpa package install failed with exit code ${install.status ?? 1}.` - ); - } -} - -async function writeRuntimePackageJson(runtimeRoot) { - await fs.writeFile( - path.join(runtimeRoot, "package.json"), - JSON.stringify( - { - name: "paseo-managed-runtime", - private: true, - version: packageVersion, - }, - null, - 2 - ) + "\n", - "utf8" - ); -} - -async function pruneChildrenExcept(parent, keepNames) { - if (!(await pathExists(parent))) { - return; - } - const entries = await fs.readdir(parent); - await Promise.all( - entries - .filter((entry) => !keepNames.has(entry)) - .map((entry) => rmSafe(path.join(parent, entry))) - ); -} - -async function pruneNodeDistribution(runtimeRoot) { - const nodeRoot = path.join(runtimeRoot, "node"); - await Promise.all([ - removeIfExists(path.join(nodeRoot, "include")), - removeIfExists(path.join(nodeRoot, "share")), - removeIfExists(path.join(nodeRoot, "CHANGELOG.md")), - ]); - - const nodeGlobalModulesRoot = path.join(nodeRoot, "lib", "node_modules"); - if (await pathExists(nodeGlobalModulesRoot)) { - const keepNames = new Set(["npm"]); - await pruneChildrenExcept(nodeGlobalModulesRoot, keepNames); - } -} - -async function pruneOnnxRuntime(runtimeRoot) { - const onnxRoot = path.join(runtimeRoot, "node_modules", "onnxruntime-node", "bin", "napi-v6"); - if (!(await pathExists(onnxRoot))) { - return; - } - if (process.platform === "darwin") { - await removeIfExists(path.join(onnxRoot, "linux")); - await removeIfExists(path.join(onnxRoot, "win32")); - await pruneChildrenExcept(path.join(onnxRoot, "darwin"), new Set([process.arch])); - return; - } - if (process.platform === "linux") { - await removeIfExists(path.join(onnxRoot, "darwin")); - await removeIfExists(path.join(onnxRoot, "win32")); - await pruneChildrenExcept(path.join(onnxRoot, "linux"), new Set([process.arch])); - const archDir = path.join(onnxRoot, "linux", process.arch); - if (await pathExists(archDir)) { - const entries = await fs.readdir(archDir); - await Promise.all( - entries - .filter((name) => name.includes("cuda") || name.includes("tensorrt")) - .map((name) => fs.rm(path.join(archDir, name), { force: true })) - ); - } - return; - } - if (process.platform === "win32") { - await removeIfExists(path.join(onnxRoot, "darwin")); - await removeIfExists(path.join(onnxRoot, "linux")); - await pruneChildrenExcept(path.join(onnxRoot, "win32"), new Set([process.arch])); - return; - } -} - -async function pruneNodePty(runtimeRoot) { - const prebuildsRoot = path.join(runtimeRoot, "node_modules", "node-pty", "prebuilds"); - const keepName = `${process.platform}-${process.arch}`; - await pruneChildrenExcept(prebuildsRoot, new Set([keepName])); - - if (process.platform !== "win32") { - await removeIfExists(path.join(runtimeRoot, "node_modules", "node-pty", "third_party")); - } -} - -async function pruneClaudeAgentSdk(runtimeRoot) { - const vendorRoot = path.join( - runtimeRoot, - "node_modules", - "@anthropic-ai", - "claude-agent-sdk", - "vendor" - ); - const ripgrepRoot = path.join(vendorRoot, "ripgrep"); - const keepName = ripgrepPlatformDirMap[process.platform]?.[process.arch]; - if (keepName) { - await pruneChildrenExcept(ripgrepRoot, new Set(["COPYING", keepName])); - } - - const treeSitterBashRoot = path.join(vendorRoot, "tree-sitter-bash"); - if (keepName) { - await pruneChildrenExcept(treeSitterBashRoot, new Set([keepName])); - } -} - -async function pruneManagedRuntime(runtimeRoot) { - await Promise.all([ - pruneNodeDistribution(runtimeRoot), - pruneOnnxRuntime(runtimeRoot), - pruneNodePty(runtimeRoot), - pruneClaudeAgentSdk(runtimeRoot), - ]); -} - -async function main() { - await ensureWorkspaceBuilds(); - - const toolVersions = await readToolVersions(); - const nodeVersion = resolveNodeVersion(toolVersions); - const nodeArtifact = resolveNodeArtifact(nodeVersion); - - await rmSafe(resourcesRoot); - await ensureDir(resourcesRoot); - - const archivePath = await ensureNodeArchive(nodeArtifact); - const { extractionRoot, extractedRoot } = await extractNodeDistribution(archivePath, nodeArtifact); - const tarballRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paseo-managed-runtime-pack-")); - - try { - const tarballs = []; - for (const workspace of workspaces) { - tarballs.push({ - name: workspace.name, - path: await packWorkspace(workspace.root, tarballRoot), - }); - } - const runtimeContentHash = await computeRuntimeContentHash({ - nodeVersion, - tarballs, - }); - const runtimeId = - `${packageVersion}-node-${nodeVersion}-${process.platform}-${process.arch}-${runtimeContentHash}`; - const runtimeRoot = path.join(resourcesRoot, runtimeId); - - await ensureDir(runtimeRoot); - await copyDir(extractedRoot, path.join(runtimeRoot, "node")); - await writeRuntimePackageJson(runtimeRoot); - await installPackedWorkspaces( - runtimeRoot, - path.join(runtimeRoot, "node"), - tarballs.map((entry) => entry.path) - ); - await ensureSherpaNativePackage(runtimeRoot, path.join(runtimeRoot, "node")); - await pruneManagedRuntime(runtimeRoot); - - const nodeRelativePath = process.platform === "win32" - ? path.join("node", "node.exe") - : path.join("node", "bin", "node"); - const manifest = { - runtimeId, - runtimeVersion: packageVersion, - platform: process.platform, - arch: process.arch, - createdAt: new Date().toISOString(), - nodeRelativePath, - cliEntrypointRelativePath: path.join( - "node_modules", - "@getpaseo", - "cli", - "dist", - "index.js" - ), - cliShimRelativePath: path.join("node_modules", "@getpaseo", "cli", "bin", "paseo"), - serverRunnerRelativePath: path.join( - "node_modules", - "@getpaseo", - "server", - "dist", - "scripts", - "daemon-runner.js" - ), - }; - - await fs.writeFile( - path.join(runtimeRoot, "runtime-manifest.json"), - JSON.stringify(manifest, null, 2) + "\n", - "utf8" - ); - - await fs.writeFile( - path.join(resourcesRoot, "current-runtime.json"), - JSON.stringify( - { - runtimeId, - runtimeVersion: packageVersion, - relativeRoot: runtimeId, - }, - null, - 2 - ) + "\n", - "utf8" - ); - - if (process.platform !== "win32") { - await fs.chmod(path.join(runtimeRoot, nodeRelativePath), 0o755); - } - } finally { - await rmSafe(extractionRoot); - await rmSafe(tarballRoot); - } -} - -await main(); diff --git a/packages/desktop/scripts/managed-daemon-smoke.mjs b/packages/desktop/scripts/managed-daemon-smoke.mjs deleted file mode 100644 index 73827c17a..000000000 --- a/packages/desktop/scripts/managed-daemon-smoke.mjs +++ /dev/null @@ -1,807 +0,0 @@ -import assert from "node:assert/strict"; -import { Buffer } from "node:buffer"; -import { execFile, spawn } from "node:child_process"; -import fs from "node:fs/promises"; -import net from "node:net"; -import os from "node:os"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { promisify } from "node:util"; -import WebSocket from "ws"; -import { createClientChannel } from "@getpaseo/relay/e2ee"; - -const execFileAsync = promisify(execFile); -const COMMAND_TIMEOUT_MS = 120_000; -const repoRoot = fileURLToPath(new URL("../../..", import.meta.url)); -const desktopRoot = path.join(repoRoot, "packages", "desktop"); -const relayRoot = path.join(repoRoot, "packages", "relay"); -const desktopPackageJson = JSON.parse( - await fs.readFile(path.join(desktopRoot, "package.json"), "utf8") -); -const currentRuntimeVersion = desktopPackageJson.version; -const currentRuntimePointer = JSON.parse( - await fs.readFile( - path.join(desktopRoot, "src-tauri", "resources", "managed-runtime", "current-runtime.json"), - "utf8" - ) -); -const currentRuntimeId = currentRuntimePointer.runtimeId; - -function resolvePackagedBinary() { - const targetRoot = process.env.PASEO_MANAGED_SMOKE_RUST_TARGET - ? path.join( - desktopRoot, - "src-tauri", - "target", - process.env.PASEO_MANAGED_SMOKE_RUST_TARGET, - "release" - ) - : path.join(desktopRoot, "src-tauri", "target", "release"); - if (process.platform === "darwin") { - return path.join( - targetRoot, - "bundle", - "macos", - "Paseo.app", - "Contents", - "MacOS", - "Paseo" - ); - } - if (process.platform === "linux") { - return path.join( - targetRoot, - "bundle", - "appimage", - `Paseo_${desktopPackageJson.version}_amd64.AppImage` - ); - } - if (process.platform === "win32") { - return path.join( - targetRoot, - "Paseo.exe" - ); - } - throw new Error(`Managed desktop smoke is not implemented for ${process.platform} yet.`); -} - -async function pathExists(target) { - try { - await fs.access(target); - return true; - } catch { - return false; - } -} - -async function snapshotTree(root) { - if (!(await pathExists(root))) { - return []; - } - const entries = []; - async function walk(current) { - const stat = await fs.stat(current); - const relative = path.relative(root, current) || "."; - entries.push({ - relative, - kind: stat.isDirectory() ? "dir" : "file", - size: stat.size, - mtimeMs: stat.mtimeMs, - }); - if (!stat.isDirectory()) { - return; - } - const children = await fs.readdir(current); - children.sort(); - for (const child of children) { - await walk(path.join(current, child)); - } - } - await walk(root); - entries.sort((left, right) => left.relative.localeCompare(right.relative)); - return entries; -} - -async function sleep(ms) { - await new Promise((resolve) => setTimeout(resolve, ms)); -} - -function escapeForRegExp(value) { - return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} - -async function waitForChildExit(child, timeoutMs) { - if (!child || child.exitCode !== null || child.killed) { - return; - } - await new Promise((resolve) => { - const timeout = setTimeout(resolve, timeoutMs); - child.once("exit", () => { - clearTimeout(timeout); - resolve(); - }); - }); -} - -async function execFileWithTimeout(command, args, options, label) { - try { - return await execFileAsync(command, args, { - timeout: COMMAND_TIMEOUT_MS, - ...options, - }); - } catch (error) { - if (error?.killed && error?.signal === "SIGTERM") { - const renderedArgs = args.map((arg) => JSON.stringify(arg)).join(" "); - throw new Error( - `Timed out after ${COMMAND_TIMEOUT_MS}ms running ${label}: ${JSON.stringify(command)} ${renderedArgs}` - ); - } - throw error; - } -} - -async function runBinary(binaryPath, args, env) { - const { stdout, stderr } = await execFileWithTimeout(binaryPath, args, { - env, - cwd: repoRoot, - maxBuffer: 10 * 1024 * 1024, - }, "packaged desktop binary"); - const trimmed = stdout.trim(); - return { - stdout, - stderr, - json: trimmed ? JSON.parse(trimmed) : null, - }; -} - -async function terminateChildProcess(child, label) { - if (!child || child.exitCode !== null || child.killed) { - return; - } - if (process.platform === "win32" && typeof child.pid === "number") { - try { - await execFileWithTimeout( - "taskkill", - ["/pid", String(child.pid), "/T", "/F"], - { maxBuffer: 1024 * 1024 }, - `${label} taskkill` - ); - } catch {} - await waitForChildExit(child, 5_000); - return; - } - try { - child.kill("SIGTERM"); - } catch {} - await waitForChildExit(child, 5_000); - if (child.exitCode === null && !child.killed) { - try { - child.kill("SIGKILL"); - } catch {} - await waitForChildExit(child, 5_000); - } -} - -async function runWorkspaceCli(args, env) { - const { stdout, stderr } = await execFileWithTimeout( - process.execPath, - [path.join(repoRoot, "packages", "cli", "dist", "index.js"), ...args], - { - cwd: repoRoot, - env: { - ...process.env, - ...env, - }, - maxBuffer: 10 * 1024 * 1024, - }, - "workspace CLI" - ); - let json = null; - if (stdout.trim()) { - try { - json = JSON.parse(stdout.trim()); - } catch { - json = null; - } - } - return { stdout, stderr, json }; -} - -async function runBundledRuntimeCli(runtimeRoot, managedHome, args, env) { - const manifest = JSON.parse( - await fs.readFile(path.join(runtimeRoot, "runtime-manifest.json"), "utf8") - ); - const { stdout, stderr } = await execFileWithTimeout( - path.join(runtimeRoot, manifest.nodeRelativePath), - [path.join(runtimeRoot, manifest.cliEntrypointRelativePath), ...args], - { - cwd: repoRoot, - env: { - ...process.env, - ...env, - PASEO_HOME: managedHome, - }, - maxBuffer: 10 * 1024 * 1024, - }, - "bundled runtime CLI" - ); - return { stdout, stderr }; -} - -async function resolvePackagedRuntimeStatus(binaryPath) { - const binaryDir = path.dirname(binaryPath); - const resourceDirs = [ - path.join(binaryDir, "resources"), - binaryDir, - path.join(binaryDir, "..", "Resources"), - path.join(binaryDir, "..", "resources"), - ]; - - let bundledRoot = null; - for (const resourceDir of resourceDirs) { - for (const candidate of [ - path.join(resourceDir, "resources", "managed-runtime"), - path.join(resourceDir, "managed-runtime"), - ]) { - if (await pathExists(candidate)) { - bundledRoot = candidate; - break; - } - } - if (bundledRoot) { - break; - } - } - - if (!bundledRoot) { - throw new Error("Managed runtime resources are not bundled with this desktop build."); - } - - const pointer = JSON.parse( - await fs.readFile(path.join(bundledRoot, "current-runtime.json"), "utf8") - ); - const runtimeRoot = path.join(bundledRoot, pointer.relativeRoot); - const manifest = JSON.parse( - await fs.readFile(path.join(runtimeRoot, "runtime-manifest.json"), "utf8") - ); - - return { - runtimeId: manifest.runtimeId, - runtimeVersion: manifest.runtimeVersion, - runtimeRoot, - }; -} - -async function readDaemonStatus(home, env) { - const result = await runWorkspaceCli(["daemon", "status", "--home", home, "--json"], env); - return result.json ?? {}; -} - -async function pidListeningOnPort(port) { - try { - const { stdout } = await execFileAsync("lsof", ["-ti", `tcp:${port}`], { - maxBuffer: 1024 * 1024, - }); - const pid = Number.parseInt(stdout.trim().split("\n")[0] ?? "", 10); - return Number.isFinite(pid) ? pid : null; - } catch { - return null; - } -} - -async function getAvailablePort() { - return await new Promise((resolve, reject) => { - const server = net.createServer(); - server.unref(); - server.on("error", reject); - server.listen(0, "127.0.0.1", () => { - const address = server.address(); - if (!address || typeof address === "string") { - server.close(() => reject(new Error("Failed to allocate an ephemeral test port."))); - return; - } - const { port } = address; - server.close((error) => { - if (error) { - reject(error); - return; - } - resolve(port); - }); - }); - }); -} - -function buildRelayWebSocketUrl({ endpoint, serverId, role }) { - const url = new URL(`ws://${endpoint}/ws`); - url.searchParams.set("serverId", serverId); - url.searchParams.set("role", role); - url.searchParams.set("v", "2"); - return url.toString(); -} - -function parseOfferUrlFromCommandOutput(stdout) { - const match = stdout.match(/https?:\/\/\S+#offer=\S+/); - if (!match) { - throw new Error(`Failed to find pairing URL in output: ${stdout}`); - } - return match[0]; -} - -function decodeOfferFromFragmentUrl(url) { - const marker = "#offer="; - const index = url.indexOf(marker); - if (index === -1) { - throw new Error(`Pairing URL is missing ${marker}: ${url}`); - } - const encoded = url.slice(index + marker.length); - const raw = Buffer.from(encoded, "base64url").toString("utf8"); - const offer = JSON.parse(raw); - if (offer?.v !== 2 || typeof offer?.serverId !== "string" || typeof offer?.daemonPublicKeyB64 !== "string") { - throw new Error(`Unexpected relay offer payload: ${raw}`); - } - return offer; -} - -async function waitForRelayWebSocketReady(endpoint, timeoutMs) { - await waitFor(async () => { - const probeServerId = `probe-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`; - const url = buildRelayWebSocketUrl({ - endpoint, - serverId: probeServerId, - role: "server", - }); - const opened = await new Promise((resolve) => { - const ws = new WebSocket(url); - const timer = setTimeout(() => { - ws.terminate(); - resolve(false); - }, 5_000); - ws.once("open", () => { - clearTimeout(timer); - ws.close(1000, "probe"); - resolve(true); - }); - ws.once("error", () => { - clearTimeout(timer); - resolve(false); - }); - }); - assert.equal(opened, true); - }, timeoutMs, "relay websocket endpoint to accept connections"); -} - -async function connectViaRelay(endpoint, offer) { - const stableClientId = `cid-managed-smoke-${Date.now().toString(36)}`; - const ws = new WebSocket( - buildRelayWebSocketUrl({ - endpoint, - serverId: offer.serverId, - role: "client", - }) - ); - return await new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - ws.close(); - reject(new Error("Timed out waiting for managed relay pong.")); - }, 20_000); - - const transport = { - send: (data) => ws.send(data), - close: (code, reason) => ws.close(code, reason), - onmessage: null, - onclose: null, - onerror: null, - }; - - ws.on("message", (data) => { - transport.onmessage?.(typeof data === "string" ? data : data.toString()); - }); - ws.on("close", (code, reason) => { - transport.onclose?.(code, reason.toString()); - }); - ws.on("error", (error) => { - transport.onerror?.(error); - }); - - ws.on("open", async () => { - try { - let pingSent = false; - let channelRef = null; - const channel = await createClientChannel(transport, offer.daemonPublicKeyB64, { - onmessage: (data) => { - try { - const payload = typeof data === "string" ? JSON.parse(data) : data; - if (payload?.type === "welcome") { - if (!pingSent && channelRef) { - pingSent = true; - void channelRef.send(JSON.stringify({ type: "ping" })); - } - return; - } - if (payload?.type === "pong") { - clearTimeout(timeout); - resolve(payload); - ws.close(); - } - } catch (error) { - clearTimeout(timeout); - reject(error); - } - }, - onerror: (error) => { - clearTimeout(timeout); - reject(error); - }, - }); - channelRef = channel; - await channel.send( - JSON.stringify({ - type: "hello", - clientId: stableClientId, - clientType: "cli", - protocolVersion: 1, - }) - ); - } catch (error) { - clearTimeout(timeout); - reject(error); - } - }); - }); -} - -function assertNoForbiddenPathsOrPorts(value, forbidden) { - const text = - typeof value === "string" ? value : JSON.stringify(value, null, 2); - for (const candidate of forbidden) { - assert.ok( - !text.includes(candidate), - `Unexpected forbidden managed smoke reference: ${candidate}` - ); - } -} - -async function ensurePackagedArtifact(binaryPath) { - if (process.env.PASEO_MANAGED_SMOKE_SKIP_BUILD === "1") { - return; - } - const npmExecPath = process.env.npm_execpath; - if (!npmExecPath) { - throw new Error("npm_execpath is required to build the packaged desktop artifact during smoke tests."); - } - try { - await execFileWithTimeout(process.execPath, [npmExecPath, "run", "build"], { - cwd: desktopRoot, - env: process.env, - maxBuffer: 20 * 1024 * 1024, - }, "desktop smoke artifact build"); - } catch (error) { - const combined = `${error.stdout ?? ""}\n${error.stderr ?? ""}`; - const signingBlocked = - combined.includes("TAURI_SIGNING_PRIVATE_KEY") && - (await pathExists(binaryPath)); - if (!signingBlocked) { - throw error; - } - console.warn( - "[managed-smoke] continuing after Tauri updater signing failure because the packaged app artifact was already produced" - ); - } -} - -async function waitFor(assertion, timeoutMs, label) { - const startedAt = Date.now(); - while (Date.now() - startedAt < timeoutMs) { - try { - return await assertion(); - } catch { - await sleep(250); - } - } - throw new Error(`Timed out waiting for ${label}`); -} - -function logStep(label) { - currentStepLabel = label; - currentStepStartedAt = Date.now(); - console.log(`\n[managed-smoke] ${label}`); -} - -const packagedBinary = resolvePackagedBinary(); -await ensurePackagedArtifact(packagedBinary); -if (!(await pathExists(packagedBinary))) { - throw new Error(`Packaged desktop artifact not found: ${packagedBinary}`); -} - -const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paseo-desktop-smoke-")); -const testRoot = path.join(tmpRoot, "managed-test-root"); -const fakeHome = path.join(tmpRoot, "fake-home"); -const fakePaseoHome = path.join(fakeHome, ".paseo"); -const cliScratchHome = path.join(tmpRoot, "cli-scratch-home"); -const externalHome = path.join(tmpRoot, "external-daemon-home"); -const externalPort = await getAvailablePort(); -const externalEndpoint = `127.0.0.1:${externalPort}`; -const relayPort = await getAvailablePort(); -const relayEndpoint = `127.0.0.1:${relayPort}`; -const managedRuntimeDir = path.join(testRoot, "runtime"); -await fs.mkdir(testRoot, { recursive: true }); -await fs.mkdir(fakePaseoHome, { recursive: true }); -await fs.mkdir(cliScratchHome, { recursive: true }); -await fs.mkdir(externalHome, { recursive: true }); -await fs.writeFile(path.join(fakePaseoHome, "sentinel.txt"), "do not touch\n", "utf8"); - -const fakePaseoSnapshotBefore = await snapshotTree(fakePaseoHome); -const smokeStartedAt = Date.now(); -const smokeDeadlineMs = 15 * 60 * 1000; -let currentStepLabel = "initializing"; -let currentStepStartedAt = smokeStartedAt; -const heartbeat = setInterval(() => { - const elapsedSeconds = Math.floor((Date.now() - smokeStartedAt) / 1000); - const currentStepSeconds = Math.floor((Date.now() - currentStepStartedAt) / 1000); - console.log( - `[managed-smoke] heartbeat elapsed=${elapsedSeconds}s currentStep=${JSON.stringify(currentStepLabel)} stepElapsed=${currentStepSeconds}s` - ); - if (Date.now() - smokeStartedAt > smokeDeadlineMs) { - console.error( - `[managed-smoke] FAIL exceeded ${smokeDeadlineMs / 1000}s overall deadline during ${JSON.stringify(currentStepLabel)}` - ); - process.exit(1); - } -}, 30_000); -const managedEnv = { - ...process.env, - HOME: fakeHome, - PASEO_HOME: cliScratchHome, - PASEO_DESKTOP_TEST_ROOT: testRoot, - PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD: "0", - PASEO_DICTATION_ENABLED: "0", - PASEO_VOICE_MODE_ENABLED: "0", - PASEO_RELAY_ENABLED: "true", - PASEO_RELAY_ENDPOINT: relayEndpoint, - PASEO_RELAY_PUBLIC_ENDPOINT: relayEndpoint, - PASEO_APP_BASE_URL: "https://app.paseo.test", - PASEO_PRIMARY_LAN_IP: "192.168.1.12", - CI: "true", -}; -let externalPid = null; -let startedTemporaryExternalDaemon = false; -let relayProcess = null; -let runtimeStatus = null; -let managedStart = null; -const forbiddenManagedReferences = ["127.0.0.1:6767", fakePaseoHome, managedRuntimeDir]; -const npmExecPath = process.env.npm_execpath; -if (!npmExecPath) { - throw new Error("npm_execpath is required to launch wrangler during managed desktop smoke tests."); -} - -try { - logStep(`Starting isolated local relay on ${relayEndpoint}`); - relayProcess = spawn(process.execPath, [ - npmExecPath, - "exec", - "--", - "wrangler", - "dev", - "--local", - "--ip", - "127.0.0.1", - "--port", - String(relayPort), - "--live-reload=false", - "--show-interactive-dev-session=false", - ], { - cwd: relayRoot, - env: process.env, - stdio: ["ignore", "pipe", "pipe"], - }); - relayProcess.stdout?.on("data", (chunk) => { - process.stdout.write(`[managed-smoke relay] ${chunk}`); - }); - relayProcess.stderr?.on("data", (chunk) => { - process.stderr.write(`[managed-smoke relay] ${chunk}`); - }); - await waitFor(async () => { - await execFileAsync(process.execPath, ["-e", `require("node:net").connect(${relayPort}, "127.0.0.1").on("connect", function () { this.end(); process.exit(0); }).on("error", () => process.exit(1));`]); - }, 60_000, "relay HTTP endpoint to start"); - await waitForRelayWebSocketReady(relayEndpoint, 60_000); - - logStep(`Starting isolated external daemon on ${externalEndpoint}`); - startedTemporaryExternalDaemon = true; - await runWorkspaceCli( - ["start", "--home", externalHome, "--listen", externalEndpoint, "--no-relay"], - managedEnv - ); - const externalStatus = await waitFor( - async () => { - const status = await readDaemonStatus(externalHome, managedEnv); - assert.equal(status.status, "running"); - assert.ok(typeof status.pid === "number" && status.pid > 0, "expected external daemon pid"); - await runWorkspaceCli(["ls", "--host", externalEndpoint, "--json"], managedEnv); - return status; - }, - 15_000, - "external daemon to start" - ); - externalPid = externalStatus.pid; - assert.ok(externalPid, "external daemon pid should be present"); - - logStep("Resolving bundled runtime from packaged desktop artifact"); - runtimeStatus = await resolvePackagedRuntimeStatus(packagedBinary); - assert.equal(runtimeStatus.runtimeId, currentRuntimeId); - assert.equal(runtimeStatus.runtimeVersion, currentRuntimeVersion); - assert.match( - runtimeStatus.runtimeRoot, - new RegExp(escapeForRegExp(currentRuntimeId)), - "runtime status should point into the bundled runtime selected by current-runtime.json" - ); - assert.equal( - runtimeStatus.runtimeRoot.startsWith(testRoot), - false, - "runtime status should not resolve into managed app data" - ); - assertNoForbiddenPathsOrPorts(runtimeStatus, forbiddenManagedReferences); - assert.equal(await pathExists(managedRuntimeDir), false, "managed app data should not gain a runtime tree"); - - const managedBootstrap = await runBundledRuntimeCli( - runtimeStatus.runtimeRoot, - cliScratchHome, - ["start", "--json"], - managedEnv - ); - managedStart = (managedBootstrap.stdout.trim() ? JSON.parse(managedBootstrap.stdout.trim()) : null) ?? await waitFor( - async () => { - const status = await runBundledRuntimeCli( - runtimeStatus.runtimeRoot, - cliScratchHome, - ["daemon", "status", "--json"], - managedEnv - ); - const json = JSON.parse(status.stdout.trim()); - assert.equal(json.status, "running"); - assert.ok(json.pid, "managed daemon pid should exist"); - assert.ok(json.serverId, "managed daemon should expose a server id"); - return json; - }, - 10_000, - "managed daemon bootstrap status" - ); - assert.equal(managedStart.status, "running"); - assert.ok(managedStart.pid, "managed daemon pid should exist"); - assert.ok(managedStart.serverId, "managed daemon should expose a server id"); - assert.ok(managedStart.home, "managed daemon should expose its home directory"); - assert.ok(managedStart.listen, "managed daemon should expose its listen target"); - assertNoForbiddenPathsOrPorts(managedStart, forbiddenManagedReferences); - assert.equal(await pathExists(managedRuntimeDir), false, "starting the daemon should not install a runtime copy"); - - const managedPid = managedStart.pid; - - logStep("Verifying the managed daemon stays alive after the packaged command exits"); - await sleep(1_500); - const persistedManagedStatus = JSON.parse( - ( - await runBundledRuntimeCli( - runtimeStatus.runtimeRoot, - managedStart.home, - ["daemon", "status", "--json"], - managedEnv - ) - ).stdout.trim() - ); - assert.equal(persistedManagedStatus.pid, managedPid); - assert.equal(persistedManagedStatus.status, "running"); - assert.equal(persistedManagedStatus.serverId, managedStart.serverId); - assertNoForbiddenPathsOrPorts(persistedManagedStatus, forbiddenManagedReferences); - - logStep("Verifying bundled CLI access without installing a shim"); - const cliVersion = await runBundledRuntimeCli( - runtimeStatus.runtimeRoot, - managedStart.home, - ["--version"], - managedEnv - ); - assert.match(cliVersion.stdout.trim(), /^0\./); - const bundledCliStatus = await runBundledRuntimeCli( - runtimeStatus.runtimeRoot, - managedStart.home, - ["daemon", "status", "--json"], - managedEnv - ); - const bundledCliDaemonStatus = JSON.parse(bundledCliStatus.stdout.trim()); - assert.equal(bundledCliDaemonStatus.pid, managedPid); - assertNoForbiddenPathsOrPorts(bundledCliDaemonStatus, forbiddenManagedReferences); - - logStep("Verifying relay connectivity still works after the desktop command has exited"); - const relayPairing = await runBundledRuntimeCli( - runtimeStatus.runtimeRoot, - managedStart.home, - ["daemon", "pair"], - managedEnv - ); - const relayOfferUrl = parseOfferUrlFromCommandOutput(relayPairing.stdout); - const relayOffer = decodeOfferFromFragmentUrl(relayOfferUrl); - assert.equal(relayOffer.relay?.endpoint, relayEndpoint); - const relayPong = await connectViaRelay(relayEndpoint, relayOffer); - assert.deepEqual(relayPong, { type: "pong" }); - - logStep("Re-running managed start without spawning duplicate daemons"); - const managedRestartless = JSON.parse( - ( - await runBundledRuntimeCli( - runtimeStatus.runtimeRoot, - managedStart.home, - ["start", "--json"], - managedEnv - ) - ).stdout.trim() - ); - assert.equal(managedRestartless.pid, managedPid); - - logStep("Verifying managed and external daemons coexist"); - const externalStatusAfter = await readDaemonStatus(externalHome, managedEnv); - const externalPidAfter = externalStatusAfter.pid ?? null; - assert.equal(externalStatusAfter.status, "running"); - assert.equal(externalPidAfter, externalPid); - await runWorkspaceCli(["ls", "--host", externalEndpoint, "--json"], managedEnv); - const managedStatus = JSON.parse( - ( - await runBundledRuntimeCli( - runtimeStatus.runtimeRoot, - managedStart.home, - ["daemon", "status", "--json"], - managedEnv - ) - ).stdout.trim() - ); - assert.ok(managedStatus.serverId, "managed daemon should expose a server id"); - assertNoForbiddenPathsOrPorts(managedStatus, forbiddenManagedReferences); - - logStep("Capturing diagnostics and verifying the fake ~/.paseo stayed untouched"); - const fakePaseoSnapshotAfter = await snapshotTree(fakePaseoHome); - assert.deepEqual(fakePaseoSnapshotAfter, fakePaseoSnapshotBefore); - await fs.writeFile( - path.join(testRoot, "managed-daemon-smoke-diagnostics.json"), - JSON.stringify( - { - runtimeStatus, - managedBootstrap: managedBootstrap.stdout.trim() ? JSON.parse(managedBootstrap.stdout.trim()) : null, - managedStart, - persistedManagedStatus, - managedRestartless, - bundledCliDaemonStatus, - relayEndpoint, - relayOfferUrl, - relayPong, - externalEndpoint, - externalPid, - externalPidAfter, - managedStatus, - }, - null, - 2 - ) + "\n", - "utf8" - ); - - console.log(`\n[managed-smoke] PASS (${testRoot})`); -} finally { - clearInterval(heartbeat); - try { - if (runtimeStatus?.runtimeRoot) { - await runBundledRuntimeCli( - runtimeStatus.runtimeRoot, - managedStart?.home ?? cliScratchHome, - ["daemon", "stop", "--json"], - managedEnv - ); - } - } catch {} - try { - if (startedTemporaryExternalDaemon) { - await runWorkspaceCli(["daemon", "stop", "--home", externalHome, "--json"]); - } - } catch {} - try { - await terminateChildProcess(relayProcess, "local relay"); - } catch {} -} diff --git a/packages/desktop/scripts/paseo b/packages/desktop/scripts/paseo new file mode 100755 index 000000000..ad08d4a10 --- /dev/null +++ b/packages/desktop/scripts/paseo @@ -0,0 +1,28 @@ +#!/bin/sh +set -eu + +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +APP_EXECUTABLE="${SCRIPT_DIR}/Paseo" + +if [ -d "${SCRIPT_DIR}/../Resources" ]; then + RESOURCES_DIR="${SCRIPT_DIR}/../Resources" +elif [ -d "${SCRIPT_DIR}/resources" ]; then + RESOURCES_DIR="${SCRIPT_DIR}/resources" +else + echo "Unable to locate Paseo resources directory next to ${SCRIPT_DIR}" >&2 + exit 1 +fi + +CLI_ENTRY="${RESOURCES_DIR}/app.asar/node_modules/@getpaseo/cli/dist/index.js" + +if [ ! -x "${APP_EXECUTABLE}" ]; then + echo "Bundled Paseo executable not found at ${APP_EXECUTABLE}" >&2 + exit 1 +fi + +if [ ! -f "${CLI_ENTRY}" ]; then + echo "Bundled Paseo CLI entrypoint not found at ${CLI_ENTRY}" >&2 + exit 1 +fi + +exec env ELECTRON_RUN_AS_NODE=1 "${APP_EXECUTABLE}" "${CLI_ENTRY}" "$@" diff --git a/packages/desktop/scripts/paseo.cmd b/packages/desktop/scripts/paseo.cmd new file mode 100644 index 000000000..95f83ebb3 --- /dev/null +++ b/packages/desktop/scripts/paseo.cmd @@ -0,0 +1,19 @@ +@echo off +setlocal + +set "SCRIPT_DIR=%~dp0" +set "APP_EXECUTABLE=%SCRIPT_DIR%Paseo.exe" +set "CLI_ENTRY=%SCRIPT_DIR%resources\app.asar\node_modules\@getpaseo\cli\dist\index.js" + +if not exist "%APP_EXECUTABLE%" ( + echo Bundled Paseo executable not found at %APP_EXECUTABLE% 1>&2 + exit /b 1 +) + +if not exist "%CLI_ENTRY%" ( + echo Bundled Paseo CLI entrypoint not found at %CLI_ENTRY% 1>&2 + exit /b 1 +) + +set "ELECTRON_RUN_AS_NODE=1" +"%APP_EXECUTABLE%" "%CLI_ENTRY%" %* diff --git a/packages/desktop/scripts/sign-managed-runtime-macos.mjs b/packages/desktop/scripts/sign-managed-runtime-macos.mjs deleted file mode 100644 index 3f8e129e9..000000000 --- a/packages/desktop/scripts/sign-managed-runtime-macos.mjs +++ /dev/null @@ -1,144 +0,0 @@ -import { spawnSync } from "node:child_process"; -import fs from "node:fs/promises"; -import path from "node:path"; - -const repoRoot = path.resolve(new URL("../../..", import.meta.url).pathname); -const desktopRoot = path.join(repoRoot, "packages", "desktop"); -const resourcesRoot = path.join(desktopRoot, "src-tauri", "resources", "managed-runtime"); -const signingIdentity = process.env.APPLE_SIGNING_IDENTITY; - -if (process.platform !== "darwin") { - throw new Error("sign-managed-runtime-macos.mjs can only run on macOS."); -} - -if (!signingIdentity) { - throw new Error("APPLE_SIGNING_IDENTITY is required to sign the managed runtime."); -} - -async function pathExists(target) { - try { - await fs.access(target); - return true; - } catch { - return false; - } -} - -async function walkFiles(root) { - const files = []; - async function walk(current) { - const stat = await fs.stat(current); - if (stat.isDirectory()) { - const children = await fs.readdir(current); - children.sort(); - for (const child of children) { - await walk(path.join(current, child)); - } - return; - } - files.push(current); - } - await walk(root); - return files; -} - -function run(command, args, options = {}) { - const result = spawnSync(command, args, { - stdio: "pipe", - encoding: "utf8", - ...options, - }); - if (result.status !== 0) { - throw new Error( - [ - `Command failed: ${command} ${args.join(" ")}`, - result.stdout?.trim(), - result.stderr?.trim(), - ] - .filter(Boolean) - .join("\n") - ); - } - return result.stdout.trim(); -} - -function getFileKind(target) { - return run("file", ["-b", target]); -} - -function isMachO(fileKind) { - return fileKind.includes("Mach-O"); -} - -function needsHardenedRuntime(fileKind) { - return fileKind.includes("executable"); -} - -function extractEntitlements(file) { - const result = spawnSync("codesign", ["-d", "--entitlements", "-", "--xml", file], { - stdio: "pipe", - encoding: "utf8", - }); - if (result.status !== 0 || !result.stdout || result.stdout.trim().length === 0) { - return null; - } - return result.stdout; -} - -async function main() { - const pointer = JSON.parse( - await fs.readFile(path.join(resourcesRoot, "current-runtime.json"), "utf8") - ); - const runtimeRoot = path.join(resourcesRoot, pointer.relativeRoot); - if (!(await pathExists(runtimeRoot))) { - throw new Error(`Managed runtime root does not exist: ${runtimeRoot}`); - } - - const files = await walkFiles(runtimeRoot); - const signTargets = []; - for (const file of files) { - const fileKind = getFileKind(file); - if (!isMachO(fileKind)) { - continue; - } - signTargets.push({ - file, - needsRuntime: needsHardenedRuntime(fileKind), - }); - } - - signTargets.sort((left, right) => left.file.localeCompare(right.file)); - - for (const target of signTargets) { - let entitlementsFile = null; - if (target.needsRuntime) { - const entitlements = extractEntitlements(target.file); - if (entitlements) { - entitlementsFile = `${target.file}.entitlements.plist`; - await fs.writeFile(entitlementsFile, entitlements, "utf8"); - } - } - - const args = [ - "--force", - "--sign", - signingIdentity, - "--timestamp", - ]; - if (target.needsRuntime) { - args.push("--options", "runtime"); - if (entitlementsFile) { - args.push("--entitlements", entitlementsFile); - } - } - args.push(target.file); - console.log(`[managed-runtime-sign] ${path.relative(repoRoot, target.file)}`); - run("codesign", args); - - if (entitlementsFile) { - await fs.unlink(entitlementsFile); - } - } -} - -await main(); diff --git a/packages/desktop/scripts/validate-managed-runtime.mjs b/packages/desktop/scripts/validate-managed-runtime.mjs deleted file mode 100644 index 0bc8384b9..000000000 --- a/packages/desktop/scripts/validate-managed-runtime.mjs +++ /dev/null @@ -1,82 +0,0 @@ -import assert from "node:assert/strict"; -import fs from "node:fs/promises"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const desktopRoot = fileURLToPath(new URL("..", import.meta.url)); -const resourcesRoot = path.join(desktopRoot, "src-tauri", "resources", "managed-runtime"); - -const desktopPackageJson = JSON.parse( - await fs.readFile(path.join(desktopRoot, "package.json"), "utf8") -); -const expectedVersion = desktopPackageJson.version; - -const currentRuntime = JSON.parse( - await fs.readFile(path.join(resourcesRoot, "current-runtime.json"), "utf8") -); -assert.equal(currentRuntime.runtimeVersion, expectedVersion, "current-runtime.json version mismatch"); -assert.ok(currentRuntime.runtimeId, "current-runtime.json missing runtimeId"); -assert.ok(currentRuntime.relativeRoot, "current-runtime.json missing relativeRoot"); - -const runtimeRoot = path.join(resourcesRoot, currentRuntime.relativeRoot); -const manifest = JSON.parse( - await fs.readFile(path.join(runtimeRoot, "runtime-manifest.json"), "utf8") -); -assert.equal(manifest.runtimeId, currentRuntime.runtimeId, "manifest runtimeId mismatch"); -assert.equal(manifest.runtimeVersion, expectedVersion, "manifest version mismatch"); -assert.ok(manifest.nodeRelativePath, "manifest missing nodeRelativePath"); -assert.ok(manifest.cliEntrypointRelativePath, "manifest missing cliEntrypointRelativePath"); -assert.ok(manifest.serverRunnerRelativePath, "manifest missing serverRunnerRelativePath"); - -const nodeBinary = path.join(runtimeRoot, manifest.nodeRelativePath); -await fs.access(nodeBinary).catch(() => { - throw new Error(`Bundled Node binary not found: ${nodeBinary}`); -}); - -const cliEntry = path.join(runtimeRoot, manifest.cliEntrypointRelativePath); -await fs.access(cliEntry).catch(() => { - throw new Error(`CLI entrypoint not found: ${cliEntry}`); -}); - -const serverRunner = path.join(runtimeRoot, manifest.serverRunnerRelativePath); -await fs.access(serverRunner).catch(() => { - throw new Error(`Server runner not found: ${serverRunner}`); -}); - -const runtimePackageJson = JSON.parse( - await fs.readFile(path.join(runtimeRoot, "package.json"), "utf8") -); -assert.equal(runtimePackageJson.version, expectedVersion, "runtime package.json version mismatch"); - -for (const pkg of ["@getpaseo/relay", "@getpaseo/server", "@getpaseo/cli"]) { - const pkgDir = path.join(runtimeRoot, "node_modules", ...pkg.split("/")); - await fs.access(pkgDir).catch(() => { - throw new Error(`Missing bundled dependency: ${pkg} (expected at ${pkgDir})`); - }); -} - -const sherpaPlatformMap = { - darwin: "darwin", - linux: "linux", - win32: "win", -}; -const sherpaPlatform = sherpaPlatformMap[manifest.platform]; -assert.ok( - sherpaPlatform, - `Unsupported sherpa platform mapping for managed runtime validation: ${manifest.platform}` -); - -const sherpaNativePackage = `sherpa-onnx-${sherpaPlatform}-${manifest.arch}`; -const sherpaNativePackageDir = path.join(runtimeRoot, "node_modules", sherpaNativePackage); -await fs.access(sherpaNativePackageDir).catch(() => { - throw new Error( - `Missing bundled native speech dependency: ${sherpaNativePackage} (expected at ${sherpaNativePackageDir})` - ); -}); - -console.log(`[validate-managed-runtime] PASS`); -console.log(` runtimeId: ${manifest.runtimeId}`); -console.log(` version: ${expectedVersion}`); -console.log(` platform: ${manifest.platform}`); -console.log(` arch: ${manifest.arch}`); -console.log(` sherpaNativePackage: ${sherpaNativePackage}`); diff --git a/packages/desktop/scripts/verify-electron-cdp.mjs b/packages/desktop/scripts/verify-electron-cdp.mjs new file mode 100644 index 000000000..2d29db6bf --- /dev/null +++ b/packages/desktop/scripts/verify-electron-cdp.mjs @@ -0,0 +1,332 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import process from "node:process"; +import { chromium } from "playwright"; + +const CDP_URL = process.env.CDP_URL ?? "http://127.0.0.1:9223"; +const OUTPUT_DIR = process.env.ELECTRON_VERIFY_OUTPUT_DIR ?? "/tmp/electron-verification"; +const APP_URL_FRAGMENT = process.env.ELECTRON_VERIFY_APP_URL_FRAGMENT ?? "localhost:8081"; +const REQUIRED_DESKTOP_KEYS = [ + "invoke", + "events", + "window", + "dialog", + "notification", + "opener", +]; + +function assert(condition, message) { + if (!condition) { + throw new Error(message); + } +} + +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +async function ensureDir(dirPath) { + await fs.mkdir(dirPath, { recursive: true }); +} + +async function captureScreenshot(page, fileName) { + const filePath = path.join(OUTPUT_DIR, fileName); + await page.screenshot({ path: filePath, fullPage: true }); + return filePath; +} + +async function findAppPage(browser) { + for (let attempt = 0; attempt < 30; attempt += 1) { + for (const context of browser.contexts()) { + for (const page of context.pages()) { + if (page.url().includes(APP_URL_FRAGMENT) && !page.url().startsWith("devtools://")) { + return page; + } + } + } + await new Promise((resolve) => setTimeout(resolve, 500)); + } + throw new Error(`Unable to find Electron app page for ${APP_URL_FRAGMENT}`); +} + +async function main() { + await ensureDir(OUTPUT_DIR); + + const browser = await chromium.connectOverCDP(CDP_URL); + const page = await findAppPage(browser); + const consoleMessages = []; + const results = []; + + page.on("console", (message) => { + consoleMessages.push({ + type: message.type(), + text: message.text(), + }); + }); + page.on("pageerror", (error) => { + consoleMessages.push({ + type: "pageerror", + text: String(error), + }); + }); + + await page.waitForLoadState("domcontentloaded"); + await page.waitForTimeout(1000); + if (!page.url().endsWith("/welcome")) { + await page.goto(`http://${APP_URL_FRAGMENT}/welcome`, { waitUntil: "domcontentloaded" }); + await page.waitForTimeout(1000); + } + + const welcomeScreenshot = await captureScreenshot(page, "01-welcome.png"); + + const desktopDetection = await page.evaluate(() => { + const bridge = window.paseoDesktop; + const keys = bridge && typeof bridge === "object" ? Object.keys(bridge) : []; + const keyTypes = bridge && typeof bridge === "object" + ? Object.fromEntries( + Object.entries(bridge).map(([key, value]) => [key, typeof value]) + ) + : {}; + return { + exists: Boolean(bridge && typeof bridge === "object"), + keys, + keyTypes, + platform: bridge?.platform ?? null, + }; + }); + + const hasExpectedDesktopShape = + desktopDetection.exists && + REQUIRED_DESKTOP_KEYS.every((key) => desktopDetection.keys.includes(key)); + + results.push({ + check: "desktop-detection", + pass: hasExpectedDesktopShape, + details: desktopDetection, + screenshot: welcomeScreenshot, + }); + + const desktopStatus = await page.evaluate(() => window.paseoDesktop.invoke("desktop_daemon_status")); + assert( + typeof desktopStatus?.serverId === "string" && desktopStatus.serverId.trim().length > 0, + "desktop_daemon_status did not return a serverId" + ); + + const serverId = desktopStatus.serverId.trim(); + await page.evaluate((nextServerId) => { + window.location.href = `/h/${nextServerId}/settings`; + }, serverId); + await page.waitForURL(new RegExp(`/h/${escapeRegExp(serverId)}/settings$`), { + timeout: 30_000, + }); + await page.getByText("Daemon management", { exact: true }).waitFor({ + timeout: 30_000, + }); + + const settingsScreenshot = await captureScreenshot(page, "02-settings-page.png"); + + const sidebarSettingsButton = page.locator('[data-testid="sidebar-settings"]').first(); + const menuToggle = page.locator('[data-testid="menu-button"]').first(); + if ( + (await sidebarSettingsButton.isVisible().catch(() => false)) && + (await menuToggle.isVisible().catch(() => false)) + ) { + await menuToggle.click(); + await sidebarSettingsButton.waitFor({ state: "hidden", timeout: 10_000 }).catch(() => undefined); + await page.waitForTimeout(500); + } + + const dragRegionCheck = await page.evaluate(() => { + const nodes = Array.from(document.querySelectorAll("*")); + const annotationId = "electron-verify-drag-style"; + const existingAnnotation = document.getElementById(annotationId); + existingAnnotation?.remove(); + + const annotationStyle = document.createElement("style"); + annotationStyle.id = annotationId; + annotationStyle.textContent = ` + [data-electron-verify-drag="true"] { + outline: 3px solid #ff4d4f !important; + outline-offset: -3px !important; + } + `; + document.head.appendChild(annotationStyle); + + function isVisible(element) { + const rect = element.getBoundingClientRect(); + const style = window.getComputedStyle(element); + return ( + rect.width > 0 && + rect.height > 0 && + style.display !== "none" && + style.visibility !== "hidden" && + style.opacity !== "0" + ); + } + + function summarizeText(element) { + return (element.textContent ?? "") + .replace(/\s+/g, " ") + .trim() + .slice(0, 120); + } + + const regions = []; + let candidate = null; + + for (const element of nodes) { + if (!(element instanceof HTMLElement)) { + continue; + } + const style = window.getComputedStyle(element); + const appRegion = + style.webkitAppRegion || style.getPropertyValue("-webkit-app-region"); + if (appRegion !== "drag" || !isVisible(element)) { + continue; + } + + const rect = element.getBoundingClientRect(); + const text = summarizeText(element); + const info = { + tagName: element.tagName.toLowerCase(), + text, + top: rect.top, + left: rect.left, + width: rect.width, + height: rect.height, + paddingLeft: Number.parseFloat(style.paddingLeft || "0"), + paddingTop: Number.parseFloat(style.paddingTop || "0"), + }; + regions.push(info); + + const looksLikeHeader = + rect.top < 180 && + rect.height >= 40 && + (text.includes("Settings") || text.includes("Sessions")); + if (!candidate && looksLikeHeader) { + candidate = { ...info }; + element.setAttribute("data-electron-verify-drag", "true"); + } + } + + if (!candidate && regions.length > 0) { + candidate = regions + .slice() + .sort((left, right) => left.top - right.top)[0]; + + for (const element of nodes) { + if (!(element instanceof HTMLElement)) { + continue; + } + const style = window.getComputedStyle(element); + const appRegion = + style.webkitAppRegion || style.getPropertyValue("-webkit-app-region"); + if (appRegion !== "drag" || !isVisible(element)) { + continue; + } + const rect = element.getBoundingClientRect(); + if ( + Math.abs(rect.top - candidate.top) < 1 && + Math.abs(rect.left - candidate.left) < 1 && + Math.abs(rect.width - candidate.width) < 1 && + Math.abs(rect.height - candidate.height) < 1 + ) { + element.setAttribute("data-electron-verify-drag", "true"); + break; + } + } + } + + return { + count: regions.length, + candidate, + regions: regions.slice(0, 10), + }; + }); + + const dragScreenshot = await captureScreenshot(page, "03-drag-region.png"); + const dragRegionPassed = + dragRegionCheck.count > 0 && + Boolean(dragRegionCheck.candidate) && + dragRegionCheck.candidate.top < 180; + + results.push({ + check: "drag-regions", + pass: dragRegionPassed, + details: dragRegionCheck, + screenshot: dragScreenshot, + }); + + const trafficLightScreenshot = await captureScreenshot(page, "04-traffic-light-padding.png"); + const isMac = process.platform === "darwin"; + const observedPaddingLeft = dragRegionCheck.candidate?.paddingLeft ?? null; + const trafficLightPaddingPassed = !isMac + ? true + : typeof observedPaddingLeft === "number" && + observedPaddingLeft >= 78 && + observedPaddingLeft <= 110; + + results.push({ + check: "traffic-light-padding", + pass: trafficLightPaddingPassed, + details: { + platform: process.platform, + observedPaddingLeft, + expectedApproximatePaddingLeft: 78, + candidate: dragRegionCheck.candidate, + }, + screenshot: trafficLightScreenshot, + }); + + const daemonManagementVisible = await Promise.all([ + page.getByText("Built-in daemon", { exact: true }).isVisible(), + page.getByText("Daemon management", { exact: true }).isVisible(), + page.getByRole("button", { name: "Restart daemon" }).first().isVisible(), + ]).then((values) => values.every(Boolean)); + const daemonManagementScreenshot = await captureScreenshot( + page, + "05-settings-daemon-management.png" + ); + + results.push({ + check: "settings-daemon-management", + pass: daemonManagementVisible, + details: { + route: page.url(), + serverId, + desktopStatus, + }, + screenshot: daemonManagementScreenshot, + }); + + const desktopDetectionScreenshot = await captureScreenshot( + page, + "06-desktop-detection.png" + ); + results[0].screenshot = desktopDetectionScreenshot; + + const report = { + cdpUrl: CDP_URL, + outputDir: OUTPUT_DIR, + pageUrl: page.url(), + desktopStatus, + results, + consoleMessages, + }; + + const reportPath = path.join(OUTPUT_DIR, "report.json"); + await fs.writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`, "utf8"); + + const failedChecks = results.filter((result) => !result.pass); + console.log(JSON.stringify(report, null, 2)); + await browser.close(); + + if (failedChecks.length > 0) { + process.exitCode = 1; + } +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/packages/desktop/src-tauri/.gitignore b/packages/desktop/src-tauri/.gitignore deleted file mode 100644 index 502406b4e..000000000 --- a/packages/desktop/src-tauri/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Generated by Cargo -# will have compiled files and executables -/target/ -/gen/schemas diff --git a/packages/desktop/src-tauri/Cargo.lock b/packages/desktop/src-tauri/Cargo.lock deleted file mode 100644 index 811a88973..000000000 --- a/packages/desktop/src-tauri/Cargo.lock +++ /dev/null @@ -1,6092 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "ahash" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" -dependencies = [ - "getrandom 0.2.17", - "once_cell", - "version_check", -] - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - -[[package]] -name = "android_log-sys" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" - -[[package]] -name = "android_logger" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3" -dependencies = [ - "android_log-sys", - "env_filter", - "log", -] - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anyhow" -version = "1.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" - -[[package]] -name = "arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" -dependencies = [ - "derive_arbitrary", -] - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "async-broadcast" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" -dependencies = [ - "event-listener", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-executor" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "pin-project-lite", - "slab", -] - -[[package]] -name = "async-io" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" -dependencies = [ - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite", - "parking", - "polling", - "rustix", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-lock" -version = "3.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" -dependencies = [ - "event-listener", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-process" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" -dependencies = [ - "async-channel", - "async-io", - "async-lock", - "async-signal", - "async-task", - "blocking", - "cfg-if", - "event-listener", - "futures-lite", - "rustix", -] - -[[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "async-signal" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" -dependencies = [ - "async-io", - "async-lock", - "atomic-waker", - "cfg-if", - "futures-core", - "futures-io", - "rustix", - "signal-hook-registry", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "atk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" -dependencies = [ - "atk-sys", - "glib", - "libc", -] - -[[package]] -name = "atk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" -dependencies = [ - "serde_core", -] - -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "block2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" -dependencies = [ - "objc2", -] - -[[package]] -name = "blocking" -version = "1.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" -dependencies = [ - "async-channel", - "async-task", - "futures-io", - "futures-lite", - "piper", -] - -[[package]] -name = "borsh" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" -dependencies = [ - "borsh-derive", - "cfg_aliases", -] - -[[package]] -name = "borsh-derive" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" -dependencies = [ - "once_cell", - "proc-macro-crate 3.4.0", - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "brotli" -version = "8.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - -[[package]] -name = "bumpalo" -version = "3.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" - -[[package]] -name = "byte-unit" -version = "5.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c6d47a4e2961fb8721bcfc54feae6455f2f64e7054f9bc67e875f0e77f4c58d" -dependencies = [ - "rust_decimal", - "schemars 1.2.0", - "serde", - "utf8-width", -] - -[[package]] -name = "bytecheck" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" -dependencies = [ - "bytecheck_derive", - "ptr_meta", - "simdutf8", -] - -[[package]] -name = "bytecheck_derive" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "bytemuck" -version = "1.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" -dependencies = [ - "serde", -] - -[[package]] -name = "cairo-rs" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" -dependencies = [ - "bitflags 2.10.0", - "cairo-sys-rs", - "glib", - "libc", - "once_cell", - "thiserror 1.0.69", -] - -[[package]] -name = "cairo-sys-rs" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - -[[package]] -name = "camino" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" -dependencies = [ - "serde_core", -] - -[[package]] -name = "cargo-platform" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" -dependencies = [ - "serde", -] - -[[package]] -name = "cargo_metadata" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" -dependencies = [ - "camino", - "cargo-platform", - "semver", - "serde", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "cargo_toml" -version = "0.22.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" -dependencies = [ - "serde", - "toml 0.9.11+spec-1.1.0", -] - -[[package]] -name = "cc" -version = "1.2.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cfb" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" -dependencies = [ - "byteorder", - "fnv", - "uuid", -] - -[[package]] -name = "cfg-expr" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" -dependencies = [ - "smallvec", - "target-lexicon", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "chrono" -version = "0.4.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" -dependencies = [ - "iana-time-zone", - "num-traits", - "serde", - "windows-link 0.2.1", -] - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - -[[package]] -name = "cookie" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" -dependencies = [ - "time", - "version_check", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "core-graphics" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" -dependencies = [ - "bitflags 2.10.0", - "core-foundation", - "core-graphics-types", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" -dependencies = [ - "bitflags 2.10.0", - "core-foundation", - "libc", -] - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "cssparser" -version = "0.29.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" -dependencies = [ - "cssparser-macros", - "dtoa-short", - "itoa", - "matches", - "phf 0.10.1", - "proc-macro2", - "quote", - "smallvec", - "syn 1.0.109", -] - -[[package]] -name = "cssparser-macros" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" -dependencies = [ - "quote", - "syn 2.0.114", -] - -[[package]] -name = "ctor" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" -dependencies = [ - "quote", - "syn 2.0.114", -] - -[[package]] -name = "darling" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.114", -] - -[[package]] -name = "darling_macro" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "data-encoding" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" - -[[package]] -name = "deranged" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" -dependencies = [ - "powerfmt", - "serde_core", -] - -[[package]] -name = "derive_arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "derive_more" -version = "0.99.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.114", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "dirs" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.61.2", -] - -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - -[[package]] -name = "dispatch2" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" -dependencies = [ - "bitflags 2.10.0", - "block2", - "libc", - "objc2", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "dlopen2" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" -dependencies = [ - "dlopen2_derive", - "libc", - "once_cell", - "winapi", -] - -[[package]] -name = "dlopen2_derive" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "dpi" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" -dependencies = [ - "serde", -] - -[[package]] -name = "dtoa" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" - -[[package]] -name = "dtoa-short" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" -dependencies = [ - "dtoa", -] - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - -[[package]] -name = "embed-resource" -version = "3.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55a075fc573c64510038d7ee9abc7990635863992f83ebc52c8b433b8411a02e" -dependencies = [ - "cc", - "memchr", - "rustc_version", - "toml 0.9.11+spec-1.1.0", - "vswhom", - "winreg", -] - -[[package]] -name = "embed_plist" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" - -[[package]] -name = "endi" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" - -[[package]] -name = "enumflags2" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" -dependencies = [ - "enumflags2_derive", - "serde", -] - -[[package]] -name = "enumflags2_derive" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "env_filter" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "erased-serde" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" -dependencies = [ - "serde", - "serde_core", - "typeid", -] - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener", - "pin-project-lite", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "fdeflate" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "fern" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29" -dependencies = [ - "log", -] - -[[package]] -name = "field-offset" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" -dependencies = [ - "memoffset", - "rustc_version", -] - -[[package]] -name = "filetime" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" -dependencies = [ - "cfg-if", - "libc", - "libredox", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" - -[[package]] -name = "flate2" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foreign-types" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -dependencies = [ - "foreign-types-macros", - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - -[[package]] -name = "futf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" -dependencies = [ - "mac", - "new_debug_unreachable", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - -[[package]] -name = "gdk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" -dependencies = [ - "cairo-rs", - "gdk-pixbuf", - "gdk-sys", - "gio", - "glib", - "libc", - "pango", -] - -[[package]] -name = "gdk-pixbuf" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" -dependencies = [ - "gdk-pixbuf-sys", - "gio", - "glib", - "libc", - "once_cell", -] - -[[package]] -name = "gdk-pixbuf-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gdk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" -dependencies = [ - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "pkg-config", - "system-deps", -] - -[[package]] -name = "gdkwayland-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" -dependencies = [ - "gdk-sys", - "glib-sys", - "gobject-sys", - "libc", - "pkg-config", - "system-deps", -] - -[[package]] -name = "gdkx11" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" -dependencies = [ - "gdk", - "gdkx11-sys", - "gio", - "glib", - "libc", - "x11", -] - -[[package]] -name = "gdkx11-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" -dependencies = [ - "gdk-sys", - "glib-sys", - "libc", - "system-deps", - "x11", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "gio" -version = "0.18.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "gio-sys", - "glib", - "libc", - "once_cell", - "pin-project-lite", - "smallvec", - "thiserror 1.0.69", -] - -[[package]] -name = "gio-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", - "winapi", -] - -[[package]] -name = "glib" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" -dependencies = [ - "bitflags 2.10.0", - "futures-channel", - "futures-core", - "futures-executor", - "futures-task", - "futures-util", - "gio-sys", - "glib-macros", - "glib-sys", - "gobject-sys", - "libc", - "memchr", - "once_cell", - "smallvec", - "thiserror 1.0.69", -] - -[[package]] -name = "glib-macros" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" -dependencies = [ - "heck 0.4.1", - "proc-macro-crate 2.0.2", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "glib-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" -dependencies = [ - "libc", - "system-deps", -] - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "gobject-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gtk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" -dependencies = [ - "atk", - "cairo-rs", - "field-offset", - "futures-channel", - "gdk", - "gdk-pixbuf", - "gio", - "glib", - "gtk-sys", - "gtk3-macros", - "libc", - "pango", - "pkg-config", -] - -[[package]] -name = "gtk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" -dependencies = [ - "atk-sys", - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "system-deps", -] - -[[package]] -name = "gtk3-macros" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" -dependencies = [ - "proc-macro-crate 1.3.1", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "html5ever" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" -dependencies = [ - "log", - "mac", - "markup5ever", - "match_token", -] - -[[package]] -name = "http" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "hyper" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "http", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "pin-utils", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots 1.0.5", -] - -[[package]] -name = "hyper-util" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core 0.62.2", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "ico" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" -dependencies = [ - "byteorder", - "png", -] - -[[package]] -name = "icu_collections" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" - -[[package]] -name = "icu_properties" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - -[[package]] -name = "indexmap" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", -] - -[[package]] -name = "infer" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" -dependencies = [ - "cfb", -] - -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "is-docker" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" -dependencies = [ - "once_cell", -] - -[[package]] -name = "is-wsl" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" -dependencies = [ - "is-docker", - "once_cell", -] - -[[package]] -name = "itoa" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" - -[[package]] -name = "javascriptcore-rs" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" -dependencies = [ - "bitflags 1.3.2", - "glib", - "javascriptcore-rs-sys", -] - -[[package]] -name = "javascriptcore-rs-sys" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - -[[package]] -name = "js-sys" -version = "0.3.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "json-patch" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" -dependencies = [ - "jsonptr", - "serde", - "serde_json", - "thiserror 1.0.69", -] - -[[package]] -name = "jsonptr" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "keyboard-types" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" -dependencies = [ - "bitflags 2.10.0", - "serde", - "unicode-segmentation", -] - -[[package]] -name = "kuchikiki" -version = "0.8.8-speedreader" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" -dependencies = [ - "cssparser", - "html5ever", - "indexmap 2.13.0", - "selectors", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libappindicator" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" -dependencies = [ - "glib", - "gtk", - "gtk-sys", - "libappindicator-sys", - "log", -] - -[[package]] -name = "libappindicator-sys" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" -dependencies = [ - "gtk-sys", - "libloading", - "once_cell", -] - -[[package]] -name = "libc" -version = "0.2.180" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" - -[[package]] -name = "libloading" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" -dependencies = [ - "cfg-if", - "winapi", -] - -[[package]] -name = "libredox" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" -dependencies = [ - "bitflags 2.10.0", - "libc", - "redox_syscall 0.7.1", -] - -[[package]] -name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" - -[[package]] -name = "litemap" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" -dependencies = [ - "value-bag", -] - -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - -[[package]] -name = "mac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" - -[[package]] -name = "mac-notification-sys" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fd3f75411f4725061682ed91f131946e912859d0044d39c4ec0aac818d7621" -dependencies = [ - "cc", - "objc2", - "objc2-foundation", - "time", -] - -[[package]] -name = "markup5ever" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" -dependencies = [ - "log", - "phf 0.11.3", - "phf_codegen 0.11.3", - "string_cache", - "string_cache_codegen", - "tendril", -] - -[[package]] -name = "match_token" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "matches" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" - -[[package]] -name = "memchr" -version = "2.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "minisign-verify" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e856fdd13623a2f5f2f54676a4ee49502a96a80ef4a62bcedd23d52427c44d43" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "mio" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" -dependencies = [ - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.61.2", -] - -[[package]] -name = "muda" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" -dependencies = [ - "crossbeam-channel", - "dpi", - "gtk", - "keyboard-types", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "once_cell", - "png", - "serde", - "thiserror 2.0.18", - "windows-sys 0.60.2", -] - -[[package]] -name = "ndk" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" -dependencies = [ - "bitflags 2.10.0", - "jni-sys", - "log", - "ndk-sys", - "num_enum", - "raw-window-handle", - "thiserror 1.0.69", -] - -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - -[[package]] -name = "ndk-sys" -version = "0.6.0+11769913" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" -dependencies = [ - "jni-sys", -] - -[[package]] -name = "new_debug_unreachable" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" - -[[package]] -name = "nodrop" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" - -[[package]] -name = "notify-rust" -version = "4.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21af20a1b50be5ac5861f74af1a863da53a11c38684d9818d82f1c42f7fdc6c2" -dependencies = [ - "futures-lite", - "log", - "mac-notification-sys", - "serde", - "tauri-winrt-notification", - "zbus", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_enum" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" -dependencies = [ - "proc-macro-crate 3.4.0", - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "num_threads" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" -dependencies = [ - "libc", -] - -[[package]] -name = "objc2" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" -dependencies = [ - "objc2-encode", - "objc2-exception-helper", -] - -[[package]] -name = "objc2-app-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" -dependencies = [ - "bitflags 2.10.0", - "block2", - "libc", - "objc2", - "objc2-cloud-kit", - "objc2-core-data", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-core-image", - "objc2-core-text", - "objc2-core-video", - "objc2-foundation", - "objc2-quartz-core", -] - -[[package]] -name = "objc2-cloud-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" -dependencies = [ - "bitflags 2.10.0", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-data" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" -dependencies = [ - "bitflags 2.10.0", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags 2.10.0", - "dispatch2", - "objc2", -] - -[[package]] -name = "objc2-core-graphics" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" -dependencies = [ - "bitflags 2.10.0", - "dispatch2", - "objc2", - "objc2-core-foundation", - "objc2-io-surface", -] - -[[package]] -name = "objc2-core-image" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-text" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" -dependencies = [ - "bitflags 2.10.0", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", -] - -[[package]] -name = "objc2-core-video" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" -dependencies = [ - "bitflags 2.10.0", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-io-surface", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "objc2-exception-helper" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" -dependencies = [ - "cc", -] - -[[package]] -name = "objc2-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "bitflags 2.10.0", - "block2", - "libc", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-io-surface" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" -dependencies = [ - "bitflags 2.10.0", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-javascript-core" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586" -dependencies = [ - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-osa-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" -dependencies = [ - "bitflags 2.10.0", - "objc2", - "objc2-app-kit", - "objc2-foundation", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" -dependencies = [ - "bitflags 2.10.0", - "objc2", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "objc2-security" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" -dependencies = [ - "bitflags 2.10.0", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-ui-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" -dependencies = [ - "bitflags 2.10.0", - "objc2", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "objc2-web-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" -dependencies = [ - "bitflags 2.10.0", - "block2", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "objc2-javascript-core", - "objc2-security", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "open" -version = "5.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" -dependencies = [ - "dunce", - "is-wsl", - "libc", - "pathdiff", -] - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "ordered-stream" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" -dependencies = [ - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "osakit" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" -dependencies = [ - "objc2", - "objc2-foundation", - "objc2-osa-kit", - "serde", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "pango" -version = "0.18.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" -dependencies = [ - "gio", - "glib", - "libc", - "once_cell", - "pango-sys", -] - -[[package]] -name = "pango-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.5.18", - "smallvec", - "windows-link 0.2.1", -] - -[[package]] -name = "paseo" -version = "0.1.30" -dependencies = [ - "base64 0.22.1", - "cc", - "dirs", - "dunce", - "futures-util", - "http", - "log", - "serde", - "serde_json", - "tauri", - "tauri-build", - "tauri-plugin-dialog", - "tauri-plugin-log", - "tauri-plugin-notification", - "tauri-plugin-opener", - "tauri-plugin-single-instance", - "tauri-plugin-updater", - "tauri-plugin-websocket", - "tokio", - "tokio-tungstenite 0.24.0", -] - -[[package]] -name = "pathdiff" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "phf" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" -dependencies = [ - "phf_shared 0.8.0", -] - -[[package]] -name = "phf" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" -dependencies = [ - "phf_macros 0.10.0", - "phf_shared 0.10.0", - "proc-macro-hack", -] - -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_macros 0.11.3", - "phf_shared 0.11.3", -] - -[[package]] -name = "phf_codegen" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" -dependencies = [ - "phf_generator 0.8.0", - "phf_shared 0.8.0", -] - -[[package]] -name = "phf_codegen" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", -] - -[[package]] -name = "phf_generator" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" -dependencies = [ - "phf_shared 0.8.0", - "rand 0.7.3", -] - -[[package]] -name = "phf_generator" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" -dependencies = [ - "phf_shared 0.10.0", - "rand 0.8.5", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared 0.11.3", - "rand 0.8.5", -] - -[[package]] -name = "phf_macros" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" -dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", - "proc-macro-hack", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "phf_shared" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" -dependencies = [ - "siphasher 0.3.11", -] - -[[package]] -name = "phf_shared" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" -dependencies = [ - "siphasher 0.3.11", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher 1.0.1", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "piper" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" -dependencies = [ - "atomic-waker", - "fastrand", - "futures-io", -] - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "plist" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" -dependencies = [ - "base64 0.22.1", - "indexmap 2.13.0", - "quick-xml 0.38.4", - "serde", - "time", -] - -[[package]] -name = "png" -version = "0.17.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" -dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "polling" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi", - "pin-project-lite", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "precomputed-hash" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" - -[[package]] -name = "proc-macro-crate" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" -dependencies = [ - "once_cell", - "toml_edit 0.19.15", -] - -[[package]] -name = "proc-macro-crate" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" -dependencies = [ - "toml_datetime 0.6.3", - "toml_edit 0.20.2", -] - -[[package]] -name = "proc-macro-crate" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" -dependencies = [ - "toml_edit 0.23.10+spec-1.0.0", -] - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - -[[package]] -name = "proc-macro-hack" -version = "0.5.20+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" - -[[package]] -name = "proc-macro2" -version = "1.0.105" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "ptr_meta" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" -dependencies = [ - "ptr_meta_derive", -] - -[[package]] -name = "ptr_meta_derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "quick-xml" -version = "0.37.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" -dependencies = [ - "memchr", -] - -[[package]] -name = "quick-xml" -version = "0.38.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" -dependencies = [ - "memchr", -] - -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror 2.0.18", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" -dependencies = [ - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand 0.9.2", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.18", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.60.2", -] - -[[package]] -name = "quote" -version = "1.0.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", - "rand_pcg", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "rand_pcg" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "raw-window-handle" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.10.0", -] - -[[package]] -name = "redox_syscall" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" -dependencies = [ - "bitflags 2.10.0", -] - -[[package]] -name = "redox_users" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" -dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror 2.0.18", -] - -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "regex" -version = "1.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" - -[[package]] -name = "rend" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" -dependencies = [ - "bytecheck", -] - -[[package]] -name = "reqwest" -version = "0.12.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-core", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tokio-util", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", - "webpki-roots 1.0.5", -] - -[[package]] -name = "rfd" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" -dependencies = [ - "block2", - "dispatch2", - "glib-sys", - "gobject-sys", - "gtk-sys", - "js-sys", - "log", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "raw-window-handle", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows-sys 0.60.2", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.17", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rkyv" -version = "0.7.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" -dependencies = [ - "bitvec", - "bytecheck", - "bytes", - "hashbrown 0.12.3", - "ptr_meta", - "rend", - "rkyv_derive", - "seahash", - "tinyvec", - "uuid", -] - -[[package]] -name = "rkyv_derive" -version = "0.7.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "rust_decimal" -version = "1.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61f703d19852dbf87cbc513643fa81428361eb6940f1ac14fd58155d295a3eb0" -dependencies = [ - "arrayvec", - "borsh", - "bytes", - "num-traits", - "rand 0.8.5", - "rkyv", - "serde", - "serde_json", -] - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" -dependencies = [ - "bitflags 2.10.0", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls" -version = "0.23.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" -dependencies = [ - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pki-types" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "ryu" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "schemars" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" -dependencies = [ - "dyn-clone", - "indexmap 1.9.3", - "schemars_derive", - "serde", - "serde_json", - "url", - "uuid", -] - -[[package]] -name = "schemars" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars_derive" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 2.0.114", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "seahash" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" - -[[package]] -name = "selectors" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" -dependencies = [ - "bitflags 1.3.2", - "cssparser", - "derive_more", - "fxhash", - "log", - "phf 0.8.0", - "phf_codegen 0.8.0", - "precomputed-hash", - "servo_arc", - "smallvec", -] - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" -dependencies = [ - "serde", - "serde_core", -] - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde-untagged" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" -dependencies = [ - "erased-serde", - "serde", - "serde_core", - "typeid", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "serde_derive_internals" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_repr" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_spanned" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" -dependencies = [ - "serde_core", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_with" -version = "3.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" -dependencies = [ - "base64 0.22.1", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.13.0", - "schemars 0.9.0", - "schemars 1.2.0", - "serde_core", - "serde_json", - "serde_with_macros", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "3.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "serialize-to-javascript" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" -dependencies = [ - "serde", - "serde_json", - "serialize-to-javascript-impl", -] - -[[package]] -name = "serialize-to-javascript-impl" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "servo_arc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" -dependencies = [ - "nodrop", - "stable_deref_trait", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[package]] -name = "simd-adler32" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" - -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - -[[package]] -name = "siphasher" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" - -[[package]] -name = "siphasher" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "socket2" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] - -[[package]] -name = "softbuffer" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" -dependencies = [ - "bytemuck", - "js-sys", - "ndk", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation", - "objc2-quartz-core", - "raw-window-handle", - "redox_syscall 0.5.18", - "tracing", - "wasm-bindgen", - "web-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "soup3" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" -dependencies = [ - "futures-channel", - "gio", - "glib", - "libc", - "soup3-sys", -] - -[[package]] -name = "soup3-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "string_cache" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" -dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared 0.11.3", - "precomputed-hash", - "serde", -] - -[[package]] -name = "string_cache_codegen" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "swift-rs" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" -dependencies = [ - "base64 0.21.7", - "serde", - "serde_json", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "system-deps" -version = "6.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" -dependencies = [ - "cfg-expr", - "heck 0.5.0", - "pkg-config", - "toml 0.8.2", - "version-compare", -] - -[[package]] -name = "tao" -version = "0.34.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" -dependencies = [ - "bitflags 2.10.0", - "block2", - "core-foundation", - "core-graphics", - "crossbeam-channel", - "dispatch", - "dlopen2", - "dpi", - "gdkwayland-sys", - "gdkx11-sys", - "gtk", - "jni", - "lazy_static", - "libc", - "log", - "ndk", - "ndk-context", - "ndk-sys", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "once_cell", - "parking_lot", - "raw-window-handle", - "scopeguard", - "tao-macros", - "unicode-segmentation", - "url", - "windows", - "windows-core 0.61.2", - "windows-version", - "x11-dl", -] - -[[package]] -name = "tao-macros" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - -[[package]] -name = "tar" -version = "0.4.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" -dependencies = [ - "filetime", - "libc", - "xattr", -] - -[[package]] -name = "target-lexicon" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - -[[package]] -name = "tauri" -version = "2.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a3868da5508446a7cd08956d523ac3edf0a8bc20bf7e4038f9a95c2800d2033" -dependencies = [ - "anyhow", - "bytes", - "cookie", - "dirs", - "dunce", - "embed_plist", - "getrandom 0.3.4", - "glob", - "gtk", - "heck 0.5.0", - "http", - "jni", - "libc", - "log", - "mime", - "muda", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "objc2-ui-kit", - "objc2-web-kit", - "percent-encoding", - "plist", - "raw-window-handle", - "reqwest", - "serde", - "serde_json", - "serde_repr", - "serialize-to-javascript", - "swift-rs", - "tauri-build", - "tauri-macros", - "tauri-runtime", - "tauri-runtime-wry", - "tauri-utils", - "thiserror 2.0.18", - "tokio", - "tray-icon", - "url", - "webkit2gtk", - "webview2-com", - "window-vibrancy", - "windows", -] - -[[package]] -name = "tauri-build" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17fcb8819fd16463512a12f531d44826ce566f486d7ccd211c9c8cebdaec4e08" -dependencies = [ - "anyhow", - "cargo_toml", - "dirs", - "glob", - "heck 0.5.0", - "json-patch", - "schemars 0.8.22", - "semver", - "serde", - "serde_json", - "tauri-utils", - "tauri-winres", - "toml 0.9.11+spec-1.1.0", - "walkdir", -] - -[[package]] -name = "tauri-codegen" -version = "2.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa9844cefcf99554a16e0a278156ae73b0d8680bbc0e2ad1e4287aadd8489cf" -dependencies = [ - "base64 0.22.1", - "brotli", - "ico", - "json-patch", - "plist", - "png", - "proc-macro2", - "quote", - "semver", - "serde", - "serde_json", - "sha2", - "syn 2.0.114", - "tauri-utils", - "thiserror 2.0.18", - "time", - "url", - "uuid", - "walkdir", -] - -[[package]] -name = "tauri-macros" -version = "2.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3764a12f886d8245e66b7ee9b43ccc47883399be2019a61d80cf0f4117446fde" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.114", - "tauri-codegen", - "tauri-utils", -] - -[[package]] -name = "tauri-plugin" -version = "2.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1d0a4860b7ff570c891e1d2a586bf1ede205ff858fbc305e0b5ae5d14c1377" -dependencies = [ - "anyhow", - "glob", - "plist", - "schemars 0.8.22", - "serde", - "serde_json", - "tauri-utils", - "toml 0.9.11+spec-1.1.0", - "walkdir", -] - -[[package]] -name = "tauri-plugin-dialog" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9204b425d9be8d12aa60c2a83a289cf7d1caae40f57f336ed1155b3a5c0e359b" -dependencies = [ - "log", - "raw-window-handle", - "rfd", - "serde", - "serde_json", - "tauri", - "tauri-plugin", - "tauri-plugin-fs", - "thiserror 2.0.18", - "url", -] - -[[package]] -name = "tauri-plugin-fs" -version = "2.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed390cc669f937afeb8b28032ce837bac8ea023d975a2e207375ec05afaf1804" -dependencies = [ - "anyhow", - "dunce", - "glob", - "percent-encoding", - "schemars 0.8.22", - "serde", - "serde_json", - "serde_repr", - "tauri", - "tauri-plugin", - "tauri-utils", - "thiserror 2.0.18", - "toml 0.9.11+spec-1.1.0", - "url", -] - -[[package]] -name = "tauri-plugin-log" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7545bd67f070a4500432c826e2e0682146a1d6712aee22a2786490156b574d93" -dependencies = [ - "android_logger", - "byte-unit", - "fern", - "log", - "objc2", - "objc2-foundation", - "serde", - "serde_json", - "serde_repr", - "swift-rs", - "tauri", - "tauri-plugin", - "thiserror 2.0.18", - "time", -] - -[[package]] -name = "tauri-plugin-notification" -version = "2.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01fc2c5ff41105bd1f7242d8201fdf3efd70749b82fa013a17f2126357d194cc" -dependencies = [ - "log", - "notify-rust", - "rand 0.9.2", - "serde", - "serde_json", - "serde_repr", - "tauri", - "tauri-plugin", - "thiserror 2.0.18", - "time", - "url", -] - -[[package]] -name = "tauri-plugin-opener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc624469b06f59f5a29f874bbc61a2ed737c0f9c23ef09855a292c389c42e83f" -dependencies = [ - "dunce", - "glob", - "objc2-app-kit", - "objc2-foundation", - "open", - "schemars 0.8.22", - "serde", - "serde_json", - "tauri", - "tauri-plugin", - "thiserror 2.0.18", - "url", - "windows", - "zbus", -] - -[[package]] -name = "tauri-plugin-single-instance" -version = "2.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acba6b5ca527a96cdfcc96ae09b09ccb91ddff5e33978ca6873b96ea16bb404c" -dependencies = [ - "serde", - "serde_json", - "tauri", - "thiserror 2.0.18", - "tracing", - "windows-sys 0.60.2", - "zbus", -] - -[[package]] -name = "tauri-plugin-updater" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27cbc31740f4d507712550694749572ec0e43bdd66992db7599b89fbfd6b167b" -dependencies = [ - "base64 0.22.1", - "dirs", - "flate2", - "futures-util", - "http", - "infer", - "log", - "minisign-verify", - "osakit", - "percent-encoding", - "reqwest", - "semver", - "serde", - "serde_json", - "tar", - "tauri", - "tauri-plugin", - "tempfile", - "thiserror 2.0.18", - "time", - "tokio", - "url", - "windows-sys 0.60.2", - "zip", -] - -[[package]] -name = "tauri-plugin-websocket" -version = "2.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe037be7e1c30be639fe12dc3077e8ec4709e6f30ca4a6016b7edc76232ae9ee" -dependencies = [ - "futures-util", - "http", - "log", - "rand 0.9.2", - "rustls", - "serde", - "serde_json", - "tauri", - "tauri-plugin", - "thiserror 2.0.18", - "tokio", - "tokio-tungstenite 0.28.0", -] - -[[package]] -name = "tauri-runtime" -version = "2.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f766fe9f3d1efc4b59b17e7a891ad5ed195fa8d23582abb02e6c9a01137892" -dependencies = [ - "cookie", - "dpi", - "gtk", - "http", - "jni", - "objc2", - "objc2-ui-kit", - "objc2-web-kit", - "raw-window-handle", - "serde", - "serde_json", - "tauri-utils", - "thiserror 2.0.18", - "url", - "webkit2gtk", - "webview2-com", - "windows", -] - -[[package]] -name = "tauri-runtime-wry" -version = "2.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "187a3f26f681bdf028f796ccf57cf478c1ee422c50128e5a0a6ebeb3f5910065" -dependencies = [ - "gtk", - "http", - "jni", - "log", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "once_cell", - "percent-encoding", - "raw-window-handle", - "softbuffer", - "tao", - "tauri-runtime", - "tauri-utils", - "url", - "webkit2gtk", - "webview2-com", - "windows", - "wry", -] - -[[package]] -name = "tauri-utils" -version = "2.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a423c51176eb3616ee9b516a9fa67fed5f0e78baaba680e44eb5dd2cc37490" -dependencies = [ - "anyhow", - "brotli", - "cargo_metadata", - "ctor", - "dunce", - "glob", - "html5ever", - "http", - "infer", - "json-patch", - "kuchikiki", - "log", - "memchr", - "phf 0.11.3", - "proc-macro2", - "quote", - "regex", - "schemars 0.8.22", - "semver", - "serde", - "serde-untagged", - "serde_json", - "serde_with", - "swift-rs", - "thiserror 2.0.18", - "toml 0.9.11+spec-1.1.0", - "url", - "urlpattern", - "uuid", - "walkdir", -] - -[[package]] -name = "tauri-winres" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0" -dependencies = [ - "dunce", - "embed-resource", - "toml 0.9.11+spec-1.1.0", -] - -[[package]] -name = "tauri-winrt-notification" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9" -dependencies = [ - "quick-xml 0.37.5", - "thiserror 2.0.18", - "windows", - "windows-version", -] - -[[package]] -name = "tempfile" -version = "3.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" -dependencies = [ - "fastrand", - "getrandom 0.3.4", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "tendril" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" -dependencies = [ - "futf", - "mac", - "utf-8", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "time" -version = "0.3.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" -dependencies = [ - "deranged", - "itoa", - "libc", - "num-conv", - "num_threads", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" - -[[package]] -name = "time-macros" -version = "0.2.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.49.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" -dependencies = [ - "bytes", - "libc", - "mio", - "pin-project-lite", - "socket2", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" -dependencies = [ - "futures-util", - "log", - "tokio", - "tungstenite 0.24.0", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" -dependencies = [ - "futures-util", - "log", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tungstenite 0.28.0", - "webpki-roots 0.26.11", -] - -[[package]] -name = "tokio-util" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" -dependencies = [ - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.3", - "toml_edit 0.20.2", -] - -[[package]] -name = "toml" -version = "0.9.11+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" -dependencies = [ - "indexmap 2.13.0", - "serde_core", - "serde_spanned 1.0.4", - "toml_datetime 0.7.5+spec-1.1.0", - "toml_parser", - "toml_writer", - "winnow 0.7.14", -] - -[[package]] -name = "toml_datetime" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.19.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" -dependencies = [ - "indexmap 2.13.0", - "toml_datetime 0.6.3", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" -dependencies = [ - "indexmap 2.13.0", - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.3", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.23.10+spec-1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" -dependencies = [ - "indexmap 2.13.0", - "toml_datetime 0.7.5+spec-1.1.0", - "toml_parser", - "winnow 0.7.14", -] - -[[package]] -name = "toml_parser" -version = "1.0.6+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" -dependencies = [ - "winnow 0.7.14", -] - -[[package]] -name = "toml_writer" -version = "1.0.6+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" - -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" -dependencies = [ - "bitflags 2.10.0", - "bytes", - "futures-util", - "http", - "http-body", - "iri-string", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "tray-icon" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" -dependencies = [ - "crossbeam-channel", - "dirs", - "libappindicator", - "muda", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation", - "once_cell", - "png", - "serde", - "thiserror 2.0.18", - "windows-sys 0.60.2", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "tungstenite" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" -dependencies = [ - "byteorder", - "bytes", - "data-encoding", - "http", - "httparse", - "log", - "rand 0.8.5", - "sha1", - "thiserror 1.0.69", - "utf-8", -] - -[[package]] -name = "tungstenite" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" -dependencies = [ - "bytes", - "data-encoding", - "http", - "httparse", - "log", - "rand 0.9.2", - "rustls", - "rustls-pki-types", - "sha1", - "thiserror 2.0.18", - "utf-8", -] - -[[package]] -name = "typeid" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" - -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - -[[package]] -name = "uds_windows" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" -dependencies = [ - "memoffset", - "tempfile", - "winapi", -] - -[[package]] -name = "unic-char-property" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" -dependencies = [ - "unic-char-range", -] - -[[package]] -name = "unic-char-range" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" - -[[package]] -name = "unic-common" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" - -[[package]] -name = "unic-ucd-ident" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" -dependencies = [ - "unic-char-property", - "unic-char-range", - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-version" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" -dependencies = [ - "unic-common", -] - -[[package]] -name = "unicode-ident" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" - -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", - "serde_derive", -] - -[[package]] -name = "urlpattern" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" -dependencies = [ - "regex", - "serde", - "unic-ucd-ident", - "url", -] - -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - -[[package]] -name = "utf8-width" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "uuid" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" -dependencies = [ - "getrandom 0.3.4", - "js-sys", - "serde_core", - "wasm-bindgen", -] - -[[package]] -name = "value-bag" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" - -[[package]] -name = "version-compare" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "vswhom" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" -dependencies = [ - "libc", - "vswhom-sys", -] - -[[package]] -name = "vswhom-sys" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" -dependencies = [ - "cfg-if", - "futures-util", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.114", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-streams" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "web-sys" -version = "0.3.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webkit2gtk" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a" -dependencies = [ - "bitflags 1.3.2", - "cairo-rs", - "gdk", - "gdk-sys", - "gio", - "gio-sys", - "glib", - "glib-sys", - "gobject-sys", - "gtk", - "gtk-sys", - "javascriptcore-rs", - "libc", - "once_cell", - "soup3", - "webkit2gtk-sys", -] - -[[package]] -name = "webkit2gtk-sys" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c" -dependencies = [ - "bitflags 1.3.2", - "cairo-sys-rs", - "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "gtk-sys", - "javascriptcore-rs-sys", - "libc", - "pkg-config", - "soup3-sys", - "system-deps", -] - -[[package]] -name = "webpki-roots" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.5", -] - -[[package]] -name = "webpki-roots" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "webview2-com" -version = "0.38.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" -dependencies = [ - "webview2-com-macros", - "webview2-com-sys", - "windows", - "windows-core 0.61.2", - "windows-implement", - "windows-interface", -] - -[[package]] -name = "webview2-com-macros" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "webview2-com-sys" -version = "0.38.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" -dependencies = [ - "thiserror 2.0.18", - "windows", - "windows-core 0.61.2", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "window-vibrancy" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" -dependencies = [ - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "raw-window-handle", - "windows-sys 0.59.0", - "windows-version", -] - -[[package]] -name = "windows" -version = "0.61.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" -dependencies = [ - "windows-collections", - "windows-core 0.61.2", - "windows-future", - "windows-link 0.1.3", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" -dependencies = [ - "windows-core 0.61.2", -] - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - -[[package]] -name = "windows-future" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-numerics" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", -] - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link 0.2.1", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[package]] -name = "windows-threading" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-version" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "winnow" -version = "0.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -dependencies = [ - "memchr", -] - -[[package]] -name = "winnow" -version = "0.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" -dependencies = [ - "memchr", -] - -[[package]] -name = "winreg" -version = "0.55.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" -dependencies = [ - "cfg-if", - "windows-sys 0.59.0", -] - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" - -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - -[[package]] -name = "wry" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728b7d4c8ec8d81cab295e0b5b8a4c263c0d41a785fb8f8c4df284e5411140a2" -dependencies = [ - "base64 0.22.1", - "block2", - "cookie", - "crossbeam-channel", - "dirs", - "dpi", - "dunce", - "gdkx11", - "gtk", - "html5ever", - "http", - "javascriptcore-rs", - "jni", - "kuchikiki", - "libc", - "ndk", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "objc2-ui-kit", - "objc2-web-kit", - "once_cell", - "percent-encoding", - "raw-window-handle", - "sha2", - "soup3", - "tao-macros", - "thiserror 2.0.18", - "url", - "webkit2gtk", - "webkit2gtk-sys", - "webview2-com", - "windows", - "windows-core 0.61.2", - "windows-version", - "x11-dl", -] - -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - -[[package]] -name = "x11" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" -dependencies = [ - "libc", - "pkg-config", -] - -[[package]] -name = "x11-dl" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" -dependencies = [ - "libc", - "once_cell", - "pkg-config", -] - -[[package]] -name = "xattr" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" -dependencies = [ - "libc", - "rustix", -] - -[[package]] -name = "yoke" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", - "synstructure", -] - -[[package]] -name = "zbus" -version = "5.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfeff997a0aaa3eb20c4652baf788d2dfa6d2839a0ead0b3ff69ce2f9c4bdd1" -dependencies = [ - "async-broadcast", - "async-executor", - "async-io", - "async-lock", - "async-process", - "async-recursion", - "async-task", - "async-trait", - "blocking", - "enumflags2", - "event-listener", - "futures-core", - "futures-lite", - "hex", - "libc", - "ordered-stream", - "rustix", - "serde", - "serde_repr", - "tracing", - "uds_windows", - "uuid", - "windows-sys 0.61.2", - "winnow 0.7.14", - "zbus_macros", - "zbus_names", - "zvariant", -] - -[[package]] -name = "zbus_macros" -version = "5.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bbd5a90dbe8feee5b13def448427ae314ccd26a49cac47905cafefb9ff846f1" -dependencies = [ - "proc-macro-crate 3.4.0", - "proc-macro2", - "quote", - "syn 2.0.114", - "zbus_names", - "zvariant", - "zvariant_utils", -] - -[[package]] -name = "zbus_names" -version = "4.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" -dependencies = [ - "serde", - "winnow 0.7.14", - "zvariant", -] - -[[package]] -name = "zerocopy" -version = "0.8.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" - -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "zip" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" -dependencies = [ - "arbitrary", - "crc32fast", - "indexmap 2.13.0", - "memchr", -] - -[[package]] -name = "zmij" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f63c051f4fe3c1509da62131a678643c5b6fbdc9273b2b79d4378ebda003d2" - -[[package]] -name = "zvariant" -version = "5.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b64ef4f40c7951337ddc7023dd03528a57a3ce3408ee9da5e948bd29b232c4" -dependencies = [ - "endi", - "enumflags2", - "serde", - "winnow 0.7.14", - "zvariant_derive", - "zvariant_utils", -] - -[[package]] -name = "zvariant_derive" -version = "5.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "484d5d975eb7afb52cc6b929c13d3719a20ad650fea4120e6310de3fc55e415c" -dependencies = [ - "proc-macro-crate 3.4.0", - "proc-macro2", - "quote", - "syn 2.0.114", - "zvariant_utils", -] - -[[package]] -name = "zvariant_utils" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" -dependencies = [ - "proc-macro2", - "quote", - "serde", - "syn 2.0.114", - "winnow 0.7.14", -] diff --git a/packages/desktop/src-tauri/Cargo.toml b/packages/desktop/src-tauri/Cargo.toml deleted file mode 100644 index 6389c6118..000000000 --- a/packages/desktop/src-tauri/Cargo.toml +++ /dev/null @@ -1,52 +0,0 @@ -[package] -name = "paseo" -version = "0.1.30" -description = "Paseo Desktop" -authors = ["moboudra"] -license = "MIT" -repository = "https://github.com/getpaseo/paseo" -edition = "2021" -rust-version = "1.77.2" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[lib] -name = "paseo_lib" -crate-type = ["staticlib", "cdylib", "rlib"] - -[[bin]] -name = "Paseo" -path = "src/main.rs" - -[build-dependencies] -cc = "1" -tauri-build = { version = "2.5.3", features = [] } - -[dependencies] -base64 = "0.22" -dirs = "6" -dunce = "1" -futures-util = "0.3" -http = "1" -serde_json = "1.0" -serde = { version = "1.0", features = ["derive"] } -log = "0.4" -tauri = { version = "2.9.5", features = [] } -tauri-plugin-dialog = "2" -tauri-plugin-log = "2" -tauri-plugin-notification = "2" -tauri-plugin-opener = "2" -tauri-plugin-single-instance = "2" -tauri-plugin-updater = "2" -tauri-plugin-websocket = "2" -tokio = { version = "1", features = ["net", "sync"] } -tokio-tungstenite = "0.24" - - - - - - - - - diff --git a/packages/desktop/src-tauri/Entitlements.plist b/packages/desktop/src-tauri/Entitlements.plist deleted file mode 100644 index d637c5893..000000000 --- a/packages/desktop/src-tauri/Entitlements.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - com.apple.security.device.audio-input - - - diff --git a/packages/desktop/src-tauri/Info.plist b/packages/desktop/src-tauri/Info.plist deleted file mode 100644 index 833209241..000000000 --- a/packages/desktop/src-tauri/Info.plist +++ /dev/null @@ -1,18 +0,0 @@ - - - - - ApplePressAndHoldEnabled - - CFBundleIdentifier - dev.paseo.desktop - CFBundleName - Paseo - CFBundleShortVersionString - 0.1.30 - CFBundleVersion - 0.1.30 - NSMicrophoneUsageDescription - Paseo needs access to your microphone for voice dictation and voice mode. - - diff --git a/packages/desktop/src-tauri/build.rs b/packages/desktop/src-tauri/build.rs deleted file mode 100644 index 49297dfa3..000000000 --- a/packages/desktop/src-tauri/build.rs +++ /dev/null @@ -1,17 +0,0 @@ -fn main() { - #[cfg(target_os = "macos")] - { - println!("cargo:rerun-if-changed=macos/paseo_notifications.h"); - println!("cargo:rerun-if-changed=macos/paseo_notifications.m"); - - cc::Build::new() - .file("macos/paseo_notifications.m") - .flag("-fobjc-arc") - .compile("paseo_notifications"); - - println!("cargo:rustc-link-lib=framework=Foundation"); - println!("cargo:rustc-link-lib=framework=UserNotifications"); - } - - tauri_build::build() -} diff --git a/packages/desktop/src-tauri/capabilities/default.json b/packages/desktop/src-tauri/capabilities/default.json deleted file mode 100644 index 49b7c618c..000000000 --- a/packages/desktop/src-tauri/capabilities/default.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "$schema": "../gen/schemas/desktop-schema.json", - "identifier": "default", - "description": "enables the default permissions", - "remote": { - "urls": [ - "http://localhost:8081/*", - "http://127.0.0.1:8081/*" - ] - }, - "windows": [ - "main" - ], - "permissions": [ - "core:default", - "core:window:allow-start-dragging", - "core:window:allow-set-badge-count", - "core:window:allow-toggle-maximize", - "dialog:default", - "notification:default", - "opener:default", - "updater:default", - "websocket:default" - ] -} diff --git a/packages/desktop/src-tauri/icons/Square107x107Logo.png b/packages/desktop/src-tauri/icons/Square107x107Logo.png deleted file mode 100644 index cf1be2299..000000000 Binary files a/packages/desktop/src-tauri/icons/Square107x107Logo.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/Square142x142Logo.png b/packages/desktop/src-tauri/icons/Square142x142Logo.png deleted file mode 100644 index bf1d3a545..000000000 Binary files a/packages/desktop/src-tauri/icons/Square142x142Logo.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/Square150x150Logo.png b/packages/desktop/src-tauri/icons/Square150x150Logo.png deleted file mode 100644 index 8391af43d..000000000 Binary files a/packages/desktop/src-tauri/icons/Square150x150Logo.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/Square284x284Logo.png b/packages/desktop/src-tauri/icons/Square284x284Logo.png deleted file mode 100644 index 6273e28ce..000000000 Binary files a/packages/desktop/src-tauri/icons/Square284x284Logo.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/Square30x30Logo.png b/packages/desktop/src-tauri/icons/Square30x30Logo.png deleted file mode 100644 index 6b3f9902c..000000000 Binary files a/packages/desktop/src-tauri/icons/Square30x30Logo.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/Square310x310Logo.png b/packages/desktop/src-tauri/icons/Square310x310Logo.png deleted file mode 100644 index 81b45b1b7..000000000 Binary files a/packages/desktop/src-tauri/icons/Square310x310Logo.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/Square44x44Logo.png b/packages/desktop/src-tauri/icons/Square44x44Logo.png deleted file mode 100644 index 1942533c3..000000000 Binary files a/packages/desktop/src-tauri/icons/Square44x44Logo.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/Square71x71Logo.png b/packages/desktop/src-tauri/icons/Square71x71Logo.png deleted file mode 100644 index 7a25884cf..000000000 Binary files a/packages/desktop/src-tauri/icons/Square71x71Logo.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/Square89x89Logo.png b/packages/desktop/src-tauri/icons/Square89x89Logo.png deleted file mode 100644 index eb75102d6..000000000 Binary files a/packages/desktop/src-tauri/icons/Square89x89Logo.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/StoreLogo.png b/packages/desktop/src-tauri/icons/StoreLogo.png deleted file mode 100644 index 37804fead..000000000 Binary files a/packages/desktop/src-tauri/icons/StoreLogo.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml b/packages/desktop/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index 2ffbf24b6..000000000 --- a/packages/desktop/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/packages/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png b/packages/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 2481390b0..000000000 Binary files a/packages/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png b/packages/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png deleted file mode 100644 index 9da5b6def..000000000 Binary files a/packages/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png b/packages/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index f797f0443..000000000 Binary files a/packages/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png b/packages/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index def890fc3..000000000 Binary files a/packages/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png b/packages/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png deleted file mode 100644 index 050a59499..000000000 Binary files a/packages/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png b/packages/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png deleted file mode 100644 index 12a0d2cef..000000000 Binary files a/packages/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png b/packages/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 3d5eba088..000000000 Binary files a/packages/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png b/packages/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png deleted file mode 100644 index be486bf3e..000000000 Binary files a/packages/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png b/packages/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png deleted file mode 100644 index 0f40806fa..000000000 Binary files a/packages/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png b/packages/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 4aa7b7aff..000000000 Binary files a/packages/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png b/packages/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png deleted file mode 100644 index 30449a4f0..000000000 Binary files a/packages/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png b/packages/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index e15ca4d56..000000000 Binary files a/packages/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png b/packages/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index cc8c7eb88..000000000 Binary files a/packages/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/packages/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png deleted file mode 100644 index 061105f11..000000000 Binary files a/packages/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png b/packages/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png deleted file mode 100644 index c9271e1a4..000000000 Binary files a/packages/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/android/values/ic_launcher_background.xml b/packages/desktop/src-tauri/icons/android/values/ic_launcher_background.xml deleted file mode 100644 index ea9c223a6..000000000 --- a/packages/desktop/src-tauri/icons/android/values/ic_launcher_background.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - #fff - \ No newline at end of file diff --git a/packages/desktop/src-tauri/icons/ios/AppIcon-20x20@1x.png b/packages/desktop/src-tauri/icons/ios/AppIcon-20x20@1x.png deleted file mode 100644 index 787d776b9..000000000 Binary files a/packages/desktop/src-tauri/icons/ios/AppIcon-20x20@1x.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/ios/AppIcon-20x20@2x-1.png b/packages/desktop/src-tauri/icons/ios/AppIcon-20x20@2x-1.png deleted file mode 100644 index bc28ce3e3..000000000 Binary files a/packages/desktop/src-tauri/icons/ios/AppIcon-20x20@2x-1.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/ios/AppIcon-20x20@2x.png b/packages/desktop/src-tauri/icons/ios/AppIcon-20x20@2x.png deleted file mode 100644 index bc28ce3e3..000000000 Binary files a/packages/desktop/src-tauri/icons/ios/AppIcon-20x20@2x.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/ios/AppIcon-20x20@3x.png b/packages/desktop/src-tauri/icons/ios/AppIcon-20x20@3x.png deleted file mode 100644 index 9490c65bb..000000000 Binary files a/packages/desktop/src-tauri/icons/ios/AppIcon-20x20@3x.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/ios/AppIcon-29x29@1x.png b/packages/desktop/src-tauri/icons/ios/AppIcon-29x29@1x.png deleted file mode 100644 index eb73ffa18..000000000 Binary files a/packages/desktop/src-tauri/icons/ios/AppIcon-29x29@1x.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/ios/AppIcon-29x29@2x-1.png b/packages/desktop/src-tauri/icons/ios/AppIcon-29x29@2x-1.png deleted file mode 100644 index 68a8965ca..000000000 Binary files a/packages/desktop/src-tauri/icons/ios/AppIcon-29x29@2x-1.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/ios/AppIcon-29x29@2x.png b/packages/desktop/src-tauri/icons/ios/AppIcon-29x29@2x.png deleted file mode 100644 index 68a8965ca..000000000 Binary files a/packages/desktop/src-tauri/icons/ios/AppIcon-29x29@2x.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/ios/AppIcon-29x29@3x.png b/packages/desktop/src-tauri/icons/ios/AppIcon-29x29@3x.png deleted file mode 100644 index df5d03c6c..000000000 Binary files a/packages/desktop/src-tauri/icons/ios/AppIcon-29x29@3x.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/ios/AppIcon-40x40@1x.png b/packages/desktop/src-tauri/icons/ios/AppIcon-40x40@1x.png deleted file mode 100644 index bc28ce3e3..000000000 Binary files a/packages/desktop/src-tauri/icons/ios/AppIcon-40x40@1x.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/ios/AppIcon-40x40@2x-1.png b/packages/desktop/src-tauri/icons/ios/AppIcon-40x40@2x-1.png deleted file mode 100644 index 2300e5b7d..000000000 Binary files a/packages/desktop/src-tauri/icons/ios/AppIcon-40x40@2x-1.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/ios/AppIcon-40x40@2x.png b/packages/desktop/src-tauri/icons/ios/AppIcon-40x40@2x.png deleted file mode 100644 index 2300e5b7d..000000000 Binary files a/packages/desktop/src-tauri/icons/ios/AppIcon-40x40@2x.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/ios/AppIcon-40x40@3x.png b/packages/desktop/src-tauri/icons/ios/AppIcon-40x40@3x.png deleted file mode 100644 index 506498926..000000000 Binary files a/packages/desktop/src-tauri/icons/ios/AppIcon-40x40@3x.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/ios/AppIcon-512@2x.png b/packages/desktop/src-tauri/icons/ios/AppIcon-512@2x.png deleted file mode 100644 index db7297ea0..000000000 Binary files a/packages/desktop/src-tauri/icons/ios/AppIcon-512@2x.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/ios/AppIcon-60x60@2x.png b/packages/desktop/src-tauri/icons/ios/AppIcon-60x60@2x.png deleted file mode 100644 index 506498926..000000000 Binary files a/packages/desktop/src-tauri/icons/ios/AppIcon-60x60@2x.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/ios/AppIcon-60x60@3x.png b/packages/desktop/src-tauri/icons/ios/AppIcon-60x60@3x.png deleted file mode 100644 index 4366ab702..000000000 Binary files a/packages/desktop/src-tauri/icons/ios/AppIcon-60x60@3x.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/ios/AppIcon-76x76@1x.png b/packages/desktop/src-tauri/icons/ios/AppIcon-76x76@1x.png deleted file mode 100644 index 5bb59f59d..000000000 Binary files a/packages/desktop/src-tauri/icons/ios/AppIcon-76x76@1x.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/ios/AppIcon-76x76@2x.png b/packages/desktop/src-tauri/icons/ios/AppIcon-76x76@2x.png deleted file mode 100644 index c7afe7f8f..000000000 Binary files a/packages/desktop/src-tauri/icons/ios/AppIcon-76x76@2x.png and /dev/null differ diff --git a/packages/desktop/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png b/packages/desktop/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png deleted file mode 100644 index 0d555db08..000000000 Binary files a/packages/desktop/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png and /dev/null differ diff --git a/packages/desktop/src-tauri/installer-hooks.nsh b/packages/desktop/src-tauri/installer-hooks.nsh deleted file mode 100644 index 105239136..000000000 --- a/packages/desktop/src-tauri/installer-hooks.nsh +++ /dev/null @@ -1,11 +0,0 @@ -!macro NSIS_HOOK_POSTINSTALL - CreateDirectory "$LOCALAPPDATA\Microsoft\WinGet\Links" - FileOpen $0 "$LOCALAPPDATA\Microsoft\WinGet\Links\paseo.cmd" w - FileWrite $0 "@echo off$\r$\n" - FileWrite $0 '"$INSTDIR\Paseo.exe" %*$\r$\n' - FileClose $0 -!macroend - -!macro NSIS_HOOK_POSTUNINSTALL - Delete "$LOCALAPPDATA\Microsoft\WinGet\Links\paseo.cmd" -!macroend diff --git a/packages/desktop/src-tauri/macos/paseo_notifications.h b/packages/desktop/src-tauri/macos/paseo_notifications.h deleted file mode 100644 index 2bbec5b3a..000000000 --- a/packages/desktop/src-tauri/macos/paseo_notifications.h +++ /dev/null @@ -1,12 +0,0 @@ -#import - -typedef void (*PaseoNotificationClickCallback)(const char* payload_json); - -bool paseo_notifications_initialize(PaseoNotificationClickCallback callback, char** error_out); -bool paseo_notifications_send( - const char* title, - const char* body, - const char* payload_json, - char** error_out -); -void paseo_notifications_free_string(char* value); diff --git a/packages/desktop/src-tauri/macos/paseo_notifications.m b/packages/desktop/src-tauri/macos/paseo_notifications.m deleted file mode 100644 index 06b04921d..000000000 --- a/packages/desktop/src-tauri/macos/paseo_notifications.m +++ /dev/null @@ -1,176 +0,0 @@ -#import "paseo_notifications.h" - -#import -#import -#import - -@interface PaseoNotificationDelegate : NSObject -@property(nonatomic, assign) PaseoNotificationClickCallback clickCallback; -@end - -@implementation PaseoNotificationDelegate - -- (void)userNotificationCenter:(UNUserNotificationCenter*)center - didReceiveNotificationResponse:(UNNotificationResponse*)response - withCompletionHandler:(void (^)(void))completionHandler { - if ([response.actionIdentifier isEqualToString:UNNotificationDefaultActionIdentifier] && - self.clickCallback != NULL) { - id payload = response.notification.request.content.userInfo[@"paseoPayloadJson"]; - if ([payload isKindOfClass:[NSString class]]) { - self.clickCallback([(NSString*)payload UTF8String]); - } else { - self.clickCallback(NULL); - } - } - - [center removeDeliveredNotificationsWithIdentifiers:@[ - response.notification.request.identifier, - ]]; - completionHandler(); -} - -- (void)userNotificationCenter:(UNUserNotificationCenter*)center - willPresentNotification:(UNNotification*)notification - withCompletionHandler: - (void (^)(UNNotificationPresentationOptions options))completionHandler { - if (@available(macOS 11.0, *)) { - completionHandler( - UNNotificationPresentationOptionBanner | UNNotificationPresentationOptionList - ); - return; - } - - completionHandler((UNNotificationPresentationOptions)0); -} - -@end - -static PaseoNotificationDelegate* paseoNotificationDelegate = nil; -static BOOL paseoNotificationsAvailable = NO; - -static void paseo_run_on_main_sync(dispatch_block_t block) { - if ([NSThread isMainThread]) { - block(); - return; - } - - dispatch_sync(dispatch_get_main_queue(), block); -} - -static char* paseo_strdup(NSString* value) { - if (value == nil) { - return NULL; - } - - const char* utf8 = [value UTF8String]; - if (utf8 == NULL) { - return NULL; - } - - size_t length = strlen(utf8); - char* buffer = malloc(length + 1); - if (buffer == NULL) { - return NULL; - } - - memcpy(buffer, utf8, length + 1); - return buffer; -} - -static BOOL paseo_is_bundled_app_host(void) { - NSURL* bundleURL = [[NSBundle mainBundle] bundleURL]; - NSString* pathExtension = [[bundleURL pathExtension] lowercaseString]; - return [pathExtension isEqualToString:@"app"]; -} - -bool paseo_notifications_initialize(PaseoNotificationClickCallback callback, char** error_out) { - @autoreleasepool { - paseo_run_on_main_sync(^{ - if (paseoNotificationDelegate == nil) { - paseoNotificationDelegate = [[PaseoNotificationDelegate alloc] init]; - paseoNotificationsAvailable = paseo_is_bundled_app_host(); - if (paseoNotificationsAvailable) { - [[UNUserNotificationCenter currentNotificationCenter] - setDelegate:paseoNotificationDelegate]; - } - } - - paseoNotificationDelegate.clickCallback = callback; - }); - - if (error_out != NULL) { - *error_out = NULL; - } - return true; - } -} - -bool paseo_notifications_send( - const char* title, - const char* body, - const char* payload_json, - char** error_out -) { - @autoreleasepool { - if (paseoNotificationDelegate == nil) { - if (error_out != NULL) { - *error_out = paseo_strdup(@"Notification bridge is not initialized."); - } - return false; - } - - if (!paseoNotificationsAvailable) { - if (error_out != NULL) { - *error_out = NULL; - } - return true; - } - - NSString* notificationTitle = - [NSString stringWithUTF8String:(title != NULL ? title : "")]; - NSString* notificationBody = - (body != NULL && body[0] != '\0') ? [NSString stringWithUTF8String:body] : nil; - NSString* payloadString = - (payload_json != NULL && payload_json[0] != '\0') - ? [NSString stringWithUTF8String:payload_json] - : nil; - NSString* identifier = [[NSUUID UUID] UUIDString]; - - dispatch_async(dispatch_get_main_queue(), ^{ - UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init]; - content.title = notificationTitle; - if (notificationBody != nil) { - content.body = notificationBody; - } - if (payloadString != nil) { - content.userInfo = @{ - @"paseoPayloadJson" : payloadString, - }; - } - - UNNotificationRequest* request = - [UNNotificationRequest requestWithIdentifier:identifier - content:content - trigger:nil]; - [[UNUserNotificationCenter currentNotificationCenter] - addNotificationRequest:request - withCompletionHandler:^(NSError* _Nullable error) { - if (error != nil) { - NSLog(@"[PaseoNotifications] Failed to schedule notification: %@", - error.localizedDescription); - } - }]; - }); - - if (error_out != NULL) { - *error_out = NULL; - } - return true; - } -} - -void paseo_notifications_free_string(char* value) { - if (value != NULL) { - free(value); - } -} diff --git a/packages/desktop/src-tauri/resources/.gitkeep b/packages/desktop/src-tauri/resources/.gitkeep deleted file mode 100644 index 8b1378917..000000000 --- a/packages/desktop/src-tauri/resources/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/packages/desktop/src-tauri/src/desktop_notifications.rs b/packages/desktop/src-tauri/src/desktop_notifications.rs deleted file mode 100644 index 2d7b7c7fb..000000000 --- a/packages/desktop/src-tauri/src/desktop_notifications.rs +++ /dev/null @@ -1,280 +0,0 @@ -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use tauri::{AppHandle, Wry}; - -pub const DESKTOP_NOTIFICATION_CLICK_EVENT: &str = "desktop-notification-click"; - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DesktopNotificationInput { - pub title: String, - pub body: Option, - pub data: Option, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct DesktopNotificationClickPayload { - data: Option, -} - -#[cfg(target_os = "macos")] -mod platform { - use super::{ - AppHandle, DesktopNotificationClickPayload, DesktopNotificationInput, - DESKTOP_NOTIFICATION_CLICK_EVENT, Value, Wry, - }; - use std::ffi::{CStr, CString}; - use std::os::raw::c_char; - use std::sync::{Mutex, OnceLock}; - use tauri::Emitter; - - #[link(name = "paseo_notifications", kind = "static")] - unsafe extern "C" { - fn paseo_notifications_initialize( - callback: extern "C" fn(*const c_char), - error_out: *mut *mut c_char, - ) -> bool; - fn paseo_notifications_send( - title: *const c_char, - body: *const c_char, - payload_json: *const c_char, - error_out: *mut *mut c_char, - ) -> bool; - fn paseo_notifications_free_string(value: *mut c_char); - } - - static MACOS_NOTIFICATION_APP: OnceLock>>> = OnceLock::new(); - static MACOS_NOTIFICATION_INIT: OnceLock> = OnceLock::new(); - - fn app_slot() -> &'static Mutex>> { - MACOS_NOTIFICATION_APP.get_or_init(|| Mutex::new(None)) - } - - fn read_owned_error_message(error_ptr: *mut c_char) -> String { - if error_ptr.is_null() { - return "Unknown macOS notification error".to_string(); - } - - let message = unsafe { CStr::from_ptr(error_ptr) } - .to_string_lossy() - .into_owned(); - unsafe { - paseo_notifications_free_string(error_ptr); - } - message - } - - extern "C" fn handle_notification_click(payload_json: *const c_char) { - let data = if payload_json.is_null() { - None - } else { - let raw = unsafe { CStr::from_ptr(payload_json) }.to_string_lossy(); - serde_json::from_str::(&raw).ok() - }; - - let payload = DesktopNotificationClickPayload { data }; - let Ok(guard) = app_slot().lock() else { - return; - }; - let Some(app) = guard.clone() else { - return; - }; - if let Err(error) = app.emit(DESKTOP_NOTIFICATION_CLICK_EVENT, payload) { - log::warn!("failed to emit desktop notification click event: {error}"); - } - } - - fn ensure_notification_bridge() -> Result<(), String> { - MACOS_NOTIFICATION_INIT - .get_or_init(|| { - let mut error_ptr: *mut c_char = std::ptr::null_mut(); - let ok = unsafe { - paseo_notifications_initialize(handle_notification_click, &mut error_ptr) - }; - if ok { - Ok(()) - } else { - Err(read_owned_error_message(error_ptr)) - } - }) - .clone() - } - - pub fn send_notification( - app: AppHandle, - input: DesktopNotificationInput, - ) -> Result<(), String> { - { - let mut guard = app_slot() - .lock() - .map_err(|_| "Failed to lock macOS notification app handle".to_string())?; - *guard = Some(app); - } - - ensure_notification_bridge()?; - - let title = CString::new(input.title) - .map_err(|_| "Desktop notification title contains interior NUL byte".to_string())?; - let body = CString::new(input.body.unwrap_or_default()) - .map_err(|_| "Desktop notification body contains interior NUL byte".to_string())?; - let payload_json = CString::new( - input - .data - .map(|value| value.to_string()) - .unwrap_or_default(), - ) - .map_err(|_| "Desktop notification payload contains interior NUL byte".to_string())?; - - let mut error_ptr: *mut c_char = std::ptr::null_mut(); - let ok = unsafe { - paseo_notifications_send( - title.as_ptr(), - body.as_ptr(), - payload_json.as_ptr(), - &mut error_ptr, - ) - }; - if ok { - Ok(()) - } else { - Err(read_owned_error_message(error_ptr)) - } - } - - #[cfg(test)] - mod tests { - use super::*; - use std::sync::mpsc; - use std::time::{Duration, Instant}; - - extern "C" fn noop_notification_click(_payload_json: *const c_char) {} - - static CLICK_TEST_SENDER: std::sync::OnceLock>> = - std::sync::OnceLock::new(); - - extern "C" fn capture_notification_click(payload_json: *const c_char) { - let payload = if payload_json.is_null() { - None - } else { - Some( - unsafe { CStr::from_ptr(payload_json) } - .to_string_lossy() - .into_owned(), - ) - }; - - if let Some(sender) = CLICK_TEST_SENDER.get() { - let _ = sender.send(payload); - } - } - - #[test] - #[ignore = "requires a running Cocoa app main queue; verify through the desktop app"] - fn native_notification_send_returns_without_waiting_for_click() { - let mut error_ptr: *mut c_char = std::ptr::null_mut(); - let initialized = unsafe { - paseo_notifications_initialize(noop_notification_click, &mut error_ptr) - }; - assert!( - initialized, - "bridge initialization failed: {}", - read_owned_error_message(error_ptr) - ); - - let title = CString::new("Paseo notification smoke test").expect("valid title"); - let started_at = Instant::now(); - let sent = unsafe { - paseo_notifications_send( - title.as_ptr(), - std::ptr::null(), - std::ptr::null(), - &mut error_ptr, - ) - }; - assert!( - sent, - "native send failed: {}", - read_owned_error_message(error_ptr) - ); - assert!( - started_at.elapsed() < Duration::from_secs(2), - "native send unexpectedly blocked for {:?}", - started_at.elapsed() - ); - } - - #[test] - #[ignore = "manual smoke test; click the macOS notification within 60 seconds"] - fn native_notification_click_callback_roundtrip() { - let (tx, rx) = mpsc::channel(); - let _ = CLICK_TEST_SENDER.set(tx); - - let mut error_ptr: *mut c_char = std::ptr::null_mut(); - let initialized = unsafe { - paseo_notifications_initialize(capture_notification_click, &mut error_ptr) - }; - assert!( - initialized, - "bridge initialization failed: {}", - read_owned_error_message(error_ptr) - ); - - let title = CString::new("Paseo manual click test").expect("valid title"); - let body = CString::new("Click this notification to verify the callback path.") - .expect("valid body"); - let payload_json = - CString::new(r#"{"kind":"manual-smoke","source":"codex"}"#).expect("valid payload"); - - let sent = unsafe { - paseo_notifications_send( - title.as_ptr(), - body.as_ptr(), - payload_json.as_ptr(), - &mut error_ptr, - ) - }; - assert!( - sent, - "native send failed: {}", - read_owned_error_message(error_ptr) - ); - - eprintln!("Notification sent. Click it within 60 seconds."); - let payload = rx - .recv_timeout(Duration::from_secs(60)) - .expect("timed out waiting for notification click"); - assert_eq!(payload.as_deref(), Some(r#"{"kind":"manual-smoke","source":"codex"}"#)); - } - } -} - -#[cfg(not(target_os = "macos"))] -mod platform { - use super::{AppHandle, DesktopNotificationInput, Wry}; - use tauri_plugin_notification::NotificationExt; - - pub fn send_notification( - app: AppHandle, - input: DesktopNotificationInput, - ) -> Result<(), String> { - let mut notification = app.notification().builder().title(&input.title); - if let Some(body) = &input.body { - notification = notification.body(body); - } - - notification - .show() - .map_err(|error| format!("Failed to send desktop notification: {error}"))?; - - Ok(()) - } -} - -#[tauri::command] -pub async fn send_desktop_notification( - app: AppHandle, - input: DesktopNotificationInput, -) -> Result<(), String> { - platform::send_notification(app, input) -} diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs deleted file mode 100644 index 6a25fcf80..000000000 --- a/packages/desktop/src-tauri/src/lib.rs +++ /dev/null @@ -1,717 +0,0 @@ -use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _}; -use serde::Serialize; -use std::collections::HashSet; -use std::fs; -use std::path::{Path, PathBuf}; -use std::process::Command; -use std::sync::atomic::{AtomicU64, Ordering}; -#[cfg(target_os = "macos")] -use tauri::menu::AboutMetadata; -use tauri::menu::{Menu, MenuItemBuilder, MenuItemKind, PredefinedMenuItem, Submenu}; -use tauri::{AppHandle, Manager, WebviewWindow}; -use tauri_plugin_updater::UpdaterExt; - -mod desktop_notifications; -mod runtime_manager; -use desktop_notifications::send_desktop_notification; -use runtime_manager::{ - cli_symlink_instructions, close_local_daemon_transport, managed_daemon_logs, - managed_daemon_pairing, managed_daemon_status, managed_runtime_status, - open_local_daemon_transport, restart_managed_daemon, run_managed_cli_from_current_process, - send_local_daemon_transport_message, start_managed_daemon, stop_managed_daemon, - update_managed_daemon_tcp_settings, LocalTransportState, -}; - -// Store zoom as u64 bits (f64 * 100 as integer for atomic ops) -static ZOOM_LEVEL: AtomicU64 = AtomicU64::new(100); - -fn get_zoom_factor() -> f64 { - ZOOM_LEVEL.load(Ordering::Relaxed) as f64 / 100.0 -} - -fn set_zoom_factor(webview: &WebviewWindow, factor: f64) { - let clamped = factor.clamp(0.5, 3.0); - ZOOM_LEVEL.store((clamped * 100.0) as u64, Ordering::Relaxed); - let _ = webview.set_zoom(clamped); -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -struct DaemonUpdateCommandResult { - exit_code: i32, - stdout: String, - stderr: String, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -struct AppUpdateCheckResult { - has_update: bool, - current_version: String, - latest_version: Option, - body: Option, - date: Option, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -struct AppUpdateInstallResult { - installed: bool, - version: Option, - message: String, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -struct LocalDaemonVersionResult { - version: Option, - error: Option, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -struct AttachmentFileResult { - path: String, - byte_size: u64, -} - -fn is_ignored_gui_launch_arg(arg: &str) -> bool { - arg.starts_with("-psn_") -} - -fn parse_cli_passthrough_args_from_argv(args: &[String]) -> Option> { - let effective = args - .iter() - .skip(1) - .filter(|arg| !is_ignored_gui_launch_arg(arg)) - .cloned() - .collect::>(); - (!effective.is_empty()).then_some(effective) -} - -pub fn try_run_pre_tauri_mode() -> Option { - let args = std::env::args().collect::>(); - let Some(cli_args) = parse_cli_passthrough_args_from_argv(&args) else { - return None; - }; - - Some( - run_managed_cli_from_current_process(cli_args).unwrap_or_else(|error| { - eprintln!("{error}"); - 1 - }), - ) -} - -fn shell_command(script: &str) -> Command { - #[cfg(unix)] - { - let shell = std::env::var("SHELL") - .ok() - .map(|value| value.trim().to_string()) - .filter(|value| !value.is_empty()) - .unwrap_or_else(|| "/bin/zsh".to_string()); - let mut cmd = Command::new(shell); - cmd.arg("-lc").arg(script); - cmd - } - #[cfg(windows)] - { - use std::os::windows::process::CommandExt; - let mut cmd = Command::new("cmd.exe"); - cmd.arg("/C").arg(script); - cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW - cmd - } -} - -fn execute_local_daemon_version() -> LocalDaemonVersionResult { - #[cfg(unix)] - let script = r#"if command -v paseo >/dev/null 2>&1; then - paseo --version -else - echo "paseo command not found in PATH" >&2 - exit 127 -fi"#; - #[cfg(windows)] - let script = r#"where paseo >nul 2>&1 && paseo --version || (echo paseo command not found in PATH >&2 & exit /b 127)"#; - - match shell_command(script).output() { - Ok(output) => { - if output.status.success() { - let version = String::from_utf8_lossy(&output.stdout).trim().to_string(); - if version.is_empty() { - LocalDaemonVersionResult { - version: None, - error: Some("paseo --version returned empty output".to_string()), - } - } else { - LocalDaemonVersionResult { - version: Some(version), - error: None, - } - } - } else { - let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); - LocalDaemonVersionResult { - version: None, - error: Some(if stderr.is_empty() { - format!( - "paseo --version exited with code {}", - output.status.code().unwrap_or(1) - ) - } else { - stderr - }), - } - } - } - Err(error) => LocalDaemonVersionResult { - version: None, - error: Some(format!("Failed to run version check: {error}")), - }, - } -} - -fn execute_local_daemon_update() -> DaemonUpdateCommandResult { - #[cfg(unix)] - let script = r#"if command -v paseo >/dev/null 2>&1; then - paseo daemon update -else - echo "paseo command not found in PATH. Ensure Paseo CLI is installed for this user." >&2 - exit 127 -fi"#; - #[cfg(windows)] - let script = r#"where paseo >nul 2>&1 && paseo daemon update || (echo paseo command not found in PATH. Ensure Paseo CLI is installed for this user. >&2 & exit /b 127)"#; - - match shell_command(script).output() { - Ok(output) => DaemonUpdateCommandResult { - exit_code: output.status.code().unwrap_or(1), - stdout: String::from_utf8_lossy(&output.stdout).into_owned(), - stderr: String::from_utf8_lossy(&output.stderr).into_owned(), - }, - Err(error) => DaemonUpdateCommandResult { - exit_code: -1, - stdout: String::new(), - stderr: format!("Failed to run daemon update command: {error}"), - }, - } -} - -#[tauri::command] -async fn get_local_daemon_version() -> LocalDaemonVersionResult { - tauri::async_runtime::spawn_blocking(execute_local_daemon_version) - .await - .unwrap_or_else(|error| LocalDaemonVersionResult { - version: None, - error: Some(format!("Version check task failed: {error}")), - }) -} - -#[tauri::command] -async fn run_local_daemon_update() -> DaemonUpdateCommandResult { - tauri::async_runtime::spawn_blocking(execute_local_daemon_update) - .await - .unwrap_or_else(|error| DaemonUpdateCommandResult { - exit_code: -1, - stdout: String::new(), - stderr: format!("Daemon update task failed: {error}"), - }) -} - -#[tauri::command] -async fn check_app_update(app: AppHandle) -> Result { - let current_version = app.package_info().version.to_string(); - let updater = app - .updater() - .map_err(|error| format!("Failed to initialize updater: {error}"))?; - let update = updater - .check() - .await - .map_err(|error| format!("Failed to check for updates: {error}"))?; - - if let Some(update) = update { - return Ok(AppUpdateCheckResult { - has_update: true, - current_version, - latest_version: Some(update.version.to_string()), - body: update.body, - date: update.date.map(|date| date.to_string()), - }); - } - - Ok(AppUpdateCheckResult { - has_update: false, - current_version, - latest_version: None, - body: None, - date: None, - }) -} - -#[tauri::command] -async fn install_app_update(app: AppHandle) -> Result { - let updater = app - .updater() - .map_err(|error| format!("Failed to initialize updater: {error}"))?; - let update = updater - .check() - .await - .map_err(|error| format!("Failed to check for updates: {error}"))?; - - let Some(update) = update else { - return Ok(AppUpdateInstallResult { - installed: false, - version: None, - message: "No update is currently available.".to_string(), - }); - }; - - let version = update.version.to_string(); - update - .download_and_install(|_, _| {}, || {}) - .await - .map_err(|error| format!("Failed to download and install update: {error}"))?; - - Ok(AppUpdateInstallResult { - installed: true, - version: Some(version), - message: "Update installed. Restart Paseo to finish applying it.".to_string(), - }) -} - -fn resolve_attachment_dir(app: &AppHandle) -> Result { - let app_data_dir = app - .path() - .app_data_dir() - .map_err(|error| format!("Failed to resolve app data directory: {error}"))?; - let attachment_dir = app_data_dir.join("paseo-desktop-attachments"); - fs::create_dir_all(&attachment_dir) - .map_err(|error| format!("Failed to create attachment directory: {error}"))?; - Ok(attachment_dir) -} - -fn normalize_extension(extension: Option) -> String { - let raw = extension - .unwrap_or_default() - .trim() - .trim_matches('.') - .to_string(); - if raw.is_empty() { - String::new() - } else { - format!(".{raw}") - } -} - -fn validate_attachment_id(attachment_id: &str) -> Result<(), String> { - if attachment_id.is_empty() { - return Err("Attachment ID cannot be empty.".to_string()); - } - if !attachment_id - .chars() - .all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '_') - { - return Err("Attachment ID contains invalid characters.".to_string()); - } - Ok(()) -} - -fn clear_existing_attachment_files( - attachment_dir: &Path, - attachment_id: &str, -) -> Result<(), String> { - let id_prefix = format!("{attachment_id}."); - let entries = fs::read_dir(attachment_dir) - .map_err(|error| format!("Failed to scan attachment directory: {error}"))?; - - for entry in entries { - let entry = entry.map_err(|error| format!("Failed to read directory entry: {error}"))?; - let path = entry.path(); - if !path.is_file() { - continue; - } - let file_name = entry.file_name(); - let file_name = file_name.to_string_lossy(); - if file_name == attachment_id || file_name.starts_with(&id_prefix) { - fs::remove_file(&path) - .map_err(|error| format!("Failed to remove prior attachment file: {error}"))?; - } - } - - Ok(()) -} - -fn build_attachment_path(attachment_dir: &Path, attachment_id: &str, extension: &str) -> PathBuf { - attachment_dir.join(format!("{attachment_id}{extension}")) -} - -fn canonicalize_managed_attachment_path( - attachment_dir: &Path, - path: &str, -) -> Result { - let candidate = PathBuf::from(path); - if !candidate.exists() { - return Err(format!("Attachment file not found at path: {path}")); - } - - let canonical_candidate = fs::canonicalize(&candidate) - .map_err(|error| format!("Failed to resolve attachment path: {error}"))?; - let canonical_dir = fs::canonicalize(attachment_dir) - .map_err(|error| format!("Failed to resolve attachment directory: {error}"))?; - - if !canonical_candidate.starts_with(&canonical_dir) { - return Err("Attachment path is outside managed attachment directory.".to_string()); - } - - Ok(canonical_candidate) -} - -#[tauri::command] -async fn write_attachment_base64( - app: AppHandle, - attachment_id: String, - base64: String, - extension: Option, -) -> Result { - tauri::async_runtime::spawn_blocking(move || { - validate_attachment_id(&attachment_id)?; - let attachment_dir = resolve_attachment_dir(&app)?; - clear_existing_attachment_files(&attachment_dir, &attachment_id)?; - let normalized_extension = normalize_extension(extension); - let attachment_path = - build_attachment_path(&attachment_dir, &attachment_id, &normalized_extension); - let decoded_bytes = BASE64_STANDARD - .decode(base64.as_bytes()) - .map_err(|error| format!("Failed to decode attachment base64: {error}"))?; - fs::write(&attachment_path, &decoded_bytes) - .map_err(|error| format!("Failed to write attachment file: {error}"))?; - - Ok(AttachmentFileResult { - path: attachment_path.to_string_lossy().into_owned(), - byte_size: decoded_bytes.len() as u64, - }) - }) - .await - .map_err(|error| format!("Attachment write task failed: {error}"))? -} - -#[tauri::command] -async fn copy_attachment_file( - app: AppHandle, - attachment_id: String, - source_path: String, - extension: Option, -) -> Result { - tauri::async_runtime::spawn_blocking(move || { - validate_attachment_id(&attachment_id)?; - let source = PathBuf::from(source_path); - if !source.exists() { - return Err("Source attachment file does not exist.".to_string()); - } - - let source_extension = source - .extension() - .and_then(|value| value.to_str()) - .map(|value| value.to_string()); - let normalized_extension = normalize_extension(extension.or(source_extension)); - let attachment_dir = resolve_attachment_dir(&app)?; - clear_existing_attachment_files(&attachment_dir, &attachment_id)?; - let destination_path = - build_attachment_path(&attachment_dir, &attachment_id, &normalized_extension); - let copied_bytes = fs::copy(&source, &destination_path) - .map_err(|error| format!("Failed to copy attachment file: {error}"))?; - - Ok(AttachmentFileResult { - path: destination_path.to_string_lossy().into_owned(), - byte_size: copied_bytes, - }) - }) - .await - .map_err(|error| format!("Attachment copy task failed: {error}"))? -} - -#[tauri::command] -async fn read_file_base64(app: AppHandle, path: String) -> Result { - tauri::async_runtime::spawn_blocking(move || { - let attachment_dir = resolve_attachment_dir(&app)?; - let attachment_path = canonicalize_managed_attachment_path(&attachment_dir, &path)?; - let bytes = fs::read(&attachment_path) - .map_err(|error| format!("Failed to read attachment file: {error}"))?; - Ok(BASE64_STANDARD.encode(bytes)) - }) - .await - .map_err(|error| format!("Attachment read task failed: {error}"))? -} - -#[tauri::command] -async fn delete_attachment_file(app: AppHandle, path: String) -> Result { - tauri::async_runtime::spawn_blocking(move || { - let attachment_dir = resolve_attachment_dir(&app)?; - let attachment_path = match canonicalize_managed_attachment_path(&attachment_dir, &path) { - Ok(path) => path, - Err(_) => return Ok(false), - }; - fs::remove_file(&attachment_path) - .map_err(|error| format!("Failed to delete attachment file: {error}"))?; - Ok(true) - }) - .await - .map_err(|error| format!("Attachment delete task failed: {error}"))? -} - -#[tauri::command] -async fn garbage_collect_attachment_files( - app: AppHandle, - referenced_ids: Vec, -) -> Result { - tauri::async_runtime::spawn_blocking(move || { - let attachment_dir = resolve_attachment_dir(&app)?; - let referenced = referenced_ids.into_iter().collect::>(); - let mut deleted_count = 0_u64; - - let entries = fs::read_dir(&attachment_dir) - .map_err(|error| format!("Failed to scan attachment directory: {error}"))?; - for entry in entries { - let entry = - entry.map_err(|error| format!("Failed to read directory entry: {error}"))?; - let path = entry.path(); - if !path.is_file() { - continue; - } - - let file_name = entry.file_name(); - let file_name = file_name.to_string_lossy(); - let id = file_name.split('.').next().unwrap_or_default(); - if id.is_empty() || referenced.contains(id) { - continue; - } - - fs::remove_file(&path) - .map_err(|error| format!("Failed to delete stale attachment file: {error}"))?; - deleted_count += 1; - } - - Ok(deleted_count) - }) - .await - .map_err(|error| format!("Attachment GC task failed: {error}"))? -} - -#[tauri::command] -fn webview_log(level: u8, message: String) { - match level { - 0 => log::debug!("[webview] {}", message), - 1 => log::info!("[webview] {}", message), - 2 => log::warn!("[webview] {}", message), - _ => log::error!("[webview] {}", message), - } -} - -#[cfg_attr(mobile, tauri::mobile_entry_point)] -pub fn run() { - tauri::Builder::default() - .manage(LocalTransportState::default()) - .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| { - if let Some(window) = app.get_webview_window("main") { - let _ = window.show(); - let _ = window.unminimize(); - let _ = window.set_focus(); - } - })) - .plugin(tauri_plugin_dialog::init()) - .plugin(tauri_plugin_notification::init()) - .plugin(tauri_plugin_opener::init()) - .plugin(tauri_plugin_updater::Builder::new().build()) - .plugin(tauri_plugin_websocket::init()) - .invoke_handler(tauri::generate_handler![ - managed_runtime_status, - managed_daemon_status, - cli_symlink_instructions, - start_managed_daemon, - stop_managed_daemon, - restart_managed_daemon, - managed_daemon_logs, - managed_daemon_pairing, - update_managed_daemon_tcp_settings, - open_local_daemon_transport, - send_local_daemon_transport_message, - close_local_daemon_transport, - get_local_daemon_version, - run_local_daemon_update, - check_app_update, - install_app_update, - write_attachment_base64, - copy_attachment_file, - read_file_base64, - delete_attachment_file, - garbage_collect_attachment_files, - send_desktop_notification, - webview_log - ]) - .setup(|app| { - let setup_start = std::time::Instant::now(); - log::info!( - "[app] Paseo Desktop v{} setup starting", - app.package_info().version - ); - - app.handle().plugin( - tauri_plugin_log::Builder::default() - .level(log::LevelFilter::Info) - .level_for("tao", log::LevelFilter::Warn) - .level_for("wry", log::LevelFilter::Warn) - .max_file_size(5_000_000) - .rotation_strategy(tauri_plugin_log::RotationStrategy::KeepAll) - .clear_targets() - .target(tauri_plugin_log::Target::new( - tauri_plugin_log::TargetKind::LogDir { - file_name: Some("app.log".into()), - }, - )) - .target(tauri_plugin_log::Target::new( - tauri_plugin_log::TargetKind::Stdout, - )) - .build(), - )?; - - // Start from Tauri's default menu so macOS standard shortcuts (Cmd+A/C/V/etc) - // keep working. Then inject our zoom controls into a View menu. - // - // On macOS in particular, a custom menu that omits Edit items can break - // responder-chain shortcuts across the whole app. - let menu = Menu::default(app.handle())?; - - #[cfg(target_os = "macos")] - { - let app_menu = menu.items()?.into_iter().find_map(|item| match item { - MenuItemKind::Submenu(submenu) => Some(submenu), - _ => None, - }); - - if let Some(submenu) = app_menu { - // Tauri's default about item sets only `version`, which macOS renders as - // "Version ()". Set only `short_version` instead. - let about_metadata = AboutMetadata { - name: Some(app.package_info().name.clone()), - short_version: Some(app.package_info().version.to_string()), - copyright: app.config().bundle.copyright.clone(), - ..Default::default() - }; - let about = - PredefinedMenuItem::about(app.handle(), None, Some(about_metadata))?; - - if submenu.remove_at(0)?.is_some() { - submenu.insert(&about, 0)?; - } - } - } - - let zoom_in = MenuItemBuilder::with_id("zoom_in", "Zoom In") - .accelerator("CmdOrCtrl+=") - .build(app)?; - let zoom_out = MenuItemBuilder::with_id("zoom_out", "Zoom Out") - .accelerator("CmdOrCtrl+-") - .build(app)?; - let zoom_reset = MenuItemBuilder::with_id("zoom_reset", "Actual Size") - .accelerator("CmdOrCtrl+0") - .build(app)?; - - let separator = PredefinedMenuItem::separator(app.handle())?; - - // On macOS, Tauri's default menu already has a "View" submenu (with Fullscreen). - // Insert our zoom items at the top so we don't duplicate the submenu. - #[cfg(target_os = "macos")] - { - let mut view_submenu: Option> = None; - for item in menu.items()? { - if let MenuItemKind::Submenu(submenu) = item { - if submenu.text()? == "View" { - view_submenu = Some(submenu); - break; - } - } - } - - if let Some(view) = view_submenu { - // Zoom controls first, then keep existing items (e.g. Fullscreen). - view.insert_items(&[&zoom_in, &zoom_out, &zoom_reset, &separator], 0)?; - } else { - // Fallback: if the default menu ever changes, create a View menu. - let view_menu = Submenu::with_items( - app, - "View", - true, - &[&zoom_in, &zoom_out, &zoom_reset, &separator], - )?; - menu.append(&view_menu)?; - } - } - - // Non-macOS: default menu doesn't include a View menu, so add it. - #[cfg(not(target_os = "macos"))] - { - let view_menu = - Submenu::with_items(app, "View", true, &[&zoom_in, &zoom_out, &zoom_reset])?; - menu.append(&view_menu)?; - } - - app.set_menu(menu)?; - log::info!("[app] setup complete ({}ms)", setup_start.elapsed().as_millis()); - - let window = app.get_webview_window("main").unwrap(); - let window_clone = window.clone(); - - app.on_menu_event(move |_app, event| { - let id = event.id().as_ref(); - if id == "zoom_in" { - let current = get_zoom_factor(); - set_zoom_factor(&window_clone, current + 0.1); - } else if id == "zoom_out" { - let current = get_zoom_factor(); - set_zoom_factor(&window_clone, current - 0.1); - } else if id == "zoom_reset" { - set_zoom_factor(&window_clone, 1.0); - } - }); - - Ok(()) - }) - .run(tauri::generate_context!()) - .expect("error while running tauri application"); -} - -#[cfg(test)] -mod tests { - use super::parse_cli_passthrough_args_from_argv; - - #[test] - fn routes_meaningful_args_to_cli_mode() { - let args = vec![ - "/Applications/Paseo.app/Contents/MacOS/Paseo".to_string(), - "--version".to_string(), - ]; - - assert_eq!( - parse_cli_passthrough_args_from_argv(&args), - Some(vec!["--version".to_string()]) - ); - } - - #[test] - fn ignores_plain_gui_launch() { - let args = vec!["/Applications/Paseo.app/Contents/MacOS/Paseo".to_string()]; - - assert_eq!(parse_cli_passthrough_args_from_argv(&args), None); - } - - #[test] - fn ignores_macos_process_serial_number_argument() { - let args = vec![ - "/Applications/Paseo.app/Contents/MacOS/Paseo".to_string(), - "-psn_0_12345".to_string(), - ]; - - assert_eq!(parse_cli_passthrough_args_from_argv(&args), None); - } -} diff --git a/packages/desktop/src-tauri/src/main.rs b/packages/desktop/src-tauri/src/main.rs deleted file mode 100644 index 09b423e9a..000000000 --- a/packages/desktop/src-tauri/src/main.rs +++ /dev/null @@ -1,15 +0,0 @@ -// Prevents additional console window on Windows in release, DO NOT REMOVE!! -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] - -// `tauri dev` runs the raw executable (`target/debug/Paseo`) on macOS (not the .app bundle), -// so we must embed an Info.plist containing usage descriptions for WebKit media -// permission prompts in dev. -#[cfg(all(target_os = "macos", debug_assertions))] -tauri::embed_plist::embed_info_plist!("../Info.plist"); - -fn main() { - if let Some(exit_code) = paseo_lib::try_run_pre_tauri_mode() { - std::process::exit(exit_code); - } - paseo_lib::run(); -} diff --git a/packages/desktop/src-tauri/src/runtime_manager.rs b/packages/desktop/src-tauri/src/runtime_manager.rs deleted file mode 100644 index f64876c8b..000000000 --- a/packages/desktop/src-tauri/src/runtime_manager.rs +++ /dev/null @@ -1,1079 +0,0 @@ -use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _}; -use futures_util::{SinkExt, StreamExt}; -use http::Request; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::fs; -use std::io; -use std::path::{Path, PathBuf}; -use std::process::{Command, Stdio}; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::{Arc, Mutex}; -use tauri::{AppHandle, Emitter, Manager, State}; -use tokio::sync::mpsc; -use tokio_tungstenite::tungstenite::client::IntoClientRequest; -use tokio_tungstenite::tungstenite::Message; - -const LOCAL_TRANSPORT_EVENT_NAME: &str = "local-daemon-transport-event"; -const UNIX_CLIENT_URL: &str = "ws://localhost/ws"; -#[cfg(windows)] -const PIPE_CLIENT_URL: &str = "ws://localhost/ws"; -const CLI_LINK_NAME: &str = "paseo"; -#[cfg(windows)] -const CLI_LINK_WINDOWS_NAME: &str = "paseo.exe"; -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ManagedRuntimeManifest { - pub runtime_id: String, - pub runtime_version: String, - pub platform: String, - pub arch: String, - pub created_at: String, - pub node_relative_path: String, - pub cli_entrypoint_relative_path: String, - pub cli_shim_relative_path: String, - pub server_runner_relative_path: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct BundledRuntimePointer { - runtime_id: String, - runtime_version: String, - relative_root: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ManagedTcpSettings { - pub enabled: bool, - pub host: String, - pub port: u16, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ManagedRuntimeStatus { - pub runtime_id: String, - pub runtime_version: String, - pub runtime_root: String, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct CliDaemonStatus { - server_id: Option, - status: String, - listen: String, - hostname: Option, - pid: Option, - home: String, - log_path: String, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ManagedDaemonStatus { - pub runtime_id: String, - pub runtime_version: String, - pub server_id: String, - pub status: String, - pub listen: String, - pub hostname: Option, - pub pid: Option, - pub home: String, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ManagedDaemonLogs { - pub log_path: String, - pub contents: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ManagedPairingOffer { - pub relay_enabled: bool, - pub url: Option, - pub qr: Option, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct CliSymlinkInstructions { - pub title: String, - pub detail: String, - pub commands: String, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct LocalTransportEvent { - session_id: String, - kind: String, - text: Option, - binary_base64: Option, - code: Option, - reason: Option, - error: Option, -} - -struct LocalTransportSession { - sender: mpsc::UnboundedSender, -} - -pub struct LocalTransportState { - next_session_id: AtomicU64, - sessions: Arc>>, -} - -impl Default for LocalTransportState { - fn default() -> Self { - Self { - next_session_id: AtomicU64::new(1), - sessions: Arc::new(Mutex::new(HashMap::new())), - } - } -} - -impl LocalTransportState { - fn alloc_session_id(&self) -> String { - format!( - "local-session-{}", - self.next_session_id.fetch_add(1, Ordering::Relaxed) - ) - } -} - -#[cfg(unix)] -fn build_local_websocket_request( - url: &str, -) -> Result, tokio_tungstenite::tungstenite::Error> { - url.into_client_request() -} - -#[cfg(windows)] -fn build_local_websocket_request( - url: &str, -) -> Result, tokio_tungstenite::tungstenite::Error> { - url.into_client_request() -} - -#[cfg(all(test, unix))] -fn local_client_url() -> &'static str { - UNIX_CLIENT_URL -} - -#[cfg(all(test, windows))] -fn local_client_url() -> &'static str { - PIPE_CLIENT_URL -} - -#[cfg(unix)] -async fn connect_local_socket( - socket_path: PathBuf, -) -> Result< - tokio_tungstenite::WebSocketStream, - tokio_tungstenite::tungstenite::Error, -> { - let stream = tokio::net::UnixStream::connect(socket_path) - .await - .map_err(tokio_tungstenite::tungstenite::Error::Io)?; - let request = build_local_websocket_request(UNIX_CLIENT_URL)?; - let (ws_stream, _) = tokio_tungstenite::client_async(request, stream).await?; - Ok(ws_stream) -} - -#[cfg(windows)] -async fn connect_local_pipe( - pipe_path: String, -) -> Result< - tokio_tungstenite::WebSocketStream, - tokio_tungstenite::tungstenite::Error, -> { - let stream = tokio::net::windows::named_pipe::ClientOptions::new() - .open(&pipe_path) - .map_err(tokio_tungstenite::tungstenite::Error::Io)?; - let request = build_local_websocket_request(PIPE_CLIENT_URL)?; - let (ws_stream, _) = tokio_tungstenite::client_async(request, stream).await?; - Ok(ws_stream) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn manual_http_request_lacks_websocket_handshake_headers() { - let request = Request::builder() - .uri(local_client_url()) - .header("Host", "localhost") - .body(()) - .expect("valid manual request"); - - assert!(request.headers().get("sec-websocket-key").is_none()); - assert!(request.headers().get("sec-websocket-version").is_none()); - assert!(request.headers().get("upgrade").is_none()); - assert!(request.headers().get("connection").is_none()); - } - - #[test] - fn generated_local_websocket_request_includes_required_headers() { - let request = build_local_websocket_request(local_client_url()) - .expect("local websocket request should be generated"); - - assert_eq!(request.uri().to_string(), local_client_url()); - assert_eq!( - request - .headers() - .get("host") - .and_then(|value| value.to_str().ok()), - Some("localhost") - ); - assert!(request.headers().contains_key("sec-websocket-key")); - assert_eq!( - request - .headers() - .get("sec-websocket-version") - .and_then(|value| value.to_str().ok()), - Some("13") - ); - assert_eq!( - request - .headers() - .get("upgrade") - .and_then(|value| value.to_str().ok()), - Some("websocket") - ); - assert_eq!( - request - .headers() - .get("connection") - .and_then(|value| value.to_str().ok()), - Some("Upgrade") - ); - } - - #[test] - fn cli_symlink_instructions_serialize_with_camel_case_keys() { - let value = serde_json::to_value(CliSymlinkInstructions { - title: "Add paseo to your shell".to_string(), - detail: "Create a symlink to the Paseo desktop executable.".to_string(), - commands: "sudo ...".to_string(), - }) - .expect("serializes"); - - assert_eq!( - value.get("title").and_then(|entry| entry.as_str()), - Some("Add paseo to your shell") - ); - assert_eq!( - value.get("commands").and_then(|entry| entry.as_str()), - Some("sudo ...") - ); - } - - #[cfg(unix)] - #[test] - #[ignore = "requires a running local daemon socket"] - fn connects_to_running_local_daemon_socket() { - let socket_path = std::env::var("PASEO_LOCAL_SOCKET_SMOKE_PATH") - .ok() - .filter(|value| !value.trim().is_empty()) - .map(PathBuf::from) - .or_else(|| dirs::home_dir().map(|home| home.join(".paseo").join("paseo.sock"))) - .expect("socket path should resolve"); - - assert!( - socket_path.exists(), - "socket path does not exist: {}", - socket_path.display() - ); - - tauri::async_runtime::block_on(async move { - let mut ws_stream = connect_local_socket(socket_path.clone()) - .await - .unwrap_or_else(|error| { - panic!( - "local socket websocket handshake failed for {}: {error}", - socket_path.display() - ) - }); - ws_stream.close(None).await.expect("close websocket stream"); - }); - } -} - -fn read_json_file Deserialize<'de>>(path: &Path) -> Result { - let raw = fs::read_to_string(path) - .map_err(|error| format!("Failed to read {}: {error}", path.display()))?; - serde_json::from_str::(&raw) - .map_err(|error| format!("Failed to parse {}: {error}", path.display())) -} - -fn bundled_runtime_root_from_resource_dir(resource_dir: &Path) -> Option { - for candidate in [ - resource_dir.join("resources").join("managed-runtime"), - resource_dir.join("managed-runtime"), - ] { - if candidate.exists() { - return Some(candidate); - } - } - - None -} - -fn bundled_runtime_root(app: &AppHandle) -> Result { - if let Ok(raw_resource_dir) = app.path().resource_dir() { - let resource_dir = dunce::simplified(&raw_resource_dir).to_path_buf(); - if let Some(candidate) = bundled_runtime_root_from_resource_dir(&resource_dir) { - log::info!("[runtime] found bundled runtime at {}", candidate.display()); - return Ok(candidate); - } - } else { - log::info!("[runtime] resource_dir() unavailable, resolving from executable path"); - } - - // Fallback: resolve symlinks and search relative to the real executable location. - // Handles CLI symlink invocation where Tauri can't resolve resource_dir. - if let Ok(canonical) = std::env::current_exe().and_then(|p| p.canonicalize()) { - let exe_dir = dunce::simplified(&canonical); - if let Some(exe_dir) = exe_dir.parent() { - let mut candidates = vec![exe_dir.join("resources"), exe_dir.to_path_buf()]; - if let Some(contents_dir) = exe_dir.parent() { - candidates.push(contents_dir.join("Resources")); - candidates.push(contents_dir.join("resources")); - } - for resource_dir in candidates { - if let Some(found) = bundled_runtime_root_from_resource_dir(&resource_dir) { - log::info!( - "[runtime] found bundled runtime (via exe fallback) at {}", - found.display() - ); - return Ok(found); - } - } - } - } - - log::error!("[runtime] no managed runtime found"); - Err("Managed runtime resources are not bundled with this desktop build.".to_string()) -} - -fn load_bundled_runtime_pointer( - app: &AppHandle, -) -> Result<(PathBuf, BundledRuntimePointer), String> { - let root = bundled_runtime_root(app)?; - let pointer_path = root.join("current-runtime.json"); - let pointer = read_json_file::(&pointer_path)?; - Ok((root, pointer)) -} - -fn load_runtime_manifest(runtime_root: &Path) -> Result { - read_json_file::(&runtime_root.join("runtime-manifest.json")) -} - -fn tail_log(path: &Path, max_lines: usize) -> String { - let raw = match fs::read_to_string(path) { - Ok(value) => value, - Err(_) => return String::new(), - }; - let mut lines = raw.lines().rev().take(max_lines).collect::>(); - lines.reverse(); - lines.join("\n") -} - -fn to_stdio_message(input: Option<&str>) -> String { - input.unwrap_or_default().trim().to_string() -} - -fn cli_command( - runtime_root: &Path, - manifest: &ManagedRuntimeManifest, - args: &[&str], -) -> Result { - let node = runtime_root.join(&manifest.node_relative_path); - let cli = runtime_root.join(&manifest.cli_entrypoint_relative_path); - if !node.exists() { - log::error!("[cli] bundled Node missing at {}", node.display()); - return Err(format!( - "Bundled Node runtime is missing at {}", - node.display() - )); - } - if !cli.exists() { - log::error!("[cli] bundled CLI missing at {}", cli.display()); - return Err(format!( - "Bundled CLI entrypoint is missing at {}", - cli.display() - )); - } - log::info!( - "[cli] node={} cli={} args={:?}", - node.display(), - cli.display(), - args - ); - let mut command = Command::new(node); - command.arg(cli); - command.args(args); - #[cfg(windows)] - { - use std::os::windows::process::CommandExt; - command.creation_flags(0x08000000); // CREATE_NO_WINDOW - } - Ok(command) -} - -fn shell_single_quote(value: &str) -> String { - format!("'{}'", value.replace('\'', r"'\''")) -} - -#[cfg(windows)] -fn powershell_double_quote(value: &str) -> String { - value.replace('`', "``").replace('"', "`\"") -} - -fn outer_cli_link_path() -> Result { - #[cfg(target_os = "macos")] - { - return Ok(PathBuf::from("/usr/local/bin").join(CLI_LINK_NAME)); - } - #[cfg(all(not(target_os = "macos"), not(windows)))] - { - return Ok(PathBuf::from("/usr/local/bin").join(CLI_LINK_NAME)); - } - #[cfg(windows)] - { - let local_app_data = dirs::data_local_dir().ok_or_else(|| { - "Failed to resolve LocalAppData for CLI symlink instructions.".to_string() - })?; - Ok(local_app_data - .join("Microsoft") - .join("WinGet") - .join("Links") - .join(CLI_LINK_WINDOWS_NAME)) - } -} - -fn desktop_cli_source_path() -> Result { - let current_exe = std::env::current_exe() - .map_err(|error| format!("Failed to resolve desktop executable path: {error}"))?; - Ok(dunce::simplified(¤t_exe).to_path_buf()) -} - -fn cli_symlink_instructions_internal() -> Result { - let outer_link = outer_cli_link_path()?; - #[cfg(windows)] - { - let desktop_executable = desktop_cli_source_path()?; - let target_dir = outer_link - .parent() - .ok_or_else(|| "CLI symlink target is missing a parent directory.".to_string())?; - return Ok(CliSymlinkInstructions { - title: "Add paseo to your shell".to_string(), - detail: "Create a symlink to the Paseo desktop executable.".to_string(), - commands: format!( - "$target = \"{}\"\nNew-Item -ItemType Directory -Force -Path \"{}\" | Out-Null\nif (Test-Path $target) {{ Remove-Item -Path $target -Force }}\nNew-Item -ItemType SymbolicLink -Path $target -Target \"{}\" | Out-Null\n", - powershell_double_quote(outer_link.to_string_lossy().as_ref()), - powershell_double_quote(target_dir.to_string_lossy().as_ref()), - powershell_double_quote(desktop_executable.to_string_lossy().as_ref()) - ), - }); - } - #[cfg(not(windows))] - { - let desktop_executable = desktop_cli_source_path()?; - Ok(CliSymlinkInstructions { - title: "Add paseo to your shell".to_string(), - detail: "Create a symlink to the Paseo desktop executable.".to_string(), - commands: format!( - "sudo mkdir -p {target_dir}\nsudo ln -sf {source_path} {target_path}\n", - target_dir = shell_single_quote( - outer_link - .parent() - .ok_or_else( - || "CLI symlink target is missing a parent directory.".to_string() - )? - .to_string_lossy() - .as_ref() - ), - target_path = shell_single_quote(outer_link.to_string_lossy().as_ref()), - source_path = shell_single_quote(desktop_executable.to_string_lossy().as_ref()) - ), - }) - } -} - -fn ensure_runtime_ready_internal(app: &AppHandle) -> Result { - log::info!("[runtime] ensuring runtime is ready"); - let (bundled_root, pointer) = load_bundled_runtime_pointer(app)?; - let runtime_root = bundled_root.join(&pointer.relative_root); - let manifest = load_runtime_manifest(&runtime_root)?; - log::info!( - "[runtime] manifest: id={} version={}", - manifest.runtime_id, - manifest.runtime_version - ); - - Ok(ManagedRuntimeStatus { - runtime_id: manifest.runtime_id, - runtime_version: manifest.runtime_version, - runtime_root: runtime_root.to_string_lossy().into_owned(), - }) -} - -fn run_cli_json_command( - runtime_root: &Path, - manifest: &ManagedRuntimeManifest, - args: &[&str], -) -> Result { - log::info!("[cli] running: {:?}", args); - let output = cli_command(runtime_root, manifest, args)? - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .map_err(|error| { - log::error!("[cli] failed to spawn: {error}"); - format!("Failed to run bundled CLI: {error}") - })?; - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - log::error!( - "[cli] exit={} stderr={}", - output.status.code().unwrap_or(-1), - stderr.trim() - ); - return Err(format!( - "Bundled CLI failed (exit {}): {}", - output.status.code().unwrap_or(-1), - stderr.trim() - )); - } - let stdout = String::from_utf8_lossy(&output.stdout); - log::info!("[cli] success, parsing JSON output"); - serde_json::from_str(stdout.trim()).map_err(|error| { - log::error!("[cli] JSON parse error: {error}; stdout={}", stdout.trim()); - format!( - "Failed to parse bundled CLI JSON output: {error}; stdout={}", - stdout.trim() - ) - }) -} - -fn run_cli_passthrough_command( - runtime_root: &Path, - manifest: &ManagedRuntimeManifest, - args: &[String], -) -> Result { - log::info!("[cli] passthrough: {:?}", args); - let status = cli_command(runtime_root, manifest, &[])? - .args(args) - .status() - .map_err(|error| format!("Failed to run bundled CLI: {error}"))?; - Ok(status.code().unwrap_or(1)) -} - -fn managed_daemon_status_internal(app: &AppHandle) -> Result { - let status = ensure_runtime_ready_internal(app)?; - let runtime_root = PathBuf::from(&status.runtime_root); - let manifest = load_runtime_manifest(&runtime_root)?; - let value = run_cli_json_command(&runtime_root, &manifest, &["daemon", "status", "--json"])?; - let daemon_status: CliDaemonStatus = serde_json::from_value(value) - .map_err(|error| format!("Failed to parse managed daemon status: {error}"))?; - - Ok(ManagedDaemonStatus { - runtime_id: manifest.runtime_id, - runtime_version: manifest.runtime_version, - server_id: daemon_status.server_id.unwrap_or_default(), - status: daemon_status.status, - listen: daemon_status.listen, - hostname: daemon_status.hostname, - pid: daemon_status.pid, - home: daemon_status.home, - }) -} - -fn start_managed_daemon_internal(app: &AppHandle) -> Result { - let t0 = std::time::Instant::now(); - log::info!("[daemon] starting managed daemon"); - let status = ensure_runtime_ready_internal(app)?; - log::info!("[daemon] runtime ready ({}ms)", t0.elapsed().as_millis()); - let runtime_root = PathBuf::from(&status.runtime_root); - let manifest = load_runtime_manifest(&runtime_root)?; - let output = cli_command(&runtime_root, &manifest, &["start"])? - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .map_err(|error| { - log::error!("[daemon] failed to spawn: {error}"); - format!("Failed to launch managed daemon: {error}") - })?; - if !output.status.success() { - let stderr = to_stdio_message(Some(&String::from_utf8_lossy(&output.stderr))); - log::error!( - "[daemon] start failed (exit {}): {}", - output.status.code().unwrap_or(-1), - stderr - ); - return Err(format!( - "Managed daemon start failed (exit {}): {}", - output.status.code().unwrap_or(-1), - stderr - )); - } - log::info!("[daemon] start command succeeded ({}ms), waiting for daemon to be ready", t0.elapsed().as_millis()); - for attempt in 0..150 { - let daemon_status = managed_daemon_status_internal(app)?; - log::info!( - "[daemon] poll attempt {}: status={:?} server_id={:?} listen={:?} pid={:?}", - attempt + 1, - daemon_status.status, - daemon_status.server_id, - daemon_status.listen, - daemon_status.pid - ); - if daemon_status.status == "running" && !daemon_status.server_id.trim().is_empty() { - log::info!( - "[daemon] ready after {} attempts, {}ms (pid={:?})", - attempt + 1, - t0.elapsed().as_millis(), - daemon_status.pid - ); - return Ok(daemon_status); - } - std::thread::sleep(std::time::Duration::from_millis(200)); - } - log::warn!("[daemon] timed out waiting for daemon to become ready ({}ms)", t0.elapsed().as_millis()); - managed_daemon_status_internal(app) -} - -fn stop_managed_daemon_internal(app: &AppHandle) -> Result { - log::info!("[daemon] stopping managed daemon"); - let status = ensure_runtime_ready_internal(app)?; - let runtime_root = PathBuf::from(&status.runtime_root); - let manifest = load_runtime_manifest(&runtime_root)?; - let output = cli_command(&runtime_root, &manifest, &["daemon", "stop", "--json"])? - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .map_err(|error| { - log::error!("[daemon] failed to spawn stop command: {error}"); - format!("Failed to stop managed daemon: {error}") - })?; - if !output.status.success() { - let stderr = to_stdio_message(Some(&String::from_utf8_lossy(&output.stderr))); - log::error!( - "[daemon] stop failed (exit {}): {}", - output.status.code().unwrap_or(-1), - stderr - ); - return Err(format!( - "Managed daemon stop failed (exit {}): {}", - output.status.code().unwrap_or(-1), - stderr - )); - } - log::info!("[daemon] stop command succeeded"); - managed_daemon_status_internal(app) -} - -fn restart_managed_daemon_internal(app: &AppHandle) -> Result { - log::info!("[daemon] restarting managed daemon"); - let status = ensure_runtime_ready_internal(app)?; - let runtime_root = PathBuf::from(&status.runtime_root); - let manifest = load_runtime_manifest(&runtime_root)?; - let output = cli_command(&runtime_root, &manifest, &["daemon", "restart", "--json"])? - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .map_err(|error| { - log::error!("[daemon] failed to spawn restart command: {error}"); - format!("Failed to restart managed daemon: {error}") - })?; - if !output.status.success() { - let stderr = to_stdio_message(Some(&String::from_utf8_lossy(&output.stderr))); - log::error!( - "[daemon] restart failed (exit {}): {}", - output.status.code().unwrap_or(-1), - stderr - ); - return Err(format!( - "Managed daemon restart failed (exit {}): {}", - output.status.code().unwrap_or(-1), - stderr - )); - } - log::info!("[daemon] restart command succeeded"); - managed_daemon_status_internal(app) -} - -fn update_managed_tcp_settings_internal( - app: &AppHandle, - _settings: ManagedTcpSettings, -) -> Result { - let _ = app; - Err("Managed daemon TCP settings are no longer configurable from desktop.".to_string()) -} - -#[tauri::command] -pub async fn managed_runtime_status(app: AppHandle) -> Result { - tauri::async_runtime::spawn_blocking(move || ensure_runtime_ready_internal(&app)) - .await - .map_err(|error| format!("Managed runtime status task failed: {error}"))? -} - -#[tauri::command] -pub async fn managed_daemon_status(app: AppHandle) -> Result { - tauri::async_runtime::spawn_blocking(move || managed_daemon_status_internal(&app)) - .await - .map_err(|error| format!("Managed daemon status task failed: {error}"))? -} - -#[tauri::command] -pub async fn start_managed_daemon(app: AppHandle) -> Result { - tauri::async_runtime::spawn_blocking(move || start_managed_daemon_internal(&app)) - .await - .map_err(|error| format!("Managed daemon start task failed: {error}"))? -} - -#[tauri::command] -pub async fn stop_managed_daemon(app: AppHandle) -> Result { - tauri::async_runtime::spawn_blocking(move || stop_managed_daemon_internal(&app)) - .await - .map_err(|error| format!("Managed daemon stop task failed: {error}"))? -} - -#[tauri::command] -pub async fn restart_managed_daemon(app: AppHandle) -> Result { - tauri::async_runtime::spawn_blocking(move || restart_managed_daemon_internal(&app)) - .await - .map_err(|error| format!("Managed daemon restart task failed: {error}"))? -} - -#[tauri::command] -pub async fn cli_symlink_instructions(app: AppHandle) -> Result { - tauri::async_runtime::spawn_blocking(move || { - let _ = app; - cli_symlink_instructions_internal() - }) - .await - .map_err(|error| format!("CLI symlink instructions task failed: {error}"))? -} - -#[tauri::command] -pub async fn managed_daemon_logs(app: AppHandle) -> Result { - tauri::async_runtime::spawn_blocking(move || { - let runtime_status = ensure_runtime_ready_internal(&app)?; - let runtime_root = PathBuf::from(&runtime_status.runtime_root); - let manifest = load_runtime_manifest(&runtime_root)?; - let value = - run_cli_json_command(&runtime_root, &manifest, &["daemon", "status", "--json"])?; - let daemon_status: CliDaemonStatus = serde_json::from_value(value) - .map_err(|error| format!("Failed to parse managed daemon status for logs: {error}"))?; - let log_path = PathBuf::from(&daemon_status.log_path); - Ok(ManagedDaemonLogs { - log_path: log_path.to_string_lossy().into_owned(), - contents: tail_log(&log_path, 400), - }) - }) - .await - .map_err(|error| format!("Managed daemon logs task failed: {error}"))? -} - -#[tauri::command] -pub async fn managed_daemon_pairing(app: AppHandle) -> Result { - tauri::async_runtime::spawn_blocking(move || { - let status = ensure_runtime_ready_internal(&app)?; - let runtime_root = PathBuf::from(&status.runtime_root); - let manifest = load_runtime_manifest(&runtime_root)?; - let value = run_cli_json_command(&runtime_root, &manifest, &["daemon", "pair", "--json"])?; - serde_json::from_value::(value) - .map_err(|error| format!("Failed to parse managed pairing offer: {error}")) - }) - .await - .map_err(|error| format!("Managed daemon pairing task failed: {error}"))? -} - -#[tauri::command] -pub async fn update_managed_daemon_tcp_settings( - app: AppHandle, - settings: ManagedTcpSettings, -) -> Result { - tauri::async_runtime::spawn_blocking(move || { - update_managed_tcp_settings_internal(&app, settings) - }) - .await - .map_err(|error| format!("Managed daemon TCP settings task failed: {error}"))? -} - -pub fn run_managed_cli_from_current_process(args: Vec) -> Result { - let current_exe = std::env::current_exe() - .and_then(|p| p.canonicalize()) - .map_err(|error| format!("Failed to resolve desktop executable path: {error}"))?; - let current_exe = dunce::simplified(¤t_exe).to_path_buf(); - let exe_dir = current_exe - .parent() - .ok_or_else(|| "Desktop executable path is missing a parent directory.".to_string())?; - let mut resource_dirs = vec![exe_dir.join("resources"), exe_dir.to_path_buf()]; - if let Some(contents_dir) = exe_dir.parent() { - resource_dirs.push(contents_dir.join("Resources")); - resource_dirs.push(contents_dir.join("resources")); - resource_dirs.push(contents_dir.join("lib").join("Paseo").join("resources")); - } - let bundled_root = resource_dirs - .into_iter() - .find_map(|resource_dir| bundled_runtime_root_from_resource_dir(&resource_dir)) - .ok_or_else(|| { - "Managed runtime resources are not bundled with this desktop build.".to_string() - })?; - let pointer = - read_json_file::(&bundled_root.join("current-runtime.json"))?; - let runtime_root = bundled_root.join(pointer.relative_root); - let manifest = load_runtime_manifest(&runtime_root)?; - run_cli_passthrough_command(&runtime_root, &manifest, &args) -} - -async fn spawn_local_transport_session( - app: AppHandle, - transport_state: State<'_, LocalTransportState>, - session_id: String, - ws_stream: tokio_tungstenite::WebSocketStream, -) -> Result -where - S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static, -{ - let (mut write, mut read) = ws_stream.split(); - let (sender, mut receiver) = mpsc::unbounded_channel::(); - transport_state - .sessions - .lock() - .map_err(|_| "Local transport session lock poisoned.".to_string())? - .insert(session_id.clone(), LocalTransportSession { sender }); - - let app_for_read = app.clone(); - let app_for_write = app.clone(); - let sessions_for_read = Arc::clone(&transport_state.sessions); - let read_session_id = session_id.clone(); - tauri::async_runtime::spawn(async move { - let _ = app_for_read.emit( - LOCAL_TRANSPORT_EVENT_NAME, - LocalTransportEvent { - session_id: read_session_id.clone(), - kind: "open".to_string(), - text: None, - binary_base64: None, - code: None, - reason: None, - error: None, - }, - ); - - while let Some(message) = read.next().await { - match message { - Ok(Message::Text(text)) => { - let _ = app_for_read.emit( - LOCAL_TRANSPORT_EVENT_NAME, - LocalTransportEvent { - session_id: read_session_id.clone(), - kind: "message".to_string(), - text: Some(text.to_string()), - binary_base64: None, - code: None, - reason: None, - error: None, - }, - ); - } - Ok(Message::Binary(bytes)) => { - let _ = app_for_read.emit( - LOCAL_TRANSPORT_EVENT_NAME, - LocalTransportEvent { - session_id: read_session_id.clone(), - kind: "message".to_string(), - text: None, - binary_base64: Some(BASE64_STANDARD.encode(bytes)), - code: None, - reason: None, - error: None, - }, - ); - } - Ok(Message::Close(frame)) => { - let _ = app_for_read.emit( - LOCAL_TRANSPORT_EVENT_NAME, - LocalTransportEvent { - session_id: read_session_id.clone(), - kind: "close".to_string(), - text: None, - binary_base64: None, - code: frame.as_ref().map(|value| value.code.into()), - reason: frame.as_ref().map(|value| value.reason.to_string()), - error: None, - }, - ); - break; - } - Ok(Message::Ping(_)) | Ok(Message::Pong(_)) => {} - Ok(Message::Frame(_)) => {} - Err(error) => { - let _ = app_for_read.emit( - LOCAL_TRANSPORT_EVENT_NAME, - LocalTransportEvent { - session_id: read_session_id.clone(), - kind: "error".to_string(), - text: None, - binary_base64: None, - code: None, - reason: None, - error: Some(error.to_string()), - }, - ); - break; - } - } - } - - if let Ok(mut sessions) = sessions_for_read.lock() { - sessions.remove(&read_session_id); - } - }); - - let sessions_for_write = Arc::clone(&transport_state.sessions); - let write_session_id = session_id.clone(); - tauri::async_runtime::spawn(async move { - while let Some(message) = receiver.recv().await { - if write.send(message).await.is_err() { - let _ = app_for_write.emit( - LOCAL_TRANSPORT_EVENT_NAME, - LocalTransportEvent { - session_id: write_session_id.clone(), - kind: "error".to_string(), - text: None, - binary_base64: None, - code: None, - reason: None, - error: Some("Local transport write failed.".to_string()), - }, - ); - break; - } - } - if let Ok(mut sessions) = sessions_for_write.lock() { - sessions.remove(&write_session_id); - } - }); - - Ok(session_id) -} - -#[tauri::command] -pub async fn open_local_daemon_transport( - app: AppHandle, - transport_state: State<'_, LocalTransportState>, - transport_type: String, - transport_path: String, -) -> Result { - let session_id = transport_state.alloc_session_id(); - log::info!( - "[transport] opening session {} type={} path={}", - session_id, - transport_type, - transport_path - ); - let _ = app; - match transport_type.as_str() { - "pipe" => { - #[cfg(windows)] - { - let ws_stream = connect_local_pipe(transport_path) - .await - .map_err(|error| format!("Failed to connect to local daemon pipe: {error}"))?; - spawn_local_transport_session(app, transport_state, session_id, ws_stream).await - } - #[cfg(not(windows))] - { - Err(tokio_tungstenite::tungstenite::Error::Io(io::Error::new( - io::ErrorKind::Unsupported, - "Local pipe transport is only available on Windows.", - )) - .to_string()) - } - } - "socket" => { - #[cfg(unix)] - { - let ws_stream = connect_local_socket(PathBuf::from(transport_path)) - .await - .map_err(|error| { - format!("Failed to connect to local daemon socket: {error}") - })?; - spawn_local_transport_session(app, transport_state, session_id, ws_stream).await - } - #[cfg(not(unix))] - { - Err(tokio_tungstenite::tungstenite::Error::Io(io::Error::new( - io::ErrorKind::Unsupported, - "Local socket transport is only available on Unix platforms.", - )) - .to_string()) - } - } - other => Err(format!("Unsupported local transport type: {other}")), - } -} - -#[tauri::command] -pub async fn send_local_daemon_transport_message( - transport_state: State<'_, LocalTransportState>, - session_id: String, - text: Option, - binary_base64: Option, -) -> Result<(), String> { - let sessions = transport_state - .sessions - .lock() - .map_err(|_| "Local transport session lock poisoned.".to_string())?; - let session = sessions - .get(&session_id) - .ok_or_else(|| format!("Local transport session not found: {session_id}"))?; - if let Some(text) = text { - session - .sender - .send(Message::Text(text.into())) - .map_err(|_| "Local transport session is closed.".to_string())?; - return Ok(()); - } - if let Some(binary_base64) = binary_base64 { - let bytes = BASE64_STANDARD - .decode(binary_base64.as_bytes()) - .map_err(|error| format!("Failed to decode local transport payload: {error}"))?; - session - .sender - .send(Message::Binary(bytes.into())) - .map_err(|_| "Local transport session is closed.".to_string())?; - return Ok(()); - } - Err("Local transport send requires text or binary payload.".to_string()) -} - -#[tauri::command] -pub async fn close_local_daemon_transport( - transport_state: State<'_, LocalTransportState>, - session_id: String, -) -> Result<(), String> { - let mut sessions = transport_state - .sessions - .lock() - .map_err(|_| "Local transport session lock poisoned.".to_string())?; - let session = sessions - .remove(&session_id) - .ok_or_else(|| format!("Local transport session not found: {session_id}"))?; - session - .sender - .send(Message::Close(None)) - .map_err(|_| "Local transport session is already closed.".to_string())?; - Ok(()) -} diff --git a/packages/desktop/src-tauri/tauri.conf.json b/packages/desktop/src-tauri/tauri.conf.json deleted file mode 100644 index 2a7ff6e5e..000000000 --- a/packages/desktop/src-tauri/tauri.conf.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", - "productName": "Paseo", - "version": "0.1.30", - "identifier": "dev.paseo.desktop", - "build": { - "frontendDist": "../../app/dist", - "devUrl": "http://localhost:8081" - }, - "app": { - "withGlobalTauri": true, - "windows": [ - { - "title": "Paseo", - "width": 1200, - "height": 800, - "minWidth": 800, - "minHeight": 600, - "resizable": true, - "fullscreen": false, - "titleBarStyle": "Overlay", - "hiddenTitle": true, - "trafficLightPosition": { - "x": 16, - "y": 22 - } - } - ], - "security": { - "csp": null - } - }, - "bundle": { - "active": true, - "createUpdaterArtifacts": true, - "resources": [ - "resources/**/*" - ], - "targets": "all", - "windows": { - "nsis": { - "installerHooks": "./installer-hooks.nsh" - } - }, - "icon": [ - "icons/32x32.png", - "icons/128x128.png", - "icons/128x128@2x.png", - "icons/icon.icns", - "icons/icon.ico" - ], - "macOS": { - "entitlements": "Entitlements.plist", - "infoPlist": "Info.plist" - } - }, - "plugins": { - "updater": { - "active": true, - "endpoints": [ - "https://github.com/getpaseo/paseo/releases/latest/download/latest.json" - ], - "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEFGNzE3OTlBNEVCM0U4ODgKUldTSTZMTk9tbmx4cnh6OStYMGxFelpnRVJwTklXMlpyM3FQTXBVbFc5NUpFNmZMbUdjRjkyN3gK" - } - } -} diff --git a/packages/desktop/src/daemon/daemon-manager.ts b/packages/desktop/src/daemon/daemon-manager.ts new file mode 100644 index 000000000..1067a549b --- /dev/null +++ b/packages/desktop/src/daemon/daemon-manager.ts @@ -0,0 +1,569 @@ +import { spawn, type ChildProcess } from "node:child_process"; +import { existsSync, readFileSync } from "node:fs"; +import path from "node:path"; +import { app, ipcMain } from "electron"; +import { + loadConfig, + resolvePaseoHome, + getOrCreateServerId, +} from "@getpaseo/server"; +import { + copyAttachmentFileToManagedStorage, + deleteManagedAttachmentFile, + garbageCollectManagedAttachmentFiles, + readManagedFileBase64, + writeAttachmentBase64, +} from "../features/attachments.js"; +import { + checkForAppUpdate, + downloadAndInstallUpdate, +} from "../features/auto-updater.js"; +import { + openLocalTransportSession, + sendLocalTransportMessage, + closeLocalTransportSession, +} from "./local-transport.js"; +import { + createElectronNodeEnv, + resolveDaemonRunnerEntrypoint, +} from "./runtime-paths.js"; + +const DAEMON_LOG_FILENAME = "daemon.log"; +const DAEMON_PID_FILENAME = "paseo.pid"; +const PID_POLL_INTERVAL_MS = 100; +const STARTUP_POLL_INTERVAL_MS = 200; +const STARTUP_POLL_MAX_ATTEMPTS = 150; +const STOP_TIMEOUT_MS = 15_000; +const KILL_TIMEOUT_MS = 3_000; +const DETACHED_STARTUP_GRACE_MS = 1200; +const DEFAULT_ELECTRON_DEV_SERVER_URL = "http://localhost:8081"; + +type DesktopDaemonState = "starting" | "running" | "stopped" | "errored"; + +type DesktopDaemonStatus = { + serverId: string; + status: DesktopDaemonState; + listen: string; + hostname: string | null; + pid: number | null; + home: string; + error: string | null; +}; + +type DesktopDaemonLogs = { + logPath: string; + contents: string; +}; + +type DesktopPairingOffer = { + relayEnabled: boolean; + url: string | null; + qr: string | null; +}; + +type CliSymlinkInstructions = { + title: string; + detail: string; + commands: string; +}; + +type DesktopCommandHandler = (args?: Record) => Promise | unknown; + +// --------------------------------------------------------------------------- +// Utilities +// --------------------------------------------------------------------------- + +function getPaseoHome(): string { + return resolvePaseoHome(process.env); +} + +function pidFilePath(): string { + return path.join(getPaseoHome(), DAEMON_PID_FILENAME); +} + +function logFilePath(): string { + return path.join(getPaseoHome(), DAEMON_LOG_FILENAME); +} + +function isProcessRunning(pid: number): boolean { + try { + process.kill(pid, 0); + return true; + } catch (err) { + if (typeof err === "object" && err !== null && "code" in err && err.code === "EPERM") { + return true; + } + return false; + } +} + +function signalProcessSafely(pid: number, signal: NodeJS.Signals): boolean { + if (!Number.isInteger(pid) || pid <= 1 || pid === process.pid) return false; + try { + process.kill(pid, signal); + return true; + } catch (err) { + if (typeof err === "object" && err !== null && "code" in err) { + if (err.code === "ESRCH") return false; + if (err.code === "EPERM") return true; + } + throw err; + } +} + +function signalProcessGroupSafely(pid: number, signal: NodeJS.Signals): boolean { + if (!Number.isInteger(pid) || pid <= 1 || pid === process.pid) return false; + if (process.platform === "win32") return signalProcessSafely(pid, signal); + try { + process.kill(-pid, signal); + return true; + } catch (err) { + if (typeof err === "object" && err !== null && "code" in err) { + if (err.code === "ESRCH") return signalProcessSafely(pid, signal); + if (err.code === "EPERM") return true; + } + throw err; + } +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +async function waitForPidExit(pid: number, timeoutMs: number): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if (!isProcessRunning(pid)) return true; + await sleep(PID_POLL_INTERVAL_MS); + } + return !isProcessRunning(pid); +} + +function tailFile(filePath: string, lines = 50): string { + try { + const content = readFileSync(filePath, "utf-8"); + return content.split("\n").filter(Boolean).slice(-lines).join("\n"); + } catch { + return ""; + } +} + +function buildDesktopDaemonCorsOriginsEnv(): string | undefined { + const origins = new Set( + (process.env.PASEO_CORS_ORIGINS ?? "") + .split(",") + .map((value) => value.trim()) + .filter((value) => value.length > 0) + ); + + const devServerUrl = process.env.EXPO_DEV_URL ?? DEFAULT_ELECTRON_DEV_SERVER_URL; + try { + const parsed = new URL(devServerUrl); + origins.add(parsed.origin); + + if (parsed.hostname === "localhost") { + origins.add(`${parsed.protocol}//127.0.0.1${parsed.port ? `:${parsed.port}` : ""}`); + } else if (parsed.hostname === "127.0.0.1") { + origins.add(`${parsed.protocol}//localhost${parsed.port ? `:${parsed.port}` : ""}`); + } + } catch { + // Ignore malformed dev server URLs and preserve any explicit env configuration. + } + + return origins.size > 0 ? Array.from(origins).join(",") : undefined; +} + +function toTrimmedString(value: unknown): string | null { + if (typeof value !== "string") { + return null; + } + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : null; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function resolveTcpHostFromListen(listen: string): string | null { + const normalized = listen.trim(); + if (!normalized) { + return null; + } + + if ( + normalized.startsWith("/") || + normalized.startsWith("unix://") || + normalized.startsWith("pipe://") || + normalized.startsWith("\\\\.\\pipe\\") + ) { + return null; + } + + if (/^\d+$/.test(normalized)) { + return `127.0.0.1:${normalized}`; + } + + if (normalized.includes(":")) { + return normalized; + } + + return null; +} + +function buildDaemonHttpBaseUrl(listen: string): string | null { + const endpoint = resolveTcpHostFromListen(listen); + if (!endpoint) { + return null; + } + return new URL(`http://${endpoint}`).toString().replace(/\/$/, ""); +} + +function resolveDesktopAppVersion(): string { + if (app.isPackaged) { + return app.getVersion(); + } + + try { + const packageJsonPath = path.join(__dirname, "..", "..", "package.json"); + const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as { + version?: unknown; + }; + if (typeof pkg.version === "string" && pkg.version.trim().length > 0) { + return pkg.version.trim(); + } + } catch { + // Fall back to Electron's default version if the package metadata is unavailable. + } + + return app.getVersion(); +} + +// --------------------------------------------------------------------------- +// Daemon lifecycle +// --------------------------------------------------------------------------- + +function resolveStatus(): DesktopDaemonStatus { + const home = getPaseoHome(); + const config = loadConfig(home, { env: process.env }); + const pidPath = pidFilePath(); + + let pid: number | null = null; + let hostname: string | null = null; + let listen: string = config.listen; + + try { + if (existsSync(pidPath)) { + const parsed = JSON.parse(readFileSync(pidPath, "utf-8")) as Record; + const pidValue = parsed.pid; + if (typeof pidValue === "number" && Number.isInteger(pidValue) && pidValue > 0) { + pid = pidValue; + hostname = typeof parsed.hostname === "string" ? parsed.hostname : null; + const pidListen = + typeof parsed.listen === "string" + ? parsed.listen + : typeof parsed.sockPath === "string" + ? (parsed.sockPath as string) + : null; + if (pidListen) listen = pidListen; + } + } + } catch { + // PID file missing or malformed — treat as stopped. + } + + const running = pid !== null && isProcessRunning(pid); + + let serverId = ""; + try { + serverId = getOrCreateServerId(home); + } catch { + // Ignore — server-id may not exist yet. + } + + return { + serverId, + status: running ? "running" : "stopped", + listen, + hostname: running ? hostname : null, + pid: running ? pid : null, + home, + error: null, + }; +} + +async function startDaemon(): Promise { + const current = resolveStatus(); + if (current.status === "running") return current; + + const home = getPaseoHome(); + const daemonRunner = resolveDaemonRunnerEntrypoint(); + const corsOrigins = buildDesktopDaemonCorsOriginsEnv(); + + const child: ChildProcess = spawn( + process.execPath, + [...daemonRunner.execArgv, daemonRunner.entryPath], + { + detached: true, + env: createElectronNodeEnv({ + ...process.env, + PASEO_HOME: home, + ...(corsOrigins ? { PASEO_CORS_ORIGINS: corsOrigins } : {}), + }), + stdio: ["ignore", "ignore", "ignore"], + } + ); + + child.unref(); + + // Wait for process to survive the grace period + const exitedEarly = await new Promise((resolve) => { + let settled = false; + const finish = (value: boolean) => { + if (settled) return; + settled = true; + resolve(value); + }; + + const timer = setTimeout(() => finish(false), DETACHED_STARTUP_GRACE_MS); + + child.once("error", () => { + clearTimeout(timer); + finish(true); + }); + child.once("exit", () => { + clearTimeout(timer); + finish(true); + }); + }); + + if (exitedEarly) { + const logs = tailFile(logFilePath(), 15); + throw new Error( + `Daemon failed to start.${logs ? `\n\nRecent logs:\n${logs}` : ""}` + ); + } + + // Poll for PID file with server ID + for (let attempt = 0; attempt < STARTUP_POLL_MAX_ATTEMPTS; attempt++) { + const status = resolveStatus(); + if (status.status === "running" && status.serverId) return status; + await sleep(STARTUP_POLL_INTERVAL_MS); + } + + return resolveStatus(); +} + +async function stopDaemon(): Promise { + const status = resolveStatus(); + if (status.status !== "running" || !status.pid) return status; + + const pid = status.pid; + signalProcessSafely(pid, "SIGTERM"); + + let stopped = await waitForPidExit(pid, STOP_TIMEOUT_MS); + if (!stopped) { + signalProcessGroupSafely(pid, "SIGKILL"); + stopped = await waitForPidExit(pid, KILL_TIMEOUT_MS); + } + + if (!stopped) { + throw new Error(`Timed out waiting for daemon PID ${pid} to stop`); + } + + return resolveStatus(); +} + +async function restartDaemon(): Promise { + await stopDaemon(); + return startDaemon(); +} + +function getDaemonLogs(): DesktopDaemonLogs { + const logPath = logFilePath(); + return { + logPath, + contents: tailFile(logPath, 100), + }; +} + +async function getDaemonPairing(): Promise { + const status = resolveStatus(); + if (status.status !== "running") { + return { + relayEnabled: false, + url: null, + qr: null, + }; + } + + try { + const baseUrl = buildDaemonHttpBaseUrl(status.listen); + if (!baseUrl) { + throw new Error(`Daemon listen target is not a TCP endpoint: ${status.listen}`); + } + + const response = await fetch(`${baseUrl}/pairing`); + if (!response.ok) { + throw new Error(`Daemon pairing request failed with ${response.status}`); + } + + const payload = (await response.json()) as unknown; + if (!isRecord(payload)) { + throw new Error("Daemon pairing response was not an object."); + } + + return { + relayEnabled: payload.relayEnabled === true, + url: toTrimmedString(payload.url), + qr: toTrimmedString(payload.qr), + }; + } catch { + return { + relayEnabled: false, + url: null, + qr: null, + }; + } +} + +async function getLocalDaemonVersion(): Promise<{ + version: string | null; + error: string | null; +}> { + const status = resolveStatus(); + if (status.status !== "running") { + return { + version: null, + error: "Daemon is not running.", + }; + } + + const baseUrl = buildDaemonHttpBaseUrl(status.listen); + if (!baseUrl) { + return { version: null, error: `Daemon listen target is not a TCP endpoint: ${status.listen}` }; + } + + try { + const response = await fetch(`${baseUrl}/api/status`); + if (!response.ok) { + return { version: null, error: `Daemon status request failed with ${response.status}` }; + } + const payload = (await response.json()) as Record; + const version = typeof payload.version === "string" ? payload.version.trim() : null; + return { + version: version && version.length > 0 ? version : null, + error: version ? null : "Running daemon did not report a version.", + }; + } catch (error) { + return { + version: null, + error: error instanceof Error ? error.message : String(error), + }; + } +} + +async function resolveCurrentUpdateVersion(): Promise { + const daemonVersion = await getLocalDaemonVersion(); + if (daemonVersion.version) { + return daemonVersion.version; + } + return resolveDesktopAppVersion(); +} + +function getCliSymlinkInstructions(): CliSymlinkInstructions { + const electronExePath = app.getPath("exe"); + const cliShimFilename = process.platform === "win32" ? "paseo.cmd" : "paseo"; + + if (process.platform === "darwin") { + const appBundle = electronExePath.replace(/\/Contents\/MacOS\/.+$/, ""); + const cliPath = path.join(appBundle, "Contents", "MacOS", cliShimFilename); + return { + title: "Add paseo to your shell", + detail: "Create a symlink to the bundled Paseo CLI shim.", + commands: `sudo ln -sf "${cliPath}" /usr/local/bin/paseo`, + }; + } + + if (process.platform === "win32") { + return { + title: "Add paseo to your PATH", + detail: "Add the Paseo installation directory to your system PATH so paseo.cmd is available.", + commands: `setx PATH "%PATH%;${path.dirname(electronExePath)}"`, + }; + } + + // Linux + return { + title: "Add paseo to your shell", + detail: "Create a symlink to the bundled Paseo CLI shim.", + commands: `sudo ln -sf "${path.join(path.dirname(electronExePath), cliShimFilename)}" /usr/local/bin/paseo`, + }; +} + +// --------------------------------------------------------------------------- +// IPC registration +// --------------------------------------------------------------------------- + +export function createDaemonCommandHandlers(): Record { + return { + desktop_daemon_status: () => resolveStatus(), + start_desktop_daemon: () => startDaemon(), + stop_desktop_daemon: () => stopDaemon(), + restart_desktop_daemon: () => restartDaemon(), + desktop_daemon_logs: () => getDaemonLogs(), + desktop_daemon_pairing: () => getDaemonPairing(), + cli_symlink_instructions: () => getCliSymlinkInstructions(), + write_attachment_base64: (args) => writeAttachmentBase64(args ?? {}), + copy_attachment_file: (args) => copyAttachmentFileToManagedStorage(args ?? {}), + read_file_base64: (args) => readManagedFileBase64(args ?? {}), + delete_attachment_file: (args) => deleteManagedAttachmentFile(args ?? {}), + garbage_collect_attachment_files: (args) => + garbageCollectManagedAttachmentFiles(args ?? {}), + open_local_daemon_transport: async (args) => { + const target = args as { transportType: "socket" | "pipe"; transportPath: string }; + return await openLocalTransportSession(target); + }, + send_local_daemon_transport_message: async (args) => { + await sendLocalTransportMessage(args as { sessionId: string; text?: string; binaryBase64?: string }); + }, + close_local_daemon_transport: (args) => { + const sessionId = typeof args === "object" && args !== null && "sessionId" in args + ? (args as { sessionId: string }).sessionId + : ""; + if (sessionId) closeLocalTransportSession(sessionId); + }, + check_app_update: async () => { + const currentVersion = await resolveCurrentUpdateVersion(); + return checkForAppUpdate(currentVersion); + }, + install_app_update: async () => { + const currentVersion = await resolveCurrentUpdateVersion(); + return downloadAndInstallUpdate(currentVersion); + }, + get_local_daemon_version: () => getLocalDaemonVersion(), + webview_log: (args) => { + const level = typeof args?.level === "number" ? args.level : 1; + const message = typeof args?.message === "string" ? args.message : ""; + const method = level === 0 ? "debug" : level === 2 ? "warn" : level >= 3 ? "error" : "info"; + console[method]("[webview]", message); + }, + }; +} + +export function registerDaemonManager(): void { + const handlers = createDaemonCommandHandlers(); + + ipcMain.handle( + "paseo:invoke", + async (_event, command: string, args?: Record) => { + const handler = handlers[command]; + if (!handler) { + throw new Error(`Unknown desktop command: ${command}`); + } + return await handler(args); + } + ); +} diff --git a/packages/desktop/src/daemon/local-transport.ts b/packages/desktop/src/daemon/local-transport.ts new file mode 100644 index 000000000..b11b07b03 --- /dev/null +++ b/packages/desktop/src/daemon/local-transport.ts @@ -0,0 +1,212 @@ +import { BrowserWindow } from "electron"; +import WebSocket from "ws"; + +type TransportTarget = { + transportType: "socket" | "pipe"; + transportPath: string; +}; + +type TransportEventPayload = { + sessionId: string; + kind: "open" | "message" | "close" | "error"; + text?: string | null; + binaryBase64?: string | null; + code?: number | null; + reason?: string | null; + error?: string | null; +}; + +type Session = { + id: string; + ws: WebSocket; + state: "opening" | "open" | "closing" | "closed"; +}; + +const WS_ENDPOINT_PATH = "/ws"; + +let nextSessionId = 0; +const sessions = new Map(); + +function emitTransportEvent(payload: TransportEventPayload): void { + for (const win of BrowserWindow.getAllWindows()) { + win.webContents.send("paseo:event:local-daemon-transport-event", payload); + } +} + +/** + * Build a WebSocket URL that connects through a Unix domain socket or Windows + * named pipe. The `ws` library supports these via the `ws+unix://` scheme: + * + * ws+unix:///path/to/socket:/ws + * ws+unix://./pipe/paseo:/ws (Windows named pipe) + * + * The part before `:` is the IPC path, the part after is the HTTP request + * path used during the WebSocket upgrade handshake. + */ +function buildLocalWebSocketUrl(target: TransportTarget): string { + const ipcPath = target.transportPath; + return `ws+unix://${ipcPath}:${WS_ENDPOINT_PATH}`; +} + +function describeTransportTarget(target: TransportTarget): string { + return target.transportType === "pipe" ? "local daemon pipe" : "local daemon socket"; +} + +function decodeTransportMessage(input: { + text?: string; + binaryBase64?: string; +}): string | Buffer { + if (typeof input.text === "string") { + return input.text; + } + + if (typeof input.binaryBase64 === "string") { + return Buffer.from(input.binaryBase64, "base64"); + } + + throw new Error("Local transport send requires text or binary payload."); +} + +export function openLocalTransportSession(target: TransportTarget): Promise { + const sessionId = `local-session-${++nextSessionId}`; + const url = buildLocalWebSocketUrl(target); + + return new Promise((resolve, reject) => { + const ws = new WebSocket(url); + const session: Session = { + id: sessionId, + ws, + state: "opening", + }; + sessions.set(sessionId, session); + + let openSettled = false; + + const finalizeOpenFailure = (message: string): void => { + if (openSettled) { + return; + } + + openSettled = true; + session.state = "closed"; + sessions.delete(sessionId); + reject(new Error(message)); + }; + + ws.once("open", () => { + openSettled = true; + session.state = "open"; + resolve(sessionId); + emitTransportEvent({ sessionId, kind: "open" }); + }); + + ws.on("message", (data: WebSocket.RawData, isBinary: boolean) => { + if (isBinary || data instanceof Buffer) { + const buf = Buffer.isBuffer(data) ? data : Buffer.from(data as ArrayBuffer); + emitTransportEvent({ + sessionId, + kind: "message", + binaryBase64: buf.toString("base64"), + }); + return; + } + + emitTransportEvent({ + sessionId, + kind: "message", + text: data.toString(), + }); + }); + + ws.on("close", (code: number, reason?: Buffer | string) => { + const shouldEmitClose = session.state === "open" || session.state === "closing"; + session.state = "closed"; + sessions.delete(sessionId); + + if (!openSettled) { + finalizeOpenFailure( + `${describeTransportTarget(target)} closed before the session became ready.` + ); + return; + } + + if (shouldEmitClose) { + emitTransportEvent({ + sessionId, + kind: "close", + code, + reason: reason ? String(reason) : "", + }); + } + }); + + ws.on("error", (err: Error) => { + if (!openSettled) { + finalizeOpenFailure( + `Failed to connect to ${describeTransportTarget(target)}: ${err.message}` + ); + return; + } + + emitTransportEvent({ + sessionId, + kind: "error", + error: err.message, + }); + }); + }); +} + +export async function sendLocalTransportMessage(input: { + sessionId: string; + text?: string; + binaryBase64?: string; +}): Promise { + const session = sessions.get(input.sessionId); + if (!session) { + throw new Error(`Local transport session not found: ${input.sessionId}`); + } + + if (session.state !== "open" || session.ws.readyState !== WebSocket.OPEN) { + throw new Error( + session.state === "opening" + ? "Local transport session is not open yet." + : "Local transport session is closed." + ); + } + + const payload = decodeTransportMessage(input); + await new Promise((resolve, reject) => { + session.ws.send(payload, (error) => { + if (error) { + reject(new Error(`Local transport write failed: ${error.message}`)); + return; + } + resolve(); + }); + }); +} + +export function closeLocalTransportSession(sessionId: string): void { + const session = sessions.get(sessionId); + if (!session) return; + + try { + if (session.ws.readyState === WebSocket.CONNECTING) { + session.state = "closed"; + session.ws.terminate(); + } else { + session.state = "closing"; + session.ws.close(); + } + } catch { + // ignore close errors + } + sessions.delete(sessionId); +} + +export function closeAllTransportSessions(): void { + for (const [id] of sessions) { + closeLocalTransportSession(id); + } +} diff --git a/packages/desktop/src/daemon/runtime-paths.ts b/packages/desktop/src/daemon/runtime-paths.ts new file mode 100644 index 000000000..484c9aa73 --- /dev/null +++ b/packages/desktop/src/daemon/runtime-paths.ts @@ -0,0 +1,196 @@ +import { existsSync, readFileSync } from "node:fs"; +import { spawnSync, type SpawnSyncReturns } from "node:child_process"; +import { createRequire } from "node:module"; +import path from "node:path"; +import { app } from "electron"; + +const CLI_PACKAGE_NAME = "@getpaseo/cli"; +const SERVER_PACKAGE_NAME = "@getpaseo/server"; +const CLI_BIN_ENTRY = `${CLI_PACKAGE_NAME}/bin/paseo`; +const IGNORED_GUI_LAUNCH_ARG_PREFIX = "-psn_"; + +type PackageInfo = { + root: string; +}; + +export type NodeEntrypointSpec = { + entryPath: string; + execArgv: string[]; +}; + +const esmRequire = createRequire(__filename); + +function findPackageRootFromResolvedPath(input: { + resolvedPath: string; + packageName: string; +}): PackageInfo { + let currentDir = path.dirname(input.resolvedPath); + + while (true) { + const packageJsonPath = path.join(currentDir, "package.json"); + if (existsSync(packageJsonPath)) { + try { + const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as { + name?: string; + }; + if (pkg.name === input.packageName) { + return { + root: currentDir, + }; + } + } catch { + // Ignore malformed package metadata while walking up. + } + } + + const parent = path.dirname(currentDir); + if (parent === currentDir) { + break; + } + currentDir = parent; + } + + throw new Error(`Unable to resolve ${input.packageName} package root`); +} + +function resolveServerPackageInfo(): PackageInfo { + const serverExportPath = esmRequire.resolve(SERVER_PACKAGE_NAME); + return findPackageRootFromResolvedPath({ + resolvedPath: serverExportPath, + packageName: SERVER_PACKAGE_NAME, + }); +} + +function resolveCliPackageInfo(): PackageInfo { + const cliBinPath = esmRequire.resolve(CLI_BIN_ENTRY); + return findPackageRootFromResolvedPath({ + resolvedPath: cliBinPath, + packageName: CLI_PACKAGE_NAME, + }); +} + +function resolvePackagedAsarPath(): string { + return path.join(process.resourcesPath, "app.asar"); +} + +function assertPathExists(input: { + label: string; + filePath: string; +}): string { + if (!existsSync(input.filePath)) { + throw new Error(`${input.label} is missing at ${input.filePath}`); + } + + return input.filePath; +} + +export function parseCliPassthroughArgsFromArgv(argv: string[]): string[] | null { + const startIndex = process.defaultApp ? 2 : 1; + const effective = argv + .slice(startIndex) + .filter((arg) => !arg.startsWith(IGNORED_GUI_LAUNCH_ARG_PREFIX)); + + return effective.length > 0 ? effective : null; +} + +export function resolveDaemonRunnerEntrypoint(): NodeEntrypointSpec { + if (app.isPackaged) { + return { + entryPath: assertPathExists({ + label: "Bundled daemon runner", + filePath: path.join( + resolvePackagedAsarPath(), + "node_modules", + "@getpaseo", + "server", + "dist", + "scripts", + "daemon-runner.js" + ), + }), + execArgv: [], + }; + } + + const serverPackage = resolveServerPackageInfo(); + const distRunner = path.join(serverPackage.root, "dist", "scripts", "daemon-runner.js"); + if (existsSync(distRunner)) { + return { + entryPath: distRunner, + execArgv: [], + }; + } + + return { + entryPath: assertPathExists({ + label: "Daemon runner source", + filePath: path.join(serverPackage.root, "scripts", "daemon-runner.ts"), + }), + execArgv: ["--import", "tsx"], + }; +} + +export function resolveCliEntrypoint(): NodeEntrypointSpec { + if (app.isPackaged) { + return { + entryPath: assertPathExists({ + label: "Bundled CLI entrypoint", + filePath: path.join( + resolvePackagedAsarPath(), + "node_modules", + "@getpaseo", + "cli", + "dist", + "index.js" + ), + }), + execArgv: [], + }; + } + + const cliPackage = resolveCliPackageInfo(); + const distEntry = path.join(cliPackage.root, "dist", "index.js"); + if (existsSync(distEntry)) { + return { + entryPath: distEntry, + execArgv: [], + }; + } + + return { + entryPath: assertPathExists({ + label: "CLI source entrypoint", + filePath: path.join(cliPackage.root, "src", "index.ts"), + }), + execArgv: ["--import", "tsx"], + }; +} + +export function createElectronNodeEnv(baseEnv: NodeJS.ProcessEnv): NodeJS.ProcessEnv { + return { + ...baseEnv, + ELECTRON_RUN_AS_NODE: "1", + }; +} + +function spawnCliProcess(args: string[]): SpawnSyncReturns { + const cli = resolveCliEntrypoint(); + + return spawnSync(process.execPath, [...cli.execArgv, cli.entryPath, ...args], { + env: createElectronNodeEnv(process.env), + stdio: "inherit", + }); +} + +export function runCliPassthroughCommand(args: string[]): number { + const result = spawnCliProcess(args); + if (result.error) { + throw result.error; + } + + if (typeof result.status === "number") { + return result.status; + } + + return result.signal ? 1 : 0; +} diff --git a/packages/desktop/src/features/attachments.ts b/packages/desktop/src/features/attachments.ts new file mode 100644 index 000000000..0681bd9db --- /dev/null +++ b/packages/desktop/src/features/attachments.ts @@ -0,0 +1,162 @@ +import { copyFile, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { resolvePaseoHome } from "@getpaseo/server"; + +const ATTACHMENTS_DIRNAME = "desktop-attachments"; +const ATTACHMENT_ID_PATTERN = /^[A-Za-z0-9_-]+$/; +const EXTENSION_PATTERN = /^\.[A-Za-z0-9]{1,16}$/; + +type AttachmentFileResult = { + path: string; + byteSize: number; +}; + +function attachmentsDirPath(): string { + return path.join(resolvePaseoHome(process.env), ATTACHMENTS_DIRNAME); +} + +async function ensureAttachmentsDir(): Promise { + const dirPath = attachmentsDirPath(); + await mkdir(dirPath, { recursive: true }); + return dirPath; +} + +function normalizeAttachmentId(value: unknown): string { + if (typeof value !== "string") { + throw new Error("Attachment id is required."); + } + const normalized = value.trim(); + if (!ATTACHMENT_ID_PATTERN.test(normalized)) { + throw new Error(`Invalid attachment id: ${value}`); + } + return normalized; +} + +function normalizeExtension(value: unknown): string { + if (value == null || value === "") { + return ".bin"; + } + if (typeof value !== "string") { + throw new Error("Attachment extension must be a string."); + } + const normalized = value.trim().toLowerCase(); + if (!EXTENSION_PATTERN.test(normalized)) { + throw new Error(`Invalid attachment extension: ${value}`); + } + return normalized; +} + +async function buildManagedAttachmentPath(input: { + attachmentId: unknown; + extension: unknown; +}): Promise { + const dirPath = await ensureAttachmentsDir(); + const attachmentId = normalizeAttachmentId(input.attachmentId); + const extension = normalizeExtension(input.extension); + return path.join(dirPath, `${attachmentId}${extension}`); +} + +function resolveManagedAttachmentPath(inputPath: unknown): string { + if (typeof inputPath !== "string" || inputPath.trim().length === 0) { + throw new Error("Attachment path is required."); + } + const resolvedDir = `${path.resolve(attachmentsDirPath())}${path.sep}`; + const resolvedPath = path.resolve(inputPath.trim()); + if (!resolvedPath.startsWith(resolvedDir)) { + throw new Error("Attachment path must stay within desktop-managed storage."); + } + return resolvedPath; +} + +export async function writeAttachmentBase64(input: { + attachmentId?: unknown; + base64?: unknown; + extension?: unknown; +}): Promise { + const base64 = typeof input.base64 === "string" ? input.base64.trim() : ""; + if (base64.length === 0) { + throw new Error("Attachment base64 payload is required."); + } + + const targetPath = await buildManagedAttachmentPath({ + attachmentId: input.attachmentId, + extension: input.extension, + }); + await writeFile(targetPath, Buffer.from(base64, "base64")); + const fileInfo = await stat(targetPath); + return { + path: targetPath, + byteSize: fileInfo.size, + }; +} + +export async function copyAttachmentFileToManagedStorage(input: { + attachmentId?: unknown; + sourcePath?: unknown; + extension?: unknown; +}): Promise { + if (typeof input.sourcePath !== "string" || input.sourcePath.trim().length === 0) { + throw new Error("Attachment source path is required."); + } + + const sourcePath = path.resolve(input.sourcePath.trim()); + const targetPath = await buildManagedAttachmentPath({ + attachmentId: input.attachmentId, + extension: input.extension, + }); + + if (sourcePath !== targetPath) { + await copyFile(sourcePath, targetPath); + } + + const fileInfo = await stat(targetPath); + return { + path: targetPath, + byteSize: fileInfo.size, + }; +} + +export async function readManagedFileBase64(input: { path?: unknown }): Promise { + const filePath = resolveManagedAttachmentPath(input.path); + const bytes = await readFile(filePath); + return bytes.toString("base64"); +} + +export async function deleteManagedAttachmentFile(input: { + path?: unknown; +}): Promise { + const filePath = resolveManagedAttachmentPath(input.path); + await rm(filePath, { force: true }); + return true; +} + +export async function garbageCollectManagedAttachmentFiles(input: { + referencedIds?: unknown; +}): Promise { + const dirPath = await ensureAttachmentsDir(); + const referencedIds = Array.isArray(input.referencedIds) + ? new Set( + input.referencedIds + .filter((value): value is string => typeof value === "string") + .map((value) => value.trim()) + .filter((value) => ATTACHMENT_ID_PATTERN.test(value)) + ) + : new Set(); + + const entries = await readdir(dirPath, { withFileTypes: true }); + let deletedCount = 0; + + for (const entry of entries) { + if (!entry.isFile()) { + continue; + } + const attachmentId = path.parse(entry.name).name; + if (referencedIds.has(attachmentId)) { + continue; + } + await rm(path.join(dirPath, entry.name), { force: true }); + deletedCount += 1; + } + + return deletedCount; +} diff --git a/packages/desktop/src/features/auto-updater.ts b/packages/desktop/src/features/auto-updater.ts new file mode 100644 index 000000000..8009b41c9 --- /dev/null +++ b/packages/desktop/src/features/auto-updater.ts @@ -0,0 +1,155 @@ +import { app } from "electron"; +import { autoUpdater, type UpdateInfo } from "electron-updater"; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export type AppUpdateCheckResult = { + hasUpdate: boolean; + currentVersion: string; + latestVersion: string; + body: string | null; + date: string | null; +}; + +export type AppUpdateInstallResult = { + installed: boolean; + version: string | null; + message: string; +}; + +// --------------------------------------------------------------------------- +// State +// --------------------------------------------------------------------------- + +let cachedUpdateInfo: UpdateInfo | null = null; +let downloading = false; + +// --------------------------------------------------------------------------- +// Configuration +// --------------------------------------------------------------------------- + +function configureAutoUpdater(): void { + // Don't auto-download — the user triggers install explicitly. + autoUpdater.autoDownload = false; + autoUpdater.autoInstallOnAppQuit = true; + + // Suppress built-in dialogs; the renderer handles UI. + autoUpdater.autoRunAppAfterInstall = true; +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +export async function checkForAppUpdate(currentVersion: string): Promise { + if (!app.isPackaged) { + return { + hasUpdate: false, + currentVersion, + latestVersion: currentVersion, + body: null, + date: null, + }; + } + + configureAutoUpdater(); + + try { + const result = await autoUpdater.checkForUpdates(); + + if (!result || !result.updateInfo) { + return { + hasUpdate: false, + currentVersion, + latestVersion: currentVersion, + body: null, + date: null, + }; + } + + const info = result.updateInfo; + const latestVersion = info.version; + const hasUpdate = latestVersion !== currentVersion; + + if (hasUpdate) { + cachedUpdateInfo = info; + } + + return { + hasUpdate, + currentVersion, + latestVersion, + body: typeof info.releaseNotes === "string" ? info.releaseNotes : null, + date: typeof info.releaseDate === "string" ? info.releaseDate : null, + }; + } catch (error) { + console.error("[auto-updater] Failed to check for updates:", error); + return { + hasUpdate: false, + currentVersion, + latestVersion: currentVersion, + body: null, + date: null, + }; + } +} + +export async function downloadAndInstallUpdate(currentVersion: string): Promise { + if (!app.isPackaged) { + return { + installed: false, + version: currentVersion, + message: "Auto-update is not available in development mode.", + }; + } + + if (downloading) { + return { + installed: false, + version: currentVersion, + message: "Update already in progress.", + }; + } + + if (!cachedUpdateInfo) { + return { + installed: false, + version: currentVersion, + message: "No update available. Check for updates first.", + }; + } + + configureAutoUpdater(); + + downloading = true; + + try { + await autoUpdater.downloadUpdate(); + // quitAndInstall restarts the app with the new version. + // Use a short delay to allow the renderer to receive the response. + setTimeout(() => { + try { + autoUpdater.quitAndInstall(/* isSilent */ false, /* isForceRunAfter */ true); + } catch (error) { + console.error("[auto-updater] quitAndInstall failed:", error); + } + }, 1500); + + return { + installed: true, + version: cachedUpdateInfo.version, + message: "Update downloaded. The app will restart shortly.", + }; + } catch (error) { + downloading = false; + const message = error instanceof Error ? error.message : String(error); + console.error("[auto-updater] Failed to download/install update:", message); + return { + installed: false, + version: currentVersion, + message: `Update failed: ${message}`, + }; + } +} diff --git a/packages/desktop/src/features/dialogs.ts b/packages/desktop/src/features/dialogs.ts new file mode 100644 index 000000000..d42ae7215 --- /dev/null +++ b/packages/desktop/src/features/dialogs.ts @@ -0,0 +1,49 @@ +import { dialog, ipcMain, BrowserWindow } from "electron"; + +type AskOptions = { + title?: string; + okLabel?: string; + cancelLabel?: string; + kind?: "info" | "warning" | "error"; +}; + +type OpenOptions = { + title?: string; + defaultPath?: string; + directory?: boolean; + multiple?: boolean; + filters?: Array<{ name: string; extensions: string[] }>; +}; + +export function registerDialogHandlers(): void { + ipcMain.handle("paseo:dialog:ask", async (event, message: string, options?: AskOptions) => { + const win = BrowserWindow.fromWebContents(event.sender); + const result = await dialog.showMessageBox(win ?? BrowserWindow.getFocusedWindow()!, { + type: options?.kind === "warning" ? "warning" : options?.kind === "error" ? "error" : "question", + title: options?.title ?? "Confirm", + message, + buttons: [options?.cancelLabel ?? "Cancel", options?.okLabel ?? "OK"], + defaultId: 1, + cancelId: 0, + }); + return result.response === 1; + }); + + ipcMain.handle("paseo:dialog:open", async (event, options?: OpenOptions) => { + const win = BrowserWindow.fromWebContents(event.sender); + const properties: Electron.OpenDialogOptions["properties"] = []; + if (options?.directory) properties.push("openDirectory"); + if (options?.multiple) properties.push("multiSelections"); + if (!options?.directory) properties.push("openFile"); + + const result = await dialog.showOpenDialog(win ?? BrowserWindow.getFocusedWindow()!, { + title: options?.title, + defaultPath: options?.defaultPath, + properties, + filters: options?.filters, + }); + + if (result.canceled) return null; + return options?.multiple ? result.filePaths : (result.filePaths[0] ?? null); + }); +} diff --git a/packages/desktop/src/features/menu.ts b/packages/desktop/src/features/menu.ts new file mode 100644 index 000000000..1d9147270 --- /dev/null +++ b/packages/desktop/src/features/menu.ts @@ -0,0 +1,92 @@ +import { app, Menu, BrowserWindow } from "electron"; + +function withBrowserWindow( + callback: (win: BrowserWindow) => void +): (_item: Electron.MenuItem, baseWin: Electron.BaseWindow | undefined) => void { + return (_item, baseWin) => { + const win = baseWin instanceof BrowserWindow ? baseWin : BrowserWindow.getFocusedWindow(); + if (win) callback(win); + }; +} + +export function setupApplicationMenu(): void { + const isMac = process.platform === "darwin"; + + const template: Electron.MenuItemConstructorOptions[] = [ + ...(isMac + ? [ + { + label: app.name, + submenu: [ + { role: "about" as const }, + { type: "separator" as const }, + { role: "services" as const }, + { type: "separator" as const }, + { role: "hide" as const }, + { role: "hideOthers" as const }, + { role: "unhide" as const }, + { type: "separator" as const }, + { role: "quit" as const }, + ], + }, + ] + : []), + { + label: "Edit", + submenu: [ + { role: "undo" }, + { role: "redo" }, + { type: "separator" }, + { role: "cut" }, + { role: "copy" }, + { role: "paste" }, + { role: "selectAll" }, + ], + }, + { + label: "View", + submenu: [ + { + label: "Zoom In", + accelerator: "CmdOrCtrl+=", + click: withBrowserWindow((win) => { + win.webContents.setZoomLevel(win.webContents.getZoomLevel() + 0.5); + }), + }, + { + label: "Zoom Out", + accelerator: "CmdOrCtrl+-", + click: withBrowserWindow((win) => { + win.webContents.setZoomLevel(win.webContents.getZoomLevel() - 0.5); + }), + }, + { + label: "Actual Size", + accelerator: "CmdOrCtrl+0", + click: withBrowserWindow((win) => { + win.webContents.setZoomLevel(0); + }), + }, + { type: "separator" }, + { role: "reload" }, + { role: "forceReload" }, + { role: "toggleDevTools" }, + { type: "separator" }, + { role: "togglefullscreen" }, + ], + }, + { + label: "Window", + submenu: [ + { role: "minimize" }, + { role: "zoom" }, + ...(isMac + ? [{ type: "separator" as const }, { role: "front" as const }] + : [{ role: "close" as const }]), + ], + }, + ]; + + const menu = Menu.buildFromTemplate(template); + Menu.setApplicationMenu(menu); +} diff --git a/packages/desktop/src/features/notifications.ts b/packages/desktop/src/features/notifications.ts new file mode 100644 index 000000000..3d32c26d7 --- /dev/null +++ b/packages/desktop/src/features/notifications.ts @@ -0,0 +1,123 @@ +import path from "node:path"; +import { existsSync } from "node:fs"; +import { app, BrowserWindow, Notification, ipcMain, nativeImage } from "electron"; + +type NotificationInput = { + title?: unknown; + body?: unknown; + data?: unknown; +}; + +type NotificationClickPayload = { + data?: Record; +}; + +const activeNotifications = new Set(); + +function toTrimmedString(value: unknown): string | null { + if (typeof value !== "string") { + return null; + } + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : null; +} + +function toRecord(value: unknown): Record | undefined { + return typeof value === "object" && value !== null && !Array.isArray(value) + ? (value as Record) + : undefined; +} + +function getNotificationIcon(): Electron.NativeImage | null { + const candidates = [ + path.resolve(__dirname, "../assets/icon.png"), + path.resolve(__dirname, "../assets/64x64.png"), + path.resolve(__dirname, "../assets/128x128.png"), + ]; + + for (const iconPath of candidates) { + if (!existsSync(iconPath)) { + continue; + } + const icon = nativeImage.createFromPath(iconPath); + if (!icon.isEmpty()) { + return icon; + } + } + + return null; +} + +function focusSenderWindow(sender: Electron.WebContents): BrowserWindow | null { + const win = BrowserWindow.fromWebContents(sender) ?? BrowserWindow.getAllWindows()[0] ?? null; + if (!win || win.isDestroyed()) { + return null; + } + win.show(); + if (win.isMinimized()) { + win.restore(); + } + win.focus(); + return win; +} + +/** + * macOS requires a notification to have been shown at least once before + * the app appears in System Preferences > Notifications. We fire a + * silent no-op notification during startup to ensure registration. + */ +export function ensureNotificationCenterRegistration(): void { + if (process.platform !== "darwin" || !Notification.isSupported()) { + return; + } + + const probe = new Notification({ title: app.name, silent: true }); + probe.on("show", () => probe.close()); + setTimeout(() => probe.close(), 2_000); + probe.show(); +} + +export function registerNotificationHandlers(): void { + ipcMain.handle("paseo:notification:isSupported", () => { + return Notification.isSupported(); + }); + + ipcMain.handle("paseo:notification:send", async (event, rawInput?: NotificationInput) => { + if (!Notification.isSupported()) { + return false; + } + + const title = toTrimmedString(rawInput?.title); + if (!title) { + return false; + } + + const body = toTrimmedString(rawInput?.body) ?? undefined; + const data = toRecord(rawInput?.data); + const icon = getNotificationIcon(); + const notification = new Notification({ + title, + ...(body ? { body } : {}), + ...(icon ? { icon } : {}), + silent: true, + }); + + activeNotifications.add(notification); + + notification.on("click", () => { + const win = focusSenderWindow(event.sender); + if (win && data && Object.keys(data).length > 0) { + const payload: NotificationClickPayload = { data }; + win.webContents.send("paseo:event:notification-click", payload); + } + activeNotifications.delete(notification); + }); + + notification.on("close", () => { + activeNotifications.delete(notification); + }); + + notification.show(); + return true; + }); +} diff --git a/packages/desktop/src/features/opener.ts b/packages/desktop/src/features/opener.ts new file mode 100644 index 000000000..d39ac9e37 --- /dev/null +++ b/packages/desktop/src/features/opener.ts @@ -0,0 +1,7 @@ +import { shell, ipcMain } from "electron"; + +export function registerOpenerHandlers(): void { + ipcMain.handle("paseo:opener:openUrl", async (_event, url: string) => { + await shell.openExternal(url); + }); +} diff --git a/packages/desktop/src/main.ts b/packages/desktop/src/main.ts new file mode 100644 index 000000000..c8b515640 --- /dev/null +++ b/packages/desktop/src/main.ts @@ -0,0 +1,180 @@ +import path from "node:path"; +import { existsSync } from "node:fs"; +import { app, BrowserWindow, nativeImage } from "electron"; +import { registerDaemonManager } from "./daemon/daemon-manager.js"; +import { parseCliPassthroughArgsFromArgv, runCliPassthroughCommand } from "./daemon/runtime-paths.js"; +import { closeAllTransportSessions } from "./daemon/local-transport.js"; +import { registerWindowManager, setupWindowResizeEvents, setupDragDropPrevention } from "./window/window-manager.js"; +import { registerDialogHandlers } from "./features/dialogs.js"; +import { registerNotificationHandlers, ensureNotificationCenterRegistration } from "./features/notifications.js"; +import { registerOpenerHandlers } from "./features/opener.js"; +import { setupApplicationMenu } from "./features/menu.js"; + +const DEV_SERVER_URL = process.env.EXPO_DEV_URL ?? "http://localhost:8081"; +app.setName("Paseo"); + +// --------------------------------------------------------------------------- +// Window creation +// --------------------------------------------------------------------------- + +function getPreloadPath(): string { + return path.join(__dirname, "preload.js"); +} + +function getProductionEntryPath(): string { + if (app.isPackaged) { + return path.join(process.resourcesPath, "app-dist", "index.html"); + } + + return path.resolve(__dirname, "../../app/dist/index.html"); +} + +function getWindowIconPath(): string | null { + const candidates = app.isPackaged + ? process.platform === "win32" + ? [path.join(process.resourcesPath, "icon.ico"), path.join(process.resourcesPath, "icon.png")] + : [path.join(process.resourcesPath, "icon.png")] + : process.platform === "darwin" + ? [path.resolve(__dirname, "../assets/icon.png")] + : process.platform === "win32" + ? [path.resolve(__dirname, "../assets/icon.ico"), path.resolve(__dirname, "../assets/icon.png")] + : [path.resolve(__dirname, "../assets/icon.png")]; + + return candidates.find((candidate) => existsSync(candidate)) ?? null; +} + +function applyAppIcon(): void { + if (process.platform !== "darwin") { + return; + } + + const iconPath = path.resolve(__dirname, "../assets/icon.png"); + if (!existsSync(iconPath)) { + return; + } + + const icon = nativeImage.createFromPath(iconPath); + if (icon.isEmpty()) { + return; + } + + app.dock?.setIcon(icon); +} + +async function createMainWindow(): Promise { + const isMac = process.platform === "darwin"; + const iconPath = getWindowIconPath(); + + const mainWindow = new BrowserWindow({ + width: 1200, + height: 800, + show: false, + ...(iconPath ? { icon: iconPath } : {}), + titleBarStyle: isMac ? "hidden" : "default", + trafficLightPosition: isMac ? { x: 16, y: 20 } : undefined, + webPreferences: { + preload: getPreloadPath(), + contextIsolation: true, + nodeIntegration: false, + }, + }); + + setupWindowResizeEvents(mainWindow); + setupDragDropPrevention(mainWindow); + + mainWindow.once("ready-to-show", () => { + mainWindow.show(); + }); + + if (!app.isPackaged) { + await mainWindow.loadURL(DEV_SERVER_URL); + mainWindow.webContents.openDevTools({ mode: "detach" }); + return; + } + + await mainWindow.loadFile(getProductionEntryPath()); +} + +// --------------------------------------------------------------------------- +// App lifecycle +// --------------------------------------------------------------------------- + +function setupSingleInstanceLock(): boolean { + const gotLock = app.requestSingleInstanceLock(); + if (!gotLock) { + app.quit(); + return false; + } + + app.on("second-instance", () => { + const win = BrowserWindow.getAllWindows()[0]; + if (win) { + win.show(); + if (win.isMinimized()) win.restore(); + win.focus(); + } + }); + + return true; +} + +async function runCliPassthroughIfRequested(): Promise { + const cliArgs = parseCliPassthroughArgsFromArgv(process.argv); + if (!cliArgs) { + return false; + } + + try { + const exitCode = runCliPassthroughCommand(cliArgs); + process.exit(exitCode); + } catch (error) { + const message = error instanceof Error ? error.stack ?? error.message : String(error); + process.stderr.write(`${message}\n`); + process.exit(1); + } + + return true; +} + +async function bootstrap(): Promise { + if (await runCliPassthroughIfRequested()) { + return; + } + + if (!setupSingleInstanceLock()) { + return; + } + + await app.whenReady(); + applyAppIcon(); + setupApplicationMenu(); + ensureNotificationCenterRegistration(); + registerDaemonManager(); + registerWindowManager(); + registerDialogHandlers(); + registerNotificationHandlers(); + registerOpenerHandlers(); + await createMainWindow(); + + app.on("activate", async () => { + if (BrowserWindow.getAllWindows().length === 0) { + await createMainWindow(); + } + }); +} + +void bootstrap().catch((error) => { + const message = error instanceof Error ? error.stack ?? error.message : String(error); + process.stderr.write(`${message}\n`); + process.exit(1); +}); + +app.on("before-quit", () => { + closeAllTransportSessions(); +}); + +app.on("window-all-closed", () => { + if (process.platform !== "darwin") { + app.quit(); + } +}); diff --git a/packages/desktop/src/preload.ts b/packages/desktop/src/preload.ts new file mode 100644 index 000000000..36b9bcad1 --- /dev/null +++ b/packages/desktop/src/preload.ts @@ -0,0 +1,51 @@ +import { contextBridge, ipcRenderer } from "electron"; + +type EventHandler = (payload: unknown) => void; + +contextBridge.exposeInMainWorld("paseoDesktop", { + platform: process.platform, + invoke: (command: string, args?: Record) => + ipcRenderer.invoke("paseo:invoke", command, args), + events: { + on: (event: string, handler: EventHandler): Promise<() => void> => { + const listener = (_ipcEvent: Electron.IpcRendererEvent, payload: unknown) => { + handler(payload); + }; + ipcRenderer.on(`paseo:event:${event}`, listener); + return Promise.resolve(() => { + ipcRenderer.removeListener(`paseo:event:${event}`, listener); + }); + }, + }, + window: { + getCurrentWindow: () => ({ + startDragging: () => ipcRenderer.invoke("paseo:window:startDragging"), + toggleMaximize: () => ipcRenderer.invoke("paseo:window:toggleMaximize"), + isFullscreen: () => ipcRenderer.invoke("paseo:window:isFullscreen"), + onResized: (handler: EventHandler): (() => void) => { + const listener = (_ipcEvent: Electron.IpcRendererEvent, payload: unknown) => { + handler(payload); + }; + ipcRenderer.on("paseo:window:resized", listener); + return () => { + ipcRenderer.removeListener("paseo:window:resized", listener); + }; + }, + setBadgeCount: (count?: number) => ipcRenderer.invoke("paseo:window:setBadgeCount", count), + }), + }, + dialog: { + ask: (message: string, options?: Record) => + ipcRenderer.invoke("paseo:dialog:ask", message, options), + open: (options?: Record) => + ipcRenderer.invoke("paseo:dialog:open", options), + }, + notification: { + isSupported: () => ipcRenderer.invoke("paseo:notification:isSupported"), + sendNotification: (payload: { title: string; body?: string; data?: Record }) => + ipcRenderer.invoke("paseo:notification:send", payload), + }, + opener: { + openUrl: (url: string) => ipcRenderer.invoke("paseo:opener:openUrl", url), + }, +}); diff --git a/packages/desktop/src/window/window-manager.ts b/packages/desktop/src/window/window-manager.ts new file mode 100644 index 000000000..ceaf52d2c --- /dev/null +++ b/packages/desktop/src/window/window-manager.ts @@ -0,0 +1,61 @@ +import { app, BrowserWindow, ipcMain } from "electron"; + +export function registerWindowManager(): void { + ipcMain.handle("paseo:window:startDragging", (event) => { + const win = BrowserWindow.fromWebContents(event.sender); + // Desktop dragging is handled via CSS `-webkit-app-region: drag`. + // This handler exists only to satisfy the renderer bridge contract. + if (win) { + // No-op. + } + }); + + ipcMain.handle("paseo:window:toggleMaximize", (event) => { + const win = BrowserWindow.fromWebContents(event.sender); + if (!win) return; + if (win.isMaximized()) { + win.unmaximize(); + } else { + win.maximize(); + } + }); + + ipcMain.handle("paseo:window:isFullscreen", (event) => { + const win = BrowserWindow.fromWebContents(event.sender); + return win?.isFullScreen() ?? false; + }); + + ipcMain.handle("paseo:window:setBadgeCount", (_event, count?: number) => { + if (process.platform === "darwin" || process.platform === "linux") { + app.setBadgeCount(count ?? 0); + } + }); +} + +export function setupWindowResizeEvents(win: BrowserWindow): void { + win.on("resize", () => { + win.webContents.send("paseo:window:resized", {}); + }); + + win.on("enter-full-screen", () => { + win.webContents.send("paseo:window:resized", {}); + }); + + win.on("leave-full-screen", () => { + win.webContents.send("paseo:window:resized", {}); + }); +} + +/** + * Prevent Electron from navigating to files dragged onto the window. + * The renderer handles drag-drop via standard HTML5 APIs instead. + */ +export function setupDragDropPrevention(win: BrowserWindow): void { + win.webContents.on("will-navigate", (event, url) => { + // Allow normal navigation (e.g. dev server hot-reload) but block file:// URLs + // that result from dropping files onto the window. + if (url.startsWith("file://")) { + event.preventDefault(); + } + }); +} diff --git a/packages/desktop/tsconfig.json b/packages/desktop/tsconfig.json new file mode 100644 index 000000000..a6415a751 --- /dev/null +++ b/packages/desktop/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "target": "ES2022", + "lib": ["ES2022", "DOM"], + "types": ["node"], + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "src/**/*.test.ts"] +} diff --git a/packages/server/CLAUDE.md b/packages/server/CLAUDE.md new file mode 100644 index 000000000..16cea533f --- /dev/null +++ b/packages/server/CLAUDE.md @@ -0,0 +1 @@ +See the repository-level instructions in `../../CLAUDE.md`. diff --git a/packages/server/src/server/agent/providers/claude/tool-call-mapper.test.ts b/packages/server/src/server/agent/providers/claude/tool-call-mapper.test.ts index 5487508aa..1039f93bb 100644 --- a/packages/server/src/server/agent/providers/claude/tool-call-mapper.test.ts +++ b/packages/server/src/server/agent/providers/claude/tool-call-mapper.test.ts @@ -280,7 +280,7 @@ describe("claude tool-call mapper", () => { name: "Grep", input: { pattern: '\\\\\\"cli\\\\\\""', - path: "/Users/moboudra/dev/paseo/packages/desktop/src-tauri/src", + path: "/Users/moboudra/dev/paseo/packages/desktop/src", output_mode: "content", "-n": true, }, diff --git a/packages/server/src/server/bootstrap.ts b/packages/server/src/server/bootstrap.ts index c5b6139ec..dda232050 100644 --- a/packages/server/src/server/bootstrap.ts +++ b/packages/server/src/server/bootstrap.ts @@ -3,6 +3,7 @@ import { createServer as createHTTPServer } from "http"; import { createReadStream, unlinkSync, existsSync } from "fs"; import { stat } from "fs/promises"; import { randomUUID } from "node:crypto"; +import { hostname as getHostname } from "node:os"; import path from "node:path"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; @@ -66,6 +67,16 @@ export function parseListenString(listen: string): ListenTarget { throw new Error(`Invalid listen string: ${listen}`); } +function formatListenTarget(listenTarget: ListenTarget | null): string | null { + if (!listenTarget) { + return null; + } + if (listenTarget.type === "tcp") { + return `${listenTarget.host}:${listenTarget.port}`; + } + return listenTarget.path; +} + import { VoiceAssistantWebSocketServer } from "./websocket-server.js"; import { DownloadTokenStore } from "./file-download/token-store.js"; import type { OpenAiSpeechProviderConfig } from "./speech/providers/openai/config.js"; @@ -88,6 +99,7 @@ import { encodeOfferToFragmentUrl, } from "./connection-offer.js"; import { loadOrCreateDaemonKeyPair } from "./daemon-keypair.js"; +import { generateLocalPairingOffer } from "./pairing-offer.js"; import { startRelayTransport, type RelayTransportController } from "./relay-transport.js"; import { getOrCreateServerId } from "./server-id.js"; import { resolveDaemonVersion } from "./daemon-version.js"; @@ -234,9 +246,9 @@ export async function createPaseoDaemon( // CORS - allow same-origin + configured origins const allowedOrigins = new Set([ ...config.corsAllowedOrigins, - // Tauri desktop app WebView origin (used for fetch/WebSocket in production builds). - // This origin can't be produced by normal websites, so it's safe to allow by default. - "tauri://localhost", + // Packaged desktop renderers may send `file://` or `null` as the origin. + "file://", + "null", // For TCP, add localhost variants ...(listenTarget.type === "tcp" ? [ @@ -273,6 +285,37 @@ export async function createPaseoDaemon( res.json({ status: "ok", timestamp: new Date().toISOString() }); }); + app.get("/api/status", (_req, res) => { + res.json({ + status: "server_info", + serverId, + hostname: getHostname(), + version: daemonVersion, + listen: formatListenTarget(boundListenTarget ?? listenTarget), + }); + }); + + app.get("/pairing", async (_req, res) => { + try { + const offer = await generateLocalPairingOffer({ + paseoHome: config.paseoHome, + relayEnabled: config.relayEnabled, + relayEndpoint: config.relayEndpoint, + relayPublicEndpoint: config.relayPublicEndpoint, + appBaseUrl: config.appBaseUrl, + logger, + }); + res.json(offer); + } catch (error) { + logger.error({ err: error }, "Failed to generate pairing offer"); + res.status(500).json({ + relayEnabled: false, + url: null, + qr: null, + }); + } + }); + app.get("/api/files/download", async (req, res) => { const token = typeof req.query.token === "string" && req.query.token.trim().length > 0 diff --git a/scripts/sync-workspace-versions.mjs b/scripts/sync-workspace-versions.mjs index e1f6201ef..cecb16a34 100644 --- a/scripts/sync-workspace-versions.mjs +++ b/scripts/sync-workspace-versions.mjs @@ -23,106 +23,6 @@ const dependencySections = [ const touched = [] -function syncDesktopTauriConfigVersion(version) { - const tauriConfigPath = path.join( - rootDir, - 'packages', - 'desktop', - 'src-tauri', - 'tauri.conf.json' - ) - - if (!existsSync(tauriConfigPath)) { - return - } - - const tauriConfig = JSON.parse(readFileSync(tauriConfigPath, 'utf8')) - if (tauriConfig.version === version) { - return - } - - tauriConfig.version = version - writeFileSync(tauriConfigPath, `${JSON.stringify(tauriConfig, null, 2)}\n`) - touched.push(path.relative(rootDir, tauriConfigPath)) -} - -function syncDesktopCargoPackageVersion(version) { - const cargoTomlPath = path.join( - rootDir, - 'packages', - 'desktop', - 'src-tauri', - 'Cargo.toml' - ) - - if (!existsSync(cargoTomlPath)) { - return - } - - const cargoLines = readFileSync(cargoTomlPath, 'utf8').split(/\r?\n/) - let inPackageSection = false - let updated = false - - const nextLines = cargoLines.map((line) => { - if (/^\[package\]\s*$/.test(line)) { - inPackageSection = true - return line - } - - if (inPackageSection && /^\[/.test(line)) { - inPackageSection = false - return line - } - - if (inPackageSection && /^version\s*=\s*".*"\s*$/.test(line)) { - const expectedLine = `version = "${version}"` - if (line === expectedLine) { - return line - } - updated = true - return expectedLine - } - - return line - }) - - if (!updated) { - return - } - - writeFileSync(cargoTomlPath, `${nextLines.join('\n')}\n`) - touched.push(path.relative(rootDir, cargoTomlPath)) -} - -function syncDesktopInfoPlistVersion(version) { - const infoPlistPath = path.join( - rootDir, - 'packages', - 'desktop', - 'src-tauri', - 'Info.plist' - ) - - if (!existsSync(infoPlistPath)) { - return - } - - const current = readFileSync(infoPlistPath, 'utf8') - - const shortVersionPattern = /(CFBundleShortVersionString<\/key>\s*)([^<]+)(<\/string>)/ - const bundleVersionPattern = /(CFBundleVersion<\/key>\s*)([^<]+)(<\/string>)/ - - let next = current.replace(shortVersionPattern, `$1${version}$3`) - next = next.replace(bundleVersionPattern, `$1${version}$3`) - - if (next === current) { - return - } - - writeFileSync(infoPlistPath, next) - touched.push(path.relative(rootDir, infoPlistPath)) -} - for (const workspacePath of workspacePaths) { const packagePath = path.join(rootDir, workspacePath, 'package.json') if (!existsSync(packagePath)) { @@ -163,10 +63,6 @@ for (const workspacePath of workspacePaths) { } } -syncDesktopTauriConfigVersion(rootVersion) -syncDesktopCargoPackageVersion(rootVersion) -syncDesktopInfoPlistVersion(rootVersion) - if (touched.length === 0) { console.log(`Workspace versions and internal deps already synced to ${rootVersion}`) } else { diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..0e6371f6f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,4 @@ +{ + "compilerOptions": {}, + "extends": "expo/tsconfig.base" +}