From a5eb2e3d505b6cc04dcda3b8c4a9a97189636b9d Mon Sep 17 00:00:00 2001 From: Wasim Yousef Said Date: Thu, 23 Apr 2026 13:50:10 +0200 Subject: [PATCH] Add tauri (#5144) * add unsloth studio desktop app * Fix review findings - studio/src-tauri/tauri.conf.json: retarget updater to staging repo (danielhanchen/unsloth-staging-2); switch to unslothai/unsloth on upstream merge. - studio/src-tauri/linux/postremove.sh: drop the interactive read loop and the /home/* iteration. Package maintainer scripts must stay non-interactive and must not touch other users' data. - studio/frontend/src/app/auth-guards.ts: honor tauriAutoAuth() boolean. Failed auto-auth now redirects to /login; requireGuest/requirePasswordChangeFlow only redirect to /chat when auth succeeds. The new early-return on failed auth is intentional so the login / change-password flows remain reachable when desktop auth is not yet established. - studio/frontend/src/config/env.ts: keep fetched=false on health failure so later calls retry instead of caching the client-side platform guess. - studio/src-tauri/src/install.rs: pick the available system package manager (apt-get, dnf, zypper, pacman); AppImage bundles run on non-Debian distros. - studio/frontend/src/lib/open-link.ts + markdown-text/sources callers: return boolean from openLink so callers only preventDefault on handled URLs; relative hrefs now navigate natively. - studio/frontend/src/features/settings/tabs/about-tab.tsx: fetch(apiUrl(...)) so the version request targets the backend port in desktop mode. The bare /api/health predates the Tauri webview (blame: the earlier onboarding commit, which ran with same-origin frontend/backend); in desktop mode the webview origin is tauri://localhost so the bare path fails. - install.ps1: gate the install_python_stack.py hotfix on a sentinel comment instead of a content regex; append the sentinel after applying so reruns are unambiguous. - unsloth_cli/commands/studio.py _write_auth_secret: use the atomic mkstemp + os.replace path on Windows too; chmod calls are wrapped in try/except OSError. - studio/src-tauri/src/preflight.rs probe_existing_backends: fan out the health probes concurrently; desktop-auth status still runs sequentially per candidate. reqwest::Client is internally Arc-wrapped so the in-loop .clone() is a refcount bump, not a deep clone; annotated inline. - studio/src-tauri/src/preflight.rs run_cli_probe: wait() after kill() to reap the child, matching probe_cli_capability. - studio/src-tauri/src/process.rs + main.rs: add stop_backend_detached and use it from the tray quit handler so the 5s graceful-wait does not block the Tauri main loop. RunEvent::Exit keeps the synchronous safety-net call. - studio/backend/main.py: drop the permissive localhost CORS regex in api-only mode; the explicit allow_origins list is sufficient. - .github/workflows/release-desktop.yml: drop max-parallel: 1 so platform builds run in parallel, and lift releaseBody to an env var so the three tauri-action invocations share one source of truth. * Fix review findings (loop 2) - studio/backend/auth/storage.py update_password: clear_desktop_secret() alongside clear_bootstrap_password() so rotating the admin password also revokes any previously provisioned .desktop_secret. Without this, an old local desktop credential keeps minting fresh admin tokens via /api/auth/desktop-login after a password rotation. - studio/src-tauri/src/desktop_auth.rs provision_desktop_auth: wrap cmd.output().await in tokio::time::timeout(30s). DESKTOP_AUTH_LOCK is held across the whole desktop_auth flow, and previously a hanging `unsloth studio provision-desktop-auth` subprocess would pin the lock indefinitely and freeze every subsequent desktop_auth call. * Add review tests * Consolidate review tests Merge review-added tests into the existing studio/backend/tests/test_desktop_auth.py (the PR's authoritative desktop-auth test file). Drops three scaffolding files under tests/python/ in favor of five focused tests next to the tests they extend: - test_update_password_clears_desktop_secret (runtime) - test_update_password_on_unknown_user_leaves_desktop_secret_intact (runtime) - test_cli_provisioning_delegates_to_storage_create_desktop_secret (source-level) - test_cli_connect_auth_db_reads_storage_db_path (source-level) - test_desktop_auth_provision_has_bounded_timeout (Rust source-level) * Revert auth-guards.ts Tauri branches to unconditional form The review loop on PR 5144 introduced a regression: the isTauri branch of requireAuth redirected to /login when tauriAutoAuth() returned false, and requireGuest / requirePasswordChangeFlow silently fell through on the same condition. The Tauri desktop app authenticates via a local auto-generated secret; it must never surface /login or /change-password to the user. A failed auto-auth should let the startup layer retry, not expose a password form. Restore the three Tauri branches to the author's original unconditional form (requireAuth: return; requireGuest / requirePasswordChangeFlow: throw redirect({to: '/chat'})). Keep the rest of the review fixes -- the apiUrl() fetch wrapping, authRedirect helper, and fetchAuthStatus refactor are all legitimate improvements and are preserved. * Revert release-desktop.yml to author's version The review loop's workflow-file tweaks (drop max-parallel: 1, lift releaseBody to an env var) are cosmetic. OAuth tokens cannot push workflow-file changes, and fine-grained PATs cannot honor maintainerCanModify on a third-party fork. Reverting the workflow file to wasimysaid's version lets the push go through without needing a classic PAT with both repo and workflow scopes. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: Lee Jackson <130007945+Imagineer99@users.noreply.github.com> Co-authored-by: Daniel Han Co-authored-by: Daniel Han Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .github/workflows/release-desktop.yml | 187 + .gitignore | 12 + install.ps1 | 78 +- install.sh | 69 +- studio/backend/auth/authentication.py | 50 +- studio/backend/auth/storage.py | 121 +- .../data_recipe/local_callable_validators.py | 6 + studio/backend/core/inference/audio_codecs.py | 9 +- studio/backend/core/inference/llama_cpp.py | 8 + studio/backend/core/training/trainer.py | 9 +- studio/backend/loggers/config.py | 8 + studio/backend/main.py | 17 +- studio/backend/models/auth.py | 6 + studio/backend/routes/auth.py | 25 +- studio/backend/routes/data_recipe/jobs.py | 15 + studio/backend/run.py | 23 +- studio/backend/tests/test_desktop_auth.py | 597 ++ studio/backend/utils/hardware/nvidia.py | 8 + studio/backend/utils/models/model_config.py | 5 + studio/backend/utils/subprocess_compat.py | 34 + studio/backend/utils/transformers_version.py | 6 + studio/frontend/package.json | 4 + studio/frontend/public/studio.png | Bin 0 -> 27420 bytes studio/frontend/src/app/auth-guards.ts | 65 +- studio/frontend/src/app/provider.tsx | 167 +- studio/frontend/src/app/routes/__root.tsx | 7 +- .../components/assistant-ui/markdown-text.tsx | 9 +- .../src/components/assistant-ui/sources.tsx | 15 +- .../src/components/tauri/startup-screen.tsx | 401 ++ .../src/components/tauri/update-banner.tsx | 84 + .../src/components/tauri/update-screen.tsx | 173 + .../src/components/ui/shimmer-button.tsx | 96 + studio/frontend/src/config/env.ts | 31 +- studio/frontend/src/features/auth/api.ts | 49 +- .../features/auth/components/auth-form.tsx | 16 +- studio/frontend/src/features/auth/index.ts | 5 + studio/frontend/src/features/auth/session.ts | 2 + .../src/features/auth/tauri-auto-auth.ts | 95 + .../src/features/settings/tabs/about-tab.tsx | 3 +- studio/frontend/src/hooks/index.ts | 1 + studio/frontend/src/hooks/use-gpu-info.ts | 3 +- .../frontend/src/hooks/use-tauri-backend.ts | 487 ++ studio/frontend/src/hooks/use-tauri-update.ts | 219 + studio/frontend/src/lib/api-base.ts | 24 + studio/frontend/src/lib/open-link.ts | 31 + studio/install_llama_prebuilt.py | 25 + studio/install_python_stack.py | 46 +- studio/setup.ps1 | 10 +- studio/setup.sh | 15 +- studio/src-tauri/Cargo.lock | 6320 +++++++++++++++++ studio/src-tauri/Cargo.toml | 42 + studio/src-tauri/Entitlements.plist | 15 + studio/src-tauri/build.rs | 3 + studio/src-tauri/capabilities/default.json | 24 + studio/src-tauri/icons/128x128.png | Bin 0 -> 9194 bytes studio/src-tauri/icons/32x32.png | Bin 0 -> 1930 bytes studio/src-tauri/icons/icon.icns | Bin 0 -> 263568 bytes studio/src-tauri/icons/icon.ico | Bin 0 -> 34589 bytes studio/src-tauri/icons/icon.png | Bin 0 -> 46007 bytes studio/src-tauri/linux/postremove.sh | 9 + studio/src-tauri/src/commands.rs | 476 ++ studio/src-tauri/src/desktop_auth.rs | 388 + studio/src-tauri/src/install.rs | 555 ++ studio/src-tauri/src/main.rs | 189 + studio/src-tauri/src/preflight.rs | 831 +++ studio/src-tauri/src/process.rs | 537 ++ studio/src-tauri/src/update.rs | 293 + studio/src-tauri/src/windows_job.rs | 72 + studio/src-tauri/tauri.conf.json | 66 + studio/src-tauri/tauri.macos.conf.json | 11 + studio/src-tauri/tauri.windows.conf.json | 16 + .../windows/branding/nsis-header.bmp | Bin 0 -> 25818 bytes .../windows/branding/nsis-sidebar.bmp | Bin 0 -> 154542 bytes studio/src-tauri/windows/hooks.nsh | 8 + tests/sh/test_tauri_install_exit_order.sh | 95 + unsloth_cli/commands/studio.py | 341 +- 76 files changed, 13533 insertions(+), 134 deletions(-) create mode 100644 .github/workflows/release-desktop.yml create mode 100644 studio/backend/tests/test_desktop_auth.py create mode 100644 studio/backend/utils/subprocess_compat.py create mode 100644 studio/frontend/public/studio.png create mode 100644 studio/frontend/src/components/tauri/startup-screen.tsx create mode 100644 studio/frontend/src/components/tauri/update-banner.tsx create mode 100644 studio/frontend/src/components/tauri/update-screen.tsx create mode 100644 studio/frontend/src/components/ui/shimmer-button.tsx create mode 100644 studio/frontend/src/features/auth/tauri-auto-auth.ts create mode 100644 studio/frontend/src/hooks/use-tauri-backend.ts create mode 100644 studio/frontend/src/hooks/use-tauri-update.ts create mode 100644 studio/frontend/src/lib/api-base.ts create mode 100644 studio/frontend/src/lib/open-link.ts create mode 100644 studio/src-tauri/Cargo.lock create mode 100644 studio/src-tauri/Cargo.toml create mode 100644 studio/src-tauri/Entitlements.plist create mode 100644 studio/src-tauri/build.rs create mode 100644 studio/src-tauri/capabilities/default.json create mode 100644 studio/src-tauri/icons/128x128.png create mode 100644 studio/src-tauri/icons/32x32.png create mode 100644 studio/src-tauri/icons/icon.icns create mode 100644 studio/src-tauri/icons/icon.ico create mode 100644 studio/src-tauri/icons/icon.png create mode 100755 studio/src-tauri/linux/postremove.sh create mode 100644 studio/src-tauri/src/commands.rs create mode 100644 studio/src-tauri/src/desktop_auth.rs create mode 100644 studio/src-tauri/src/install.rs create mode 100644 studio/src-tauri/src/main.rs create mode 100644 studio/src-tauri/src/preflight.rs create mode 100644 studio/src-tauri/src/process.rs create mode 100644 studio/src-tauri/src/update.rs create mode 100644 studio/src-tauri/src/windows_job.rs create mode 100644 studio/src-tauri/tauri.conf.json create mode 100644 studio/src-tauri/tauri.macos.conf.json create mode 100644 studio/src-tauri/tauri.windows.conf.json create mode 100644 studio/src-tauri/windows/branding/nsis-header.bmp create mode 100644 studio/src-tauri/windows/branding/nsis-sidebar.bmp create mode 100644 studio/src-tauri/windows/hooks.nsh create mode 100644 tests/sh/test_tauri_install_exit_order.sh diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml new file mode 100644 index 0000000000..2d1c6f51f2 --- /dev/null +++ b/.github/workflows/release-desktop.yml @@ -0,0 +1,187 @@ +name: Release Desktop App + +on: + workflow_dispatch: + inputs: + draft: + description: 'Create as draft release' + type: boolean + default: true + +permissions: + contents: write + +jobs: + build: + strategy: + fail-fast: false + max-parallel: 1 + matrix: + include: + - platform: macos-latest + args: '--target aarch64-apple-darwin' + label: macOS (Apple Silicon) + # - platform: macos-latest + # args: '--target x86_64-apple-darwin' + # label: macOS (Intel) + - platform: ubuntu-22.04 + args: '' + label: Linux (x64) + - platform: windows-latest + args: '' + label: Windows (x64) + + name: Build ${{ matrix.label }} + runs-on: ${{ matrix.platform }} + + env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + + + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + + # ── Linux dependencies ── + - name: Install Linux dependencies + if: matrix.platform == 'ubuntu-22.04' + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev libxdo-dev libssl-dev patchelf + + # ── Node.js ── + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + node-version: 24 + + - name: Install frontend dependencies + working-directory: studio/frontend + run: npm install + + # ── Rust ── + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} + + - name: Rust cache + uses: swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae + with: + workspaces: 'studio/src-tauri -> target' + + # ── macOS: import signing certificate ── + - name: Import Apple certificate + if: matrix.platform == 'macos-latest' + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + run: | + echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12 + security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain + security default-keychain -s build.keychain + security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain + security set-keychain-settings -t 3600 -u build.keychain + security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain + security find-identity -v -p codesigning build.keychain + rm -f certificate.p12 + + # ── Windows: install Azure Trusted Signing CLI ── + - name: Install trusted-signing-cli + if: matrix.platform == 'windows-latest' + run: | + cargo install trusted-signing-cli --version 0.9.0 --locked + echo "$env:USERPROFILE\.cargo\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + # ── Windows: verify signing CLI is accessible ── + - name: Verify trusted-signing-cli + if: matrix.platform == 'windows-latest' + run: | + Write-Output "PATH: $env:PATH" + Get-Command trusted-signing-cli -ErrorAction SilentlyContinue || Write-Output "trusted-signing-cli NOT in PATH" + trusted-signing-cli --version || Write-Output "trusted-signing-cli failed to run" + + # ── Linux: build + sign + upload ── + - name: Build Linux app + if: matrix.platform == 'ubuntu-22.04' + uses: tauri-apps/tauri-action@fce9c6108b31ea247710505d3aaaa893ee6768d4 + 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: studio + tagName: desktop-v__VERSION__ + releaseName: 'Unsloth Studio (Desktop) v__VERSION__' + releaseBody: | + Desktop app for Unsloth Studio. + + **macOS**: Download the Apple Silicon `.dmg`. + **Windows**: Download the `-setup.exe` installer. + **Linux**: Download `.deb` (Ubuntu/Debian) or `.AppImage` (universal). + + > Linux in-app updates are AppImage-oriented. Package installs should update by downloading a new package. + > Linux AppImage on Ubuntu 24.04+ may require: `sudo apt install libfuse2t64` + releaseDraft: ${{ inputs.draft }} + prerelease: false + args: -v ${{ matrix.args }} + + # ── macOS: build + sign + notarize + upload ── + - name: Build macOS app + if: matrix.platform == 'macos-latest' + uses: tauri-apps/tauri-action@fce9c6108b31ea247710505d3aaaa893ee6768d4 + 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 }} + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + with: + projectPath: studio + tagName: desktop-v__VERSION__ + releaseName: 'Unsloth Studio (Desktop) v__VERSION__' + releaseBody: | + Desktop app for Unsloth Studio. + + **macOS**: Download the Apple Silicon `.dmg`. + **Windows**: Download the `-setup.exe` installer. + **Linux**: Download `.deb` (Ubuntu/Debian) or `.AppImage` (universal). + + > Linux in-app updates are AppImage-oriented. Package installs should update by downloading a new package. + > Linux AppImage on Ubuntu 24.04+ may require: `sudo apt install libfuse2t64` + releaseDraft: ${{ inputs.draft }} + prerelease: false + args: -v ${{ matrix.args }} + + # ── Windows: build + sign + upload ── + - name: Build Windows app + if: matrix.platform == 'windows-latest' + uses: tauri-apps/tauri-action@fce9c6108b31ea247710505d3aaaa893ee6768d4 + 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 }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} + AZURE_CERTIFICATE_PROFILE_NAME: ${{ secrets.AZURE_CERTIFICATE_PROFILE_NAME }} + with: + projectPath: studio + tagName: desktop-v__VERSION__ + releaseName: 'Unsloth Studio (Desktop) v__VERSION__' + releaseBody: | + Desktop app for Unsloth Studio. + + **macOS**: Download the Apple Silicon `.dmg`. + **Windows**: Download the `-setup.exe` installer. + **Linux**: Download `.deb` (Ubuntu/Debian) or `.AppImage` (universal). + + > Linux in-app updates are AppImage-oriented. Package installs should update by downloading a new package. + > Linux AppImage on Ubuntu 24.04+ may require: `sudo apt install libfuse2t64` + releaseDraft: ${{ inputs.draft }} + prerelease: false + args: -v ${{ matrix.args }} diff --git a/.gitignore b/.gitignore index 7a24d07c6f..b6786ee655 100644 --- a/.gitignore +++ b/.gitignore @@ -204,6 +204,18 @@ tmp/ **/node_modules/ auth.db +# Tauri local build/generated output +studio/src-tauri/target/ +studio/src-tauri/gen/ +studio/src-tauri/artifacts/ +studio/src-tauri/icons/android/ +studio/src-tauri/icons/ios/ +studio/src-tauri/icons/128x128@2x.png +studio/src-tauri/icons/64x64.png +studio/src-tauri/icons/Square*Logo.png +studio/src-tauri/icons/StoreLogo.png +studio/src-tauri/icons/squarehq.png + # Local working docs **/CLAUDE.md **/claude.md diff --git a/install.ps1 b/install.ps1 index 7f98fc5cb8..44464101f3 100644 --- a/install.ps1 +++ b/install.ps1 @@ -12,11 +12,13 @@ function Install-UnslothStudio { $StudioLocalInstall = $false $PackageName = "unsloth" $RepoRoot = "" + $TauriMode = $false $SkipTorch = $false $argList = $args for ($i = 0; $i -lt $argList.Count; $i++) { switch ($argList[$i]) { "--local" { $StudioLocalInstall = $true } + "--tauri" { $TauriMode = $true } "--no-torch" { $SkipTorch = $true } "--verbose" { $script:UnslothVerbose = $true } "-v" { $script:UnslothVerbose = $true } @@ -44,6 +46,20 @@ function Install-UnslothStudio { } } + # Validate --package to prevent injection into shell/Python commands + if ($PackageName -notmatch '^[a-zA-Z0-9][a-zA-Z0-9._-]*$') { + Write-Host "[ERROR] --package name contains invalid characters (allowed: a-z A-Z 0-9 . _ -)" -ForegroundColor Red + return + } + + # ── Tauri structured output ── + function Write-TauriLog { + param([string]$Tag, [string]$Message) + if ($TauriMode) { + Write-Host "[TAURI:$Tag] $Message" + } + } + $PythonVersion = "3.13" $StudioHome = Join-Path $env:USERPROFILE ".unsloth\studio" $VenvDir = Join-Path $StudioHome "unsloth_studio" @@ -609,6 +625,7 @@ shell.Run cmd, 0, False } # ── Check winget ── + Write-TauriLog "STEP" "Checking system dependencies" if (-not (Get-Command winget -ErrorAction SilentlyContinue)) { step "winget" "not available" "Red" substep "Install it from https://aka.ms/getwinget" "Yellow" @@ -688,6 +705,7 @@ shell.Run cmd, 0, False # ── Install Python if no compatible version (3.11-3.13) found ── # Find-CompatiblePython returns @{ Version = "3.13"; Path = "C:\...\python.exe" } or $null. + Write-TauriLog "STEP" "Installing Python" $DetectedPython = Find-CompatiblePython if ($DetectedPython) { step "python" "Python $($DetectedPython.Version) already installed" @@ -736,6 +754,7 @@ shell.Run cmd, 0, False } # ── Install uv if not present ── + Write-TauriLog "STEP" "Installing uv package manager" if (-not (Get-Command uv -ErrorAction SilentlyContinue)) { substep "installing uv package manager..." $prevEAP = $ErrorActionPreference @@ -746,7 +765,7 @@ shell.Run cmd, 0, False # Fallback: if winget didn't put uv on PATH, try the PowerShell installer if (-not (Get-Command uv -ErrorAction SilentlyContinue)) { substep "trying alternative uv installer..." "Yellow" - powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" + Invoke-Expression (Invoke-RestMethod -Uri "https://astral.sh/uv/install.ps1") Refresh-SessionPath } } @@ -760,6 +779,7 @@ shell.Run cmd, 0, False # ── Create venv (migrate old layout if possible, otherwise fresh) ── # Pass the resolved executable path to uv so it does not re-resolve # a version string back to a conda interpreter. + Write-TauriLog "STEP" "Creating virtual environment" if (-not (Test-Path $StudioHome)) { New-Item -ItemType Directory -Path $StudioHome -Force | Out-Null } @@ -806,6 +826,7 @@ shell.Run cmd, 0, False substep "$VenvDir" $venvExit = Invoke-InstallCommand { uv venv $VenvDir --python "$($DetectedPython.Path)" } if ($venvExit -ne 0) { + Write-TauriLog "ERROR" "Failed to create virtual environment (exit code $venvExit)" Write-Host "[ERROR] Failed to create virtual environment (exit code $venvExit)" -ForegroundColor Red return } @@ -908,6 +929,7 @@ shell.Run cmd, 0, False if ($_Migrated) { # Migrated env: force-reinstall unsloth+unsloth-zoo to ensure clean state # in the new venv location, while preserving existing torch/CUDA + Write-TauriLog "STEP" "Installing unsloth" substep "upgrading unsloth in migrated environment..." if ($SkipTorch) { # No-torch: install unsloth + unsloth-zoo with --no-deps, then @@ -938,14 +960,17 @@ shell.Run cmd, 0, False if ($SkipTorch) { substep "skipping PyTorch (--no-torch flag set)." "Yellow" } else { + Write-TauriLog "STEP" "Installing PyTorch" substep "installing PyTorch ($TorchIndexUrl)..." $torchInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython "torch>=2.4,<2.11.0" torchvision torchaudio --index-url $TorchIndexUrl } if ($torchInstallExit -ne 0) { + Write-TauriLog "ERROR" "Failed to install PyTorch (exit code $torchInstallExit)" Write-Host "[ERROR] Failed to install PyTorch (exit code $torchInstallExit)" -ForegroundColor Red return } } + Write-TauriLog "STEP" "Installing unsloth" substep "installing unsloth (this may take a few minutes)..." if ($SkipTorch) { # No-torch: install unsloth + unsloth-zoo with --no-deps, then @@ -960,9 +985,10 @@ shell.Run cmd, 0, False } elseif ($StudioLocalInstall) { $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --upgrade-package unsloth "unsloth>=2026.4.7" unsloth-zoo } } else { - $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --upgrade-package unsloth "$PackageName" } + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --upgrade-package unsloth -- "$PackageName" } } if ($baseInstallExit -ne 0) { + Write-TauriLog "ERROR" "Failed to install unsloth (exit code $baseInstallExit)" Write-Host "[ERROR] Failed to install unsloth (exit code $baseInstallExit)" -ForegroundColor Red return } @@ -977,6 +1003,7 @@ shell.Run cmd, 0, False } } else { # Fallback: GPU detection failed to produce a URL -- let uv resolve torch + Write-TauriLog "STEP" "Installing unsloth" substep "installing unsloth (this may take a few minutes)..." if ($StudioLocalInstall) { $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython unsloth-zoo "unsloth>=2026.4.7" --torch-backend=auto } @@ -991,20 +1018,52 @@ shell.Run cmd, 0, False return } } else { - $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython "$PackageName" --torch-backend=auto } + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --torch-backend=auto -- "$PackageName" } if ($baseInstallExit -ne 0) { + Write-TauriLog "ERROR" "Failed to install unsloth (exit code $baseInstallExit)" Write-Host "[ERROR] Failed to install unsloth (exit code $baseInstallExit)" -ForegroundColor Red return } } } + # Hotfix: patch install_python_stack.py for Windows GUI stdout + # The PyPI version crashes with OSError when stdout is piped from a GUI app. + # Copy our fixed version (bundled by Tauri) over the installed one. + # Remove this block once PyPI ships the fix from commit 18c5aae7. + if ($TauriMode) { + $rawPath = if ($PSCommandPath) { $PSCommandPath } else { $MyInvocation.ScriptName } + $scriptDir = Split-Path -Parent ($rawPath -replace '^\\\\\?\\', '') + $fixedPy = Join-Path $scriptDir "install_python_stack.py" + $target = Join-Path $VenvDir "Lib\site-packages\studio\install_python_stack.py" + $sentinel = "# UNSLOTH_DESKTOP_HOTFIX_APPLIED_v1" + $sentinelPattern = [regex]::Escape($sentinel) + if ((Test-Path $fixedPy) -and (Test-Path $target)) { + $installed = Get-Content $target -Raw + if ($installed -notmatch $sentinelPattern) { + Copy-Item $fixedPy $target -Force + Add-Content -Path $target -Value "`n$sentinel" + substep "patched install_python_stack.py (stdout fix)" + } else { + substep "install_python_stack.py already has stdout fix" + } + } elseif ((Test-Path $fixedPy) -and (Test-Path (Split-Path $target))) { + Copy-Item $fixedPy $target -Force + Add-Content -Path $target -Value "`n$sentinel" + substep "patched install_python_stack.py (stdout fix)" + } else { + Write-Host "[WARN] Could not patch install_python_stack.py (bundled file or target dir missing)" -ForegroundColor Yellow + } + } + # ── Run studio setup ── # setup.ps1 will handle installing Git, CMake, Visual Studio Build Tools, # CUDA Toolkit, Node.js, and other dependencies automatically via winget. + Write-TauriLog "STEP" "Running studio setup" step "setup" "running unsloth studio setup..." $UnslothExe = Join-Path $VenvDir "Scripts\unsloth.exe" if (-not (Test-Path $UnslothExe)) { + Write-TauriLog "ERROR" "unsloth CLI was not installed correctly" Write-Host "[ERROR] unsloth CLI was not installed correctly." -ForegroundColor Red Write-Host " Expected: $UnslothExe" -ForegroundColor Yellow Write-Host " This usually means an older unsloth version was installed that does not include the Studio CLI." -ForegroundColor Yellow @@ -1015,6 +1074,8 @@ shell.Run cmd, 0, False $env:SKIP_STUDIO_BASE = "1" $env:STUDIO_PACKAGE_NAME = $PackageName $env:UNSLOTH_NO_TORCH = if ($SkipTorch) { "true" } else { "false" } + # Tauri desktop app bundles its own frontend — skip Node/npm/frontend build + $env:SKIP_STUDIO_FRONTEND = if ($TauriMode) { "1" } else { "0" } # Always set STUDIO_LOCAL_INSTALL explicitly to avoid stale values from # a previous --local run in the same PowerShell session. if ($StudioLocalInstall) { @@ -1032,12 +1093,11 @@ shell.Run cmd, 0, False & $UnslothExe @studioArgs $setupExit = $LASTEXITCODE if ($setupExit -ne 0) { + Write-TauriLog "ERROR" "unsloth studio setup failed (exit code $setupExit)" Write-Host "[ERROR] unsloth studio setup failed (exit code $setupExit)" -ForegroundColor Red return } - New-StudioShortcuts -UnslothExePath $UnslothExe - # ── Expose `unsloth` via a shim dir containing only unsloth.exe ── # We do NOT add the venv Scripts dir to PATH (it also holds python.exe # and pip.exe, which would hijack the user's system interpreter). @@ -1109,6 +1169,14 @@ shell.Run cmd, 0, False } Refresh-SessionPath # sync current session with registry + # ── Tauri mode: done, skip shortcuts and auto-launch ── + if ($TauriMode) { + Write-TauriLog "DONE" "" + return + } + + New-StudioShortcuts -UnslothExePath $UnslothExe + # Launch studio automatically in interactive terminals; # in non-interactive environments (CI, Docker) just print instructions. $IsInteractive = [Environment]::UserInteractive -and (-not [Console]::IsInputRedirected) diff --git a/install.sh b/install.sh index b1663ba0ff..07473e441d 100755 --- a/install.sh +++ b/install.sh @@ -35,6 +35,7 @@ substep() { printf " ${C_DIM}%-15s${2:-$C_DIM}%s${C_RST}\n" "" "$1"; } # ── Parse flags ── STUDIO_LOCAL_INSTALL=false PACKAGE_NAME="unsloth" +TAURI_MODE=false _USER_PYTHON="" _NO_TORCH_FLAG=false _VERBOSE=false @@ -54,6 +55,7 @@ for arg in "$@"; do case "$arg" in --local) STUDIO_LOCAL_INSTALL=true ;; --package) _next_is_package=true ;; + --tauri) TAURI_MODE=true ;; --python) _next_is_python=true ;; --no-torch) _NO_TORCH_FLAG=true ;; --verbose|-v) _VERBOSE=true ;; @@ -142,6 +144,24 @@ if [ "$_next_is_python" = true ]; then exit 1 fi +# Validate --package to prevent injection into shell/Python commands. +# Must start with a letter/digit (rejects leading dashes that uv would parse as flags). +case "$PACKAGE_NAME" in + [!a-zA-Z0-9]*) + echo "❌ ERROR: --package name must start with a letter or digit." >&2 + exit 1 ;; + *[!a-zA-Z0-9._-]*) + echo "❌ ERROR: --package name contains invalid characters (allowed: a-z A-Z 0-9 . _ -)" >&2 + exit 1 ;; +esac + +# ── Tauri structured output ── +tauri_log() { + if [ "$TAURI_MODE" = true ]; then + echo "[TAURI:$1] $2" + fi +} + PYTHON_VERSION="" # resolved after platform detection STUDIO_HOME="$HOME/.unsloth/studio" VENV_DIR="$STUDIO_HOME/unsloth_studio" @@ -192,6 +212,12 @@ _smart_apt_install() { return 0 fi + # In Tauri mode, report needed packages and exit — Rust handles elevation + if [ "$TAURI_MODE" = true ]; then + tauri_log "NEED_SUDO" "$_STILL_MISSING" + exit 2 + fi + # Step 3: Escalate -- need elevated permissions for remaining packages if command -v sudo >/dev/null 2>&1; then echo "" @@ -752,6 +778,7 @@ printf " ${C_DIM}%s${C_RST}\n" "$RULE" echo "" # ── Detect platform ── +tauri_log "STEP" "Detecting platform" OS="linux" if [ "$(uname)" = "Darwin" ]; then OS="macos" @@ -804,6 +831,7 @@ fi # ── Check system dependencies ── # cmake and git are needed by unsloth studio setup to build the GGUF inference # engine (llama.cpp). build-essential and libcurl-dev are also needed on Linux. +tauri_log "STEP" "Checking system dependencies" MISSING="" command -v cmake >/dev/null 2>&1 || MISSING="$MISSING cmake" @@ -868,6 +896,7 @@ else fi # ── Install uv ── +tauri_log "STEP" "Installing uv package manager" UV_MIN_VERSION="0.7.14" version_ge() { @@ -922,6 +951,7 @@ if ! command -v uv >/dev/null 2>&1 || ! _uv_version_ok uv; then fi # ── Create venv (migrate old layout if possible, otherwise fresh) ── +tauri_log "STEP" "Creating virtual environment" mkdir -p "$STUDIO_HOME" _MIGRATED=false @@ -1304,6 +1334,7 @@ case "$TORCH_INDEX_URL" in esac # ── Install unsloth directly into the venv (no activation needed) ── +tauri_log "STEP" "Installing PyTorch" _VENV_PY="$VENV_DIR/bin/python" if [ "$_MIGRATED" = true ]; then # Migrated env: force-reinstall unsloth+unsloth-zoo to ensure clean state @@ -1481,6 +1512,7 @@ elif [ -n "$TORCH_INDEX_URL" ]; then esac fi # Fresh: Step 2 - install unsloth, preserving pre-installed torch + tauri_log "STEP" "Installing Unsloth" substep "installing unsloth (this may take a few minutes)..." if [ "$SKIP_TORCH" = true ]; then # No-torch: install unsloth + unsloth-zoo with --no-deps, then @@ -1503,7 +1535,7 @@ elif [ -n "$TORCH_INDEX_URL" ]; then run_install_cmd "overlay local repo" uv pip install --python "$_VENV_PY" -e "$_REPO_ROOT" --no-deps else run_install_cmd "install unsloth" uv pip install --python "$_VENV_PY" \ - --upgrade-package unsloth "$PACKAGE_NAME" + --upgrade-package unsloth -- "$PACKAGE_NAME" fi # AMD ROCm: repair torch if the unsloth/unsloth-zoo install pulled in # CUDA torch from PyPI, overwriting the ROCm wheels installed in Step 1. @@ -1523,17 +1555,19 @@ elif [ -n "$TORCH_INDEX_URL" ]; then fi else # Fallback: GPU detection failed to produce a URL -- let uv resolve torch + tauri_log "STEP" "Installing Unsloth" substep "installing unsloth (this may take a few minutes)..." if [ "$STUDIO_LOCAL_INSTALL" = true ]; then run_install_cmd "install unsloth (auto torch backend)" uv pip install --python "$_VENV_PY" unsloth-zoo "unsloth>=2026.4.7" --torch-backend=auto substep "overlaying local repo (editable)..." run_install_cmd "overlay local repo" uv pip install --python "$_VENV_PY" -e "$_REPO_ROOT" --no-deps else - run_install_cmd "install unsloth (auto torch backend)" uv pip install --python "$_VENV_PY" "$PACKAGE_NAME" --torch-backend=auto + run_install_cmd "install unsloth (auto torch backend)" uv pip install --python "$_VENV_PY" --torch-backend=auto -- "$PACKAGE_NAME" fi fi # ── Run studio setup ── +tauri_log "STEP" "Running Studio setup" # When --local, use the repo's own setup.sh directly. # Otherwise, find it inside the installed package. SETUP_SH="" @@ -1554,6 +1588,7 @@ if [ -z "$SETUP_SH" ] || [ ! -f "$SETUP_SH" ]; then fi if [ -z "$SETUP_SH" ] || [ ! -f "$SETUP_SH" ]; then + tauri_log "ERROR" "Could not find studio/setup.sh in the installed package" echo "❌ ERROR: Could not find studio/setup.sh in the installed package." exit 1 fi @@ -1571,16 +1606,16 @@ if ! command -v bash >/dev/null 2>&1; then fi step "setup" "running unsloth studio update..." -# install.sh already installs base packages (unsloth + unsloth-zoo) and -# no-torch-runtime.txt above, so tell install_python_stack.py to skip -# the base step to avoid redundant reinstallation. _SKIP_BASE=1 -# Run setup.sh outside set -e so that a llama.cpp build failure (exit 1) -# does not skip PATH setup, shortcuts, and launch below. We capture the -# exit code and propagate it after post-install steps finish. _SETUP_EXIT=0 +# Tauri desktop app bundles its own frontend — skip Node/npm/frontend build +_SKIP_FRONTEND=0 +if [ "$TAURI_MODE" = true ]; then + _SKIP_FRONTEND=1 +fi if [ "$STUDIO_LOCAL_INSTALL" = true ]; then SKIP_STUDIO_BASE="$_SKIP_BASE" \ + SKIP_STUDIO_FRONTEND="$_SKIP_FRONTEND" \ STUDIO_PACKAGE_NAME="$PACKAGE_NAME" \ STUDIO_LOCAL_INSTALL=1 \ STUDIO_LOCAL_REPO="$_REPO_ROOT" \ @@ -1593,6 +1628,7 @@ else # local-dev path in setup.sh and install_python_stack.py. Mirrors the # reset already done in install.ps1 for PowerShell. SKIP_STUDIO_BASE="$_SKIP_BASE" \ + SKIP_STUDIO_FRONTEND="$_SKIP_FRONTEND" \ STUDIO_PACKAGE_NAME="$PACKAGE_NAME" \ STUDIO_LOCAL_INSTALL=0 \ STUDIO_LOCAL_REPO= \ @@ -1629,23 +1665,26 @@ case ":$PATH:" in ;; esac -create_studio_shortcuts "$VENV_ABS_BIN/unsloth" "$OS" +# Non-Tauri installs keep shortcuts even if setup reports failure. +if [ "$TAURI_MODE" != true ]; then + create_studio_shortcuts "$VENV_ABS_BIN/unsloth" "$OS" +fi # If setup.sh failed, report and exit now. # PATH and shortcuts are already set up so the user can fix and retry. if [ "$_SETUP_EXIT" -ne 0 ]; then echo "" step "error" "studio setup failed (exit code $_SETUP_EXIT)" "$C_ERR" - substep "Check the output above for details, then re-run:" - if [ "$STUDIO_LOCAL_INSTALL" = true ]; then - substep " unsloth studio update --local" - else - substep " unsloth studio update" - fi echo "" exit "$_SETUP_EXIT" fi +# ── Tauri mode: done, skip shortcuts and auto-launch ── +if [ "$TAURI_MODE" = true ]; then + tauri_log "DONE" "" + exit 0 +fi + echo "" printf " ${C_TITLE}%s${C_RST}\n" "Unsloth Studio installed!" printf " ${C_DIM}%s${C_RST}\n" "$RULE" diff --git a/studio/backend/auth/authentication.py b/studio/backend/auth/authentication.py index da59ba9a1a..6ddcbc8e0b 100644 --- a/studio/backend/auth/authentication.py +++ b/studio/backend/auth/authentication.py @@ -52,6 +52,8 @@ def _decode_subject_without_verification(token: str) -> Optional[str]: def create_access_token( subject: str, expires_delta: Optional[timedelta] = None, + *, + desktop: bool = False, ) -> str: """ Create a signed JWT for the given subject (e.g. username). @@ -59,6 +61,8 @@ def create_access_token( Tokens are valid across restarts because the signing secret is stored in SQLite. """ to_encode = {"sub": subject} + if desktop: + to_encode["desktop"] = True expire = datetime.now(timezone.utc) + ( expires_delta or timedelta(minutes = ACCESS_TOKEN_EXPIRE_MINUTES) ) @@ -70,7 +74,29 @@ def create_access_token( ) -def create_refresh_token(subject: str) -> str: +def is_desktop_access_token(token: str) -> bool: + """Return true only for a valid desktop-issued JWT access token.""" + if token.startswith(API_KEY_PREFIX): + return False + + subject = _decode_subject_without_verification(token) + if subject is None: + return False + + record = get_user_and_secret(subject) + if record is None: + return False + + _salt, _pwd_hash, jwt_secret, _must_change_password = record + try: + payload = jwt.decode(token, jwt_secret, algorithms = [ALGORITHM]) + except jwt.InvalidTokenError: + return False + + return payload.get("sub") == subject and payload.get("desktop") is True + + +def create_refresh_token(subject: str, *, desktop: bool = False) -> str: """ Create a random refresh token, store its hash in SQLite, and return it. @@ -78,21 +104,28 @@ def create_refresh_token(subject: str) -> str: """ token = secrets.token_urlsafe(48) expires_at = datetime.now(timezone.utc) + timedelta(days = REFRESH_TOKEN_EXPIRE_DAYS) - save_refresh_token(token, subject, expires_at.isoformat()) + save_refresh_token(token, subject, expires_at.isoformat(), is_desktop = desktop) return token -def refresh_access_token(refresh_token: str) -> Tuple[Optional[str], Optional[str]]: +def refresh_access_token( + refresh_token: str, +) -> Tuple[Optional[str], Optional[str], bool]: """ Validate a refresh token and issue a new access token. The refresh token itself is NOT consumed — it stays valid until expiry. Returns a new access_token or None if the refresh token is invalid/expired. """ - username = verify_refresh_token(refresh_token) - if username is None: - return None, None - return create_access_token(subject = username), username + verified = verify_refresh_token(refresh_token) + if verified is None: + return None, None, False + username, is_desktop = verified + return ( + create_access_token(subject = username, desktop = is_desktop), + username, + is_desktop, + ) def reload_secret() -> None: @@ -173,7 +206,8 @@ async def _get_current_subject( status_code = status.HTTP_401_UNAUTHORIZED, detail = "Invalid token payload", ) - if must_change_password and not allow_password_change: + is_desktop = payload.get("desktop") is True + if must_change_password and not allow_password_change and not is_desktop: raise HTTPException( status_code = status.HTTP_403_FORBIDDEN, detail = "Password change required", diff --git a/studio/backend/auth/storage.py b/studio/backend/auth/storage.py index 7d55a2dc59..2b0e359d39 100644 --- a/studio/backend/auth/storage.py +++ b/studio/backend/auth/storage.py @@ -6,6 +6,7 @@ SQLite storage for authentication data (user credentials + JWT secret). """ import hashlib +import os import secrets import sqlite3 from datetime import datetime, timezone @@ -54,6 +55,10 @@ def generate_bootstrap_password() -> str: # before the user changes the password. ensure_dir(_BOOTSTRAP_PW_PATH.parent) _BOOTSTRAP_PW_PATH.write_text(_bootstrap_password) + try: + os.chmod(_BOOTSTRAP_PW_PATH, 0o600) + except OSError: + pass return _bootstrap_password @@ -63,6 +68,17 @@ def get_bootstrap_password() -> Optional[str]: return _bootstrap_password +def _load_bootstrap_password() -> Optional[str]: + """Load an existing bootstrap password without creating one.""" + global _bootstrap_password + _bootstrap_password = None + if _BOOTSTRAP_PW_PATH.is_file(): + bootstrap_password = _BOOTSTRAP_PW_PATH.read_text().strip() + if bootstrap_password: + _bootstrap_password = bootstrap_password + return _bootstrap_password + + def clear_bootstrap_password() -> None: """Delete the persisted bootstrap password file (called after password change).""" global _bootstrap_password @@ -114,7 +130,8 @@ def get_connection() -> sqlite3.Connection: id INTEGER PRIMARY KEY, token_hash TEXT NOT NULL, username TEXT NOT NULL, - expires_at TEXT NOT NULL + expires_at TEXT NOT NULL, + is_desktop INTEGER NOT NULL DEFAULT 0 ); """ ) @@ -146,6 +163,13 @@ def get_connection() -> sqlite3.Connection: conn.execute( "ALTER TABLE auth_user ADD COLUMN must_change_password INTEGER NOT NULL DEFAULT 0" ) + refresh_columns = { + row["name"] for row in conn.execute("PRAGMA table_info(refresh_tokens)") + } + if "is_desktop" not in refresh_columns: + conn.execute( + "ALTER TABLE refresh_tokens ADD COLUMN is_desktop INTEGER NOT NULL DEFAULT 0" + ) conn.commit() return conn @@ -201,6 +225,9 @@ def _get_or_create_api_key_pbkdf2_salt() -> bytes: _API_KEY_PBKDF2_ITERATIONS = 100_000 +DESKTOP_SECRET_PREFIX = "desktop-" +_DESKTOP_SECRET_HASH_KEY = "desktop_secret_hash" +_DESKTOP_SECRET_CREATED_AT_KEY = "desktop_secret_created_at" def _pbkdf2_api_key(raw_key: str) -> str: @@ -233,6 +260,10 @@ def _pbkdf2_api_key(raw_key: str) -> str: return dk.hex() +def _pbkdf2_desktop_secret(raw_secret: str) -> str: + return _pbkdf2_api_key(raw_secret) + + def is_initialized() -> bool: """Check if auth is ready for login (at least one user exists in DB).""" conn = get_connection() @@ -374,6 +405,10 @@ def ensure_default_admin() -> bool: Uses a randomly generated diceware passphrase as the bootstrap password. Returns True when the default admin was created in this call. """ + if get_user_and_secret(DEFAULT_ADMIN_USERNAME) is not None: + _load_bootstrap_password() + return False + bootstrap_pw = generate_bootstrap_password() try: create_initial_user( @@ -406,12 +441,19 @@ def update_password(username: str, new_password: str) -> bool: conn.commit() if cursor.rowcount > 0: clear_bootstrap_password() + clear_desktop_secret() return cursor.rowcount > 0 finally: conn.close() -def save_refresh_token(token: str, username: str, expires_at: str) -> None: +def save_refresh_token( + token: str, + username: str, + expires_at: str, + *, + is_desktop: bool = False, +) -> None: """ Store a hashed refresh token with its associated username and expiry. """ @@ -420,21 +462,21 @@ def save_refresh_token(token: str, username: str, expires_at: str) -> None: try: conn.execute( """ - INSERT INTO refresh_tokens (token_hash, username, expires_at) - VALUES (?, ?, ?) + INSERT INTO refresh_tokens (token_hash, username, expires_at, is_desktop) + VALUES (?, ?, ?, ?) """, - (token_hash, username, expires_at), + (token_hash, username, expires_at, int(is_desktop)), ) conn.commit() finally: conn.close() -def verify_refresh_token(token: str) -> Optional[str]: +def verify_refresh_token(token: str) -> Optional[Tuple[str, bool]]: """ - Verify a refresh token and return the username. + Verify a refresh token and return the username plus desktop marker. - Returns the username if valid and not expired, None otherwise. + Returns the username and desktop marker if valid and not expired, None otherwise. The token is NOT consumed — it stays valid until it expires. """ token_hash = _hash_token(token) @@ -449,7 +491,7 @@ def verify_refresh_token(token: str) -> Optional[str]: cur = conn.execute( """ - SELECT id, username, expires_at FROM refresh_tokens + SELECT id, username, expires_at, is_desktop FROM refresh_tokens WHERE token_hash = ? """, (token_hash,), @@ -465,7 +507,7 @@ def verify_refresh_token(token: str) -> Optional[str]: conn.commit() return None - return row["username"] + return row["username"], bool(row["is_desktop"]) finally: conn.close() @@ -480,6 +522,65 @@ def revoke_user_refresh_tokens(username: str) -> None: conn.close() +def create_desktop_secret() -> str: + """Create/rotate the local desktop credential and return it once.""" + ensure_default_admin() + raw_secret = DESKTOP_SECRET_PREFIX + secrets.token_urlsafe(48) + secret_hash = _pbkdf2_desktop_secret(raw_secret) + now = datetime.now(timezone.utc).isoformat() + conn = get_connection() + try: + conn.execute( + "INSERT OR REPLACE INTO app_secrets (key, value) VALUES (?, ?)", + (_DESKTOP_SECRET_HASH_KEY, secret_hash), + ) + conn.execute( + "INSERT OR REPLACE INTO app_secrets (key, value) VALUES (?, ?)", + (_DESKTOP_SECRET_CREATED_AT_KEY, now), + ) + conn.commit() + return raw_secret + finally: + conn.close() + + +def validate_desktop_secret(raw_secret: str) -> Optional[str]: + """Return the real admin username when the desktop secret matches.""" + if not raw_secret.startswith(DESKTOP_SECRET_PREFIX): + return None + if get_user_and_secret(DEFAULT_ADMIN_USERNAME) is None: + return None + + secret_hash = _pbkdf2_desktop_secret(raw_secret) + conn = get_connection() + try: + cur = conn.execute( + "SELECT value FROM app_secrets WHERE key = ?", + (_DESKTOP_SECRET_HASH_KEY,), + ) + row = cur.fetchone() + if row is None: + return None + if not secrets.compare_digest(row["value"], secret_hash): + return None + return DEFAULT_ADMIN_USERNAME + finally: + conn.close() + + +def clear_desktop_secret() -> None: + """Remove backend-side desktop auth state.""" + conn = get_connection() + try: + conn.execute( + "DELETE FROM app_secrets WHERE key IN (?, ?)", + (_DESKTOP_SECRET_HASH_KEY, _DESKTOP_SECRET_CREATED_AT_KEY), + ) + conn.commit() + finally: + conn.close() + + # --------------------------------------------------------------------------- # API key management # --------------------------------------------------------------------------- diff --git a/studio/backend/core/data_recipe/local_callable_validators.py b/studio/backend/core/data_recipe/local_callable_validators.py index c32b2fccaf..afd10b02d1 100644 --- a/studio/backend/core/data_recipe/local_callable_validators.py +++ b/studio/backend/core/data_recipe/local_callable_validators.py @@ -33,6 +33,11 @@ _OXC_TOOL_DIR = Path(__file__).resolve().parent / "oxc-validator" _OXC_RUNNER_PATH = _OXC_TOOL_DIR / "validate.mjs" +from utils.subprocess_compat import ( + windows_hidden_subprocess_kwargs as _windows_hidden_subprocess_kwargs, +) + + @dataclass(frozen = True) class OxcLocalCallableValidatorSpec: name: str @@ -256,6 +261,7 @@ def _run_oxc_batch( capture_output = True, check = False, env = env, + **_windows_hidden_subprocess_kwargs(), ) except (OSError, ValueError) as exc: logger.warning("OXC subprocess launch failed: %s", exc) diff --git a/studio/backend/core/inference/audio_codecs.py b/studio/backend/core/inference/audio_codecs.py index bcf3ec2937..895b112e85 100644 --- a/studio/backend/core/inference/audio_codecs.py +++ b/studio/backend/core/inference/audio_codecs.py @@ -8,6 +8,7 @@ Supports: SNAC (Orpheus), CSM (Sesame), BiCodec (Spark), DAC (OuteTTS) import io import re +import subprocess import wave import structlog from loggers import get_logger @@ -16,6 +17,10 @@ from typing import Optional, Tuple import numpy as np import torch +from utils.subprocess_compat import ( + windows_hidden_subprocess_kwargs as _windows_hidden_subprocess_kwargs, +) + logger = get_logger(__name__) @@ -81,7 +86,6 @@ class AudioCodecManager: return import os import sys - import subprocess # Clone SparkAudio/Spark-TTS GitHub repo for the sparktts Python package # (same approach as training — the HF model repos don't contain the package) @@ -101,6 +105,7 @@ class AudioCodecManager: spark_code_dir, ], check = True, + **_windows_hidden_subprocess_kwargs(), ) if spark_code_dir not in sys.path: @@ -119,7 +124,6 @@ class AudioCodecManager: return import os import sys - import subprocess # Clone OuteTTS repo (same pattern as Spark-TTS / BiCodec) # The pip package has problematic dependencies; the notebook clones and @@ -139,6 +143,7 @@ class AudioCodecManager: outetts_code_dir, ], check = True, + **_windows_hidden_subprocess_kwargs(), ) # Remove files that pull in heavy / incompatible dependencies # (matches notebook: gguf_model.py is under models/, others under outetts/) diff --git a/studio/backend/core/inference/llama_cpp.py b/studio/backend/core/inference/llama_cpp.py index 2e26995309..6ad05ac52d 100644 --- a/studio/backend/core/inference/llama_cpp.py +++ b/studio/backend/core/inference/llama_cpp.py @@ -18,6 +18,7 @@ from loggers import get_logger import shutil import socket import subprocess +import sys import threading import time from pathlib import Path @@ -26,8 +27,13 @@ from urllib.parse import urlparse import httpx +from utils.subprocess_compat import ( + windows_hidden_subprocess_kwargs as _windows_hidden_subprocess_kwargs, +) + logger = get_logger(__name__) + # ── Pre-compiled patterns for plan-without-action re-prompt ── # Forward-looking intent signals that indicate the model is # describing what it *will* do rather than giving a final answer. @@ -440,6 +446,7 @@ class LlamaCppBackend: capture_output = True, text = True, timeout = 10, + **_windows_hidden_subprocess_kwargs(), ) if result.returncode != 0: return [] @@ -1658,6 +1665,7 @@ class LlamaCppBackend: stderr = subprocess.STDOUT, text = True, env = env, + **_windows_hidden_subprocess_kwargs(), ) # Start background thread to drain stdout and prevent pipe deadlock diff --git a/studio/backend/core/training/trainer.py b/studio/backend/core/training/trainer.py index 77cbda6b45..c1e2ac4a85 100644 --- a/studio/backend/core/training/trainer.py +++ b/studio/backend/core/training/trainer.py @@ -49,6 +49,7 @@ from unsloth.chat_templates import get_chat_template import json import threading import math +import subprocess import structlog from loggers import get_logger import time @@ -69,6 +70,10 @@ from utils.paths import ( ) from trl import SFTTrainer, SFTConfig +from utils.subprocess_compat import ( + windows_hidden_subprocess_kwargs as _windows_hidden_subprocess_kwargs, +) + logger = get_logger(__name__) @@ -1765,6 +1770,7 @@ class UnslothTrainer: spark_code_dir, ], check = True, + **_windows_hidden_subprocess_kwargs(), ) if spark_code_dir not in sys.path: @@ -1982,8 +1988,6 @@ class UnslothTrainer: device = "cuda" if torch.cuda.is_available() else "cpu" # Clone OuteTTS repo (same as audio_codecs._load_dac) - import subprocess - base_dir = os.path.dirname(os.path.abspath(__file__)) outetts_code_dir = os.path.join(base_dir, "inference", "OuteTTS") outetts_pkg = os.path.join(outetts_code_dir, "outetts") @@ -2000,6 +2004,7 @@ class UnslothTrainer: outetts_code_dir, ], check = True, + **_windows_hidden_subprocess_kwargs(), ) for fpath in [ os.path.join(outetts_pkg, "models", "gguf_model.py"), diff --git a/studio/backend/loggers/config.py b/studio/backend/loggers/config.py index 0d32a64657..4c0d8ade28 100644 --- a/studio/backend/loggers/config.py +++ b/studio/backend/loggers/config.py @@ -44,6 +44,14 @@ class LogConfig: # Fallback to INFO if an invalid level is provided log_level = getattr(logging, log_level_name, logging.INFO) + if sys.platform == "win32": + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + try: + stream.reconfigure(encoding = "utf-8", errors = "replace") + except Exception: + pass + structlog.configure( processors = [ # Reorder processors to control field order diff --git a/studio/backend/main.py b/studio/backend/main.py index d146a8ef12..05adcaa2ea 100644 --- a/studio/backend/main.py +++ b/studio/backend/main.py @@ -179,9 +179,22 @@ logger = LogConfig.setup_logging( app.add_middleware(LoggingMiddleware) # CORS middleware +_api_only = os.environ.get("UNSLOTH_API_ONLY") == "1" +_cors_origins = ["*"] +if _api_only: + _cors_origins = [ + "tauri://localhost", # Linux/macOS Tauri webview + "http://tauri.localhost", # Windows Tauri webview + "http://localhost", # dev fallback + ] + _cors_origin_regex = None +else: + _cors_origin_regex = None + app.add_middleware( CORSMiddleware, - allow_origins = ["*"], # In production, specify allowed origins + allow_origins = _cors_origins, + allow_origin_regex = _cors_origin_regex, allow_credentials = True, allow_methods = ["*"], allow_headers = ["*"], @@ -223,6 +236,8 @@ async def health_check(): "version": UNSLOTH_VERSION, "device_type": device_type, "chat_only": _hw_module.CHAT_ONLY, + "desktop_protocol_version": 1, + "supports_desktop_auth": True, } diff --git a/studio/backend/models/auth.py b/studio/backend/models/auth.py index c55e646508..23eb0ac4c0 100644 --- a/studio/backend/models/auth.py +++ b/studio/backend/models/auth.py @@ -17,6 +17,12 @@ class AuthLoginRequest(BaseModel): password: str = Field(..., description = "Password") +class DesktopLoginRequest(BaseModel): + """Desktop-only local secret exchange payload.""" + + secret: str = Field(..., description = "Desktop local auth secret") + + class RefreshTokenRequest(BaseModel): """Refresh token payload to obtain new access + refresh tokens.""" diff --git a/studio/backend/routes/auth.py b/studio/backend/routes/auth.py index 5cd23bd450..3deeb6793b 100644 --- a/studio/backend/routes/auth.py +++ b/studio/backend/routes/auth.py @@ -17,6 +17,7 @@ from models.auth import ( ChangePasswordRequest, CreateApiKeyRequest, CreateApiKeyResponse, + DesktopLoginRequest, RefreshTokenRequest, ) from models.users import Token @@ -80,6 +81,24 @@ async def login(payload: AuthLoginRequest) -> Token: ) +@router.post("/desktop-login", response_model = Token) +async def desktop_login(payload: DesktopLoginRequest) -> Token: + """Exchange a local desktop secret for normal admin-subject tokens.""" + username = storage.validate_desktop_secret(payload.secret) + if username is None: + raise HTTPException( + status_code = status.HTTP_401_UNAUTHORIZED, + detail = "Desktop authentication failed", + ) + + return Token( + access_token = create_access_token(subject = username, desktop = True), + refresh_token = create_refresh_token(subject = username, desktop = True), + token_type = "bearer", + must_change_password = False, + ) + + @router.post("/refresh", response_model = Token) async def refresh(payload: RefreshTokenRequest) -> Token: """ @@ -87,7 +106,7 @@ async def refresh(payload: RefreshTokenRequest) -> Token: The refresh token itself is reusable until it expires (7 days). """ - new_access_token, username = refresh_access_token(payload.refresh_token) + new_access_token, username, is_desktop = refresh_access_token(payload.refresh_token) if new_access_token is None or username is None: raise HTTPException( status_code = status.HTTP_401_UNAUTHORIZED, @@ -98,7 +117,9 @@ async def refresh(payload: RefreshTokenRequest) -> Token: access_token = new_access_token, refresh_token = payload.refresh_token, token_type = "bearer", - must_change_password = storage.requires_password_change(username), + must_change_password = False + if is_desktop + else storage.requires_password_change(username), ) diff --git a/studio/backend/routes/data_recipe/jobs.py b/studio/backend/routes/data_recipe/jobs.py index 00546b47a4..606ef1832c 100644 --- a/studio/backend/routes/data_recipe/jobs.py +++ b/studio/backend/routes/data_recipe/jobs.py @@ -57,6 +57,20 @@ def _resolve_local_v1_endpoint(request: Request) -> str: return f"http://127.0.0.1:{int(port)}/v1" +def _request_has_desktop_access_token(request: Request) -> bool: + auth_header = request.headers.get("authorization") + if not auth_header: + return False + + parts = auth_header.split(None, 1) + if len(parts) != 2 or parts[0].lower() != "bearer": + return False + + from auth.authentication import is_desktop_access_token + + return is_desktop_access_token(parts[1]) + + def _used_llm_model_aliases(recipe: dict[str, Any]) -> set[str]: """Return the set of model_aliases that are actually referenced by an LLM column. Used to narrow the "Chat model loaded" gate so that orphan @@ -154,6 +168,7 @@ def _inject_local_providers(recipe: dict[str, Any], request: Request) -> None: token = create_access_token( subject = "unsloth", expires_delta = timedelta(hours = 24), + desktop = _request_has_desktop_access_token(request), ) # Defensively strip any stale "external"-only fields the frontend may diff --git a/studio/backend/run.py b/studio/backend/run.py index 9675b9ea4c..7590ef1067 100644 --- a/studio/backend/run.py +++ b/studio/backend/run.py @@ -248,6 +248,7 @@ def run_server( port: int = 8888, frontend_path: Path = Path(__file__).resolve().parent.parent / "frontend" / "dist", silent: bool = False, + api_only: bool = False, llama_parallel_slots: int = 1, ): """ @@ -258,6 +259,7 @@ def run_server( port: Port to bind to (auto-increments if in use) frontend_path: Path to frontend build directory (optional) silent: Suppress startup messages + api_only: Run API server only, no frontend serving (for Tauri desktop app) llama_parallel_slots: Number of parallel slots for llama-server Note: @@ -275,6 +277,10 @@ def run_server( except Exception: pass + # Set env var BEFORE importing main so CORS middleware picks it up + if api_only: + os.environ["UNSLOTH_API_ONLY"] = "1" + import nest_asyncio nest_asyncio.apply() @@ -310,8 +316,12 @@ def run_server( print("=" * 50) print("") - # Setup frontend if path provided - if frontend_path: + # Output port for Tauri to parse when in api-only mode + if api_only: + print(f"TAURI_PORT={port}", flush = True) + + # Setup frontend if path provided (skip in api-only mode) + if frontend_path and not api_only: if setup_frontend(app, frontend_path): if not silent: print(f"[OK] Frontend loaded from {frontend_path}") @@ -391,10 +401,17 @@ if __name__ == "__main__": help = "Path to frontend build", ) parser.add_argument("--silent", action = "store_true", help = "Suppress output") + parser.add_argument( + "--api-only", + action = "store_true", + help = "API server only, no frontend (for Tauri)", + ) args = parser.parse_args() - kwargs = dict(host = args.host, port = args.port, silent = args.silent) + kwargs = dict( + host = args.host, port = args.port, silent = args.silent, api_only = args.api_only + ) if args.frontend is not None: kwargs["frontend_path"] = Path(args.frontend) diff --git a/studio/backend/tests/test_desktop_auth.py b/studio/backend/tests/test_desktop_auth.py new file mode 100644 index 0000000000..c8cf1c7081 --- /dev/null +++ b/studio/backend/tests/test_desktop_auth.py @@ -0,0 +1,597 @@ +import importlib.util +import asyncio +import hashlib +import json +import os +import platform +import secrets +import sqlite3 +import subprocess +import sys +from pathlib import Path +from types import SimpleNamespace + +import jwt +import pytest +from fastapi import APIRouter, FastAPI +from fastapi.security import HTTPAuthorizationCredentials +from fastapi.testclient import TestClient + +from auth import storage + + +@pytest.fixture(autouse = True) +def isolated_auth_db(tmp_path, monkeypatch): + monkeypatch.setattr(storage, "DB_PATH", tmp_path / "auth.db") + monkeypatch.setattr(storage, "_BOOTSTRAP_PW_PATH", tmp_path / ".bootstrap_password") + monkeypatch.setattr(storage, "_bootstrap_password", None) + monkeypatch.setattr(storage, "_api_key_pbkdf2_salt_cache", None) + yield + + +def seed_user(*, must_change_password = False): + storage.create_initial_user( + username = storage.DEFAULT_ADMIN_USERNAME, + password = "human-password-123", + jwt_secret = secrets.token_urlsafe(64), + must_change_password = must_change_password, + ) + + +def auth_client(): + route_path = Path(__file__).resolve().parents[1] / "routes" / "auth.py" + spec = importlib.util.spec_from_file_location("_desktop_auth_route", route_path) + auth_route = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(auth_route) + + app = FastAPI() + app.include_router(auth_route.router, prefix = "/api/auth") + return TestClient(app) + + +def data_recipe_jobs_module(): + route_path = ( + Path(__file__).resolve().parents[1] / "routes" / "data_recipe" / "jobs.py" + ) + spec = importlib.util.spec_from_file_location( + "_desktop_data_recipe_jobs", route_path + ) + jobs_route = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(jobs_route) + return jobs_route + + +def local_recipe(): + return { + "model_providers": [{"name": "local", "is_local": True}], + "model_configs": [{"alias": "local-model", "provider": "local"}], + "columns": [{"column_type": "llm-text", "model_alias": "local-model"}], + } + + +def local_recipe_request(token): + return SimpleNamespace( + headers = {"authorization": f"Bearer {token}"}, + app = SimpleNamespace(state = SimpleNamespace(server_port = 8888)), + scope = {}, + base_url = "http://testserver/", + ) + + +@pytest.fixture +def loaded_local_model(monkeypatch): + inference_module = SimpleNamespace( + get_llama_cpp_backend = lambda: SimpleNamespace(is_loaded = True), + ) + monkeypatch.setitem(sys.modules, "routes.inference", inference_module) + + +def test_desktop_secret_round_trip_uses_real_admin_subject(): + seed_user() + raw = storage.create_desktop_secret() + + assert raw.startswith("desktop-") + assert storage.validate_desktop_secret(raw) == storage.DEFAULT_ADMIN_USERNAME + assert storage.validate_desktop_secret(raw + "x") is None + + +def test_create_desktop_secret_rotates_old_secret(): + seed_user() + old = storage.create_desktop_secret() + new = storage.create_desktop_secret() + + assert old != new + assert storage.validate_desktop_secret(old) is None + assert storage.validate_desktop_secret(new) == storage.DEFAULT_ADMIN_USERNAME + + +def test_clear_desktop_secret_invalidates_secret(): + seed_user() + raw = storage.create_desktop_secret() + + storage.clear_desktop_secret() + + assert storage.validate_desktop_secret(raw) is None + + +def test_ensure_default_admin_does_not_recreate_bootstrap_for_existing_admin(): + seed_user() + + created = storage.ensure_default_admin() + + assert created is False + assert not storage._BOOTSTRAP_PW_PATH.exists() + + +def test_ensure_default_admin_loads_existing_bootstrap_after_restart(monkeypatch): + created = storage.ensure_default_admin() + bootstrap_pw = storage._BOOTSTRAP_PW_PATH.read_text().strip() + + monkeypatch.setattr(storage, "_bootstrap_password", None) + created_again = storage.ensure_default_admin() + + assert created is True + assert storage._BOOTSTRAP_PW_PATH.exists() + assert created_again is False + assert storage.get_bootstrap_password() == bootstrap_pw + + +def test_ensure_default_admin_does_not_generate_for_empty_existing_bootstrap(): + seed_user() + storage._BOOTSTRAP_PW_PATH.write_text(" \n") + + created = storage.ensure_default_admin() + + assert created is False + assert storage._BOOTSTRAP_PW_PATH.read_text() == " \n" + assert storage.get_bootstrap_password() is None + + +def test_web_login_token_has_no_desktop_marker_and_keeps_password_gate(): + seed_user(must_change_password = True) + client = auth_client() + + response = client.post( + "/api/auth/login", + json = { + "username": storage.DEFAULT_ADMIN_USERNAME, + "password": "human-password-123", + }, + ) + + assert response.status_code == 200 + body = response.json() + assert body["must_change_password"] is True + payload = jwt.decode( + body["access_token"], + storage.get_jwt_secret(storage.DEFAULT_ADMIN_USERNAME), + algorithms = ["HS256"], + ) + assert payload["sub"] == storage.DEFAULT_ADMIN_USERNAME + assert "desktop" not in payload + + gated = client.post( + "/api/auth/api-keys", + headers = {"Authorization": f"Bearer {body['access_token']}"}, + json = {"name": "web"}, + ) + assert gated.status_code == 403 + + +def test_desktop_login_mints_admin_token_without_clearing_web_password_change(): + seed_user(must_change_password = True) + raw = storage.create_desktop_secret() + client = auth_client() + + response = client.post("/api/auth/desktop-login", json = {"secret": raw}) + + assert response.status_code == 200 + body = response.json() + assert body["access_token"] + assert body["refresh_token"] + assert body["token_type"] == "bearer" + assert body["must_change_password"] is False + assert storage.requires_password_change(storage.DEFAULT_ADMIN_USERNAME) is True + + payload = jwt.decode( + body["access_token"], + storage.get_jwt_secret(storage.DEFAULT_ADMIN_USERNAME), + algorithms = ["HS256"], + ) + assert payload["sub"] == storage.DEFAULT_ADMIN_USERNAME + assert payload["desktop"] is True + + +def test_desktop_refresh_preserves_desktop_marker(): + seed_user(must_change_password = True) + raw = storage.create_desktop_secret() + client = auth_client() + login_body = client.post("/api/auth/desktop-login", json = {"secret": raw}).json() + + response = client.post( + "/api/auth/refresh", + json = {"refresh_token": login_body["refresh_token"]}, + ) + + assert response.status_code == 200 + body = response.json() + assert body["must_change_password"] is False + payload = jwt.decode( + body["access_token"], + storage.get_jwt_secret(storage.DEFAULT_ADMIN_USERNAME), + algorithms = ["HS256"], + ) + assert payload["sub"] == storage.DEFAULT_ADMIN_USERNAME + assert payload["desktop"] is True + + +def test_desktop_session_uses_real_admin_identity_for_api_keys(): + seed_user(must_change_password = True) + raw = storage.create_desktop_secret() + client = auth_client() + token = client.post("/api/auth/desktop-login", json = {"secret": raw}).json()[ + "access_token" + ] + + response = client.post( + "/api/auth/api-keys", + headers = {"Authorization": f"Bearer {token}"}, + json = {"name": "desktop"}, + ) + + assert response.status_code == 200 + rows = storage.list_api_keys(storage.DEFAULT_ADMIN_USERNAME) + assert [row["name"] for row in rows] == ["desktop"] + + +def test_local_recipe_token_preserves_desktop_marker(loaded_local_model): + from auth.authentication import create_access_token, get_current_subject + + seed_user(must_change_password = True) + jobs_route = data_recipe_jobs_module() + incoming_token = create_access_token( + subject = storage.DEFAULT_ADMIN_USERNAME, + desktop = True, + ) + recipe = local_recipe() + + jobs_route._inject_local_providers(recipe, local_recipe_request(incoming_token)) + + local_token = recipe["model_providers"][0]["api_key"] + payload = jwt.decode( + local_token, + storage.get_jwt_secret(storage.DEFAULT_ADMIN_USERNAME), + algorithms = ["HS256"], + ) + assert payload["sub"] == storage.DEFAULT_ADMIN_USERNAME + assert payload["desktop"] is True + credentials = HTTPAuthorizationCredentials( + scheme = "Bearer", + credentials = local_token, + ) + assert ( + asyncio.run(get_current_subject(credentials)) == storage.DEFAULT_ADMIN_USERNAME + ) + + +def test_local_recipe_token_keeps_web_marker_absent(loaded_local_model): + from auth.authentication import create_access_token + + seed_user(must_change_password = False) + jobs_route = data_recipe_jobs_module() + incoming_token = create_access_token(subject = storage.DEFAULT_ADMIN_USERNAME) + recipe = local_recipe() + + jobs_route._inject_local_providers(recipe, local_recipe_request(incoming_token)) + + local_token = recipe["model_providers"][0]["api_key"] + payload = jwt.decode( + local_token, + storage.get_jwt_secret(storage.DEFAULT_ADMIN_USERNAME), + algorithms = ["HS256"], + ) + assert payload["sub"] == storage.DEFAULT_ADMIN_USERNAME + assert "desktop" not in payload + + +def test_desktop_login_rejects_invalid_secret(): + seed_user(must_change_password = False) + client = auth_client() + + response = client.post( + "/api/auth/desktop-login", + json = {"secret": "desktop-invalid"}, + ) + + assert response.status_code == 401 + + +def test_write_desktop_secret_file_is_0600_on_unix(tmp_path): + from unsloth_cli.commands import studio as studio_cli + + path = tmp_path / ".desktop_secret" + if platform.system() != "Windows": + path.write_text("old-secret") + os.chmod(path, 0o644) + + studio_cli._write_auth_secret(path, "desktop-secret") + + assert path.read_text() == "desktop-secret" + if platform.system() != "Windows": + assert oct(path.stat().st_mode & 0o777) == "0o600" + + +def test_reset_password_removes_desktop_secret_files(tmp_path, monkeypatch): + from typer.testing import CliRunner + from unsloth_cli.commands import studio as studio_cli + + auth_dir = tmp_path / "auth" + auth_dir.mkdir() + (auth_dir / "auth.db").write_text("db") + (auth_dir / ".bootstrap_password").write_text("boot") + (auth_dir / ".desktop_secret").write_text("new") + monkeypatch.setattr(studio_cli, "STUDIO_HOME", tmp_path) + + result = CliRunner().invoke(studio_cli.studio_app, ["reset-password"]) + + assert result.exit_code == 0 + assert not (auth_dir / "auth.db").exists() + assert not (auth_dir / ".bootstrap_password").exists() + assert not (auth_dir / ".desktop_secret").exists() + + +def test_reset_password_removes_desktop_secret_files_without_db(tmp_path, monkeypatch): + from typer.testing import CliRunner + from unsloth_cli.commands import studio as studio_cli + + auth_dir = tmp_path / "auth" + auth_dir.mkdir() + (auth_dir / ".desktop_secret").write_text("new") + monkeypatch.setattr(studio_cli, "STUDIO_HOME", tmp_path) + + result = CliRunner().invoke(studio_cli.studio_app, ["reset-password"]) + + assert result.exit_code == 0 + assert not (auth_dir / ".desktop_secret").exists() + + +def test_desktop_capabilities_json_reports_rollout_safe_flags(): + from typer.testing import CliRunner + import unsloth_cli.commands.studio as studio_cli + + result = CliRunner().invoke( + studio_cli.studio_app, + ["desktop-capabilities", "--json"], + ) + + assert result.exit_code == 0 + body = json.loads(result.output) + assert body["desktop_protocol_version"] == 1 + assert body["supports_provision_desktop_auth"] is True + assert body["supports_api_only"] is True + assert isinstance(body["version"], str) + + +def test_health_response_reports_desktop_capability_fields(monkeypatch): + router_stub = SimpleNamespace( + auth_router = APIRouter(), + data_recipe_router = APIRouter(), + datasets_router = APIRouter(), + export_router = APIRouter(), + inference_router = APIRouter(), + models_router = APIRouter(), + training_history_router = APIRouter(), + training_router = APIRouter(), + ) + monkeypatch.setitem(sys.modules, "routes", router_stub) + + import studio.backend.main as backend_main + + monkeypatch.setattr(backend_main._hw_module, "CHAT_ONLY", False) + + body = asyncio.run(backend_main.health_check()) + + assert body["desktop_protocol_version"] == 1 + assert body["supports_desktop_auth"] is True + + +def test_provision_desktop_auth_writes_secret_and_creates_db_without_backend_deps( + tmp_path, + monkeypatch, +): + auth_dir = tmp_path / "auth" + auth_dir.mkdir() + + code = """ +import builtins +import sys +from pathlib import Path +from typer.testing import CliRunner + +studio_home = Path(sys.argv[1]) +real_import = builtins.__import__ + +def guarded_import(name, *args, **kwargs): + blocked = ("auth", "fastapi", "structlog", "utils") + if name in blocked or name.startswith(("auth.", "utils.")): + raise ModuleNotFoundError(name) + return real_import(name, *args, **kwargs) + +builtins.__import__ = guarded_import +from unsloth_cli.commands import studio as studio_cli + +studio_cli.STUDIO_HOME = studio_home +result = CliRunner().invoke(studio_cli.studio_app, ["provision-desktop-auth"]) +if result.exit_code != 0: + print(result.output) + if result.exception is not None: + raise result.exception + raise SystemExit(result.exit_code) +""" + result = subprocess.run( + [sys.executable, "-c", code, str(tmp_path)], + cwd = Path(__file__).resolve().parents[3], + env = {**os.environ, "PYTHONPATH": "."}, + text = True, + capture_output = True, + ) + assert result.returncode == 0, result.stderr + result.stdout + secret = (auth_dir / ".desktop_secret").read_text() + assert secret.startswith("desktop-") + + conn = sqlite3.connect(auth_dir / "auth.db") + conn.row_factory = sqlite3.Row + try: + user = conn.execute( + """ + SELECT username, password_salt, password_hash, must_change_password + FROM auth_user + """ + ).fetchone() + app_secrets = { + row["key"]: row["value"] + for row in conn.execute("SELECT key, value FROM app_secrets") + } + refresh_columns = { + row["name"] for row in conn.execute("PRAGMA table_info(refresh_tokens)") + } + finally: + conn.close() + + bootstrap_password = (auth_dir / ".bootstrap_password").read_text().strip() + bootstrap_hash = hashlib.pbkdf2_hmac( + "sha256", + bootstrap_password.encode("utf-8"), + user["password_salt"].encode("utf-8"), + 100_000, + ).hex() + + assert bootstrap_password + assert user["username"] == "unsloth" + assert user["must_change_password"] == 1 + assert bootstrap_hash == user["password_hash"] + assert len(app_secrets["api_key_pbkdf2_salt"]) == 64 + assert len(app_secrets["desktop_secret_hash"]) == 64 + assert app_secrets["desktop_secret_created_at"] + assert "is_desktop" in refresh_columns + + monkeypatch.setattr(storage, "DB_PATH", auth_dir / "auth.db") + monkeypatch.setattr(storage, "_api_key_pbkdf2_salt_cache", None) + assert storage.validate_desktop_secret(secret) == storage.DEFAULT_ADMIN_USERNAME + assert storage.requires_password_change(storage.DEFAULT_ADMIN_USERNAME) is True + + +def test_provision_desktop_auth_keeps_existing_admin_password(tmp_path, monkeypatch): + from typer.testing import CliRunner + from unsloth_cli.commands import studio as studio_cli + + auth_dir = tmp_path / "auth" + auth_dir.mkdir() + monkeypatch.setattr(studio_cli, "STUDIO_HOME", tmp_path) + + conn = sqlite3.connect(auth_dir / "auth.db") + try: + conn.execute( + """ + CREATE TABLE auth_user ( + id INTEGER PRIMARY KEY, + username TEXT UNIQUE NOT NULL, + password_salt TEXT NOT NULL, + password_hash TEXT NOT NULL, + jwt_secret TEXT NOT NULL, + must_change_password INTEGER NOT NULL DEFAULT 0 + ) + """ + ) + conn.execute( + """ + INSERT INTO auth_user ( + username, password_salt, password_hash, jwt_secret, must_change_password + ) + VALUES (?, ?, ?, ?, ?) + """, + ("unsloth", "existing-salt", "existing-hash", "existing-jwt", 0), + ) + conn.commit() + finally: + conn.close() + + result = CliRunner().invoke(studio_cli.studio_app, ["provision-desktop-auth"]) + + assert result.exit_code == 0 + assert not (auth_dir / ".bootstrap_password").exists() + conn = sqlite3.connect(auth_dir / "auth.db") + conn.row_factory = sqlite3.Row + try: + user = conn.execute( + """ + SELECT password_salt, password_hash, jwt_secret, must_change_password + FROM auth_user WHERE username = ? + """, + ("unsloth",), + ).fetchone() + finally: + conn.close() + + assert dict(user) == { + "password_salt": "existing-salt", + "password_hash": "existing-hash", + "jwt_secret": "existing-jwt", + "must_change_password": 0, + } + + +def test_update_password_clears_desktop_secret(): + seed_user() + raw = storage.create_desktop_secret() + assert storage.validate_desktop_secret(raw) == storage.DEFAULT_ADMIN_USERNAME + + changed = storage.update_password( + storage.DEFAULT_ADMIN_USERNAME, "new-admin-password" + ) + assert changed is True + assert storage.validate_desktop_secret(raw) is None + + +def test_update_password_on_unknown_user_leaves_desktop_secret_intact(): + seed_user() + raw = storage.create_desktop_secret() + + changed = storage.update_password("not-a-user", "irrelevant") + assert changed is False + assert storage.validate_desktop_secret(raw) == storage.DEFAULT_ADMIN_USERNAME + + +def test_desktop_auth_provision_has_bounded_timeout(): + rs_path = ( + Path(__file__).resolve().parents[3] + / "studio" + / "src-tauri" + / "src" + / "desktop_auth.rs" + ) + src = rs_path.read_text() + start = src.index("async fn provision_desktop_auth(") + depth = 0 + body_start = src.index("{", start) + body_end = None + for i in range(body_start, len(src)): + c = src[i] + if c == "{": + depth += 1 + elif c == "}": + depth -= 1 + if depth == 0: + body_end = i + 1 + break + assert body_end is not None + body = src[start:body_end] + assert "tokio::time::timeout" in body + import re + + m = re.search(r"Duration::from_secs\(\s*(\d+)\s*\)", body) + assert m is not None + seconds = int(m.group(1)) + assert 5 <= seconds <= 120 diff --git a/studio/backend/utils/hardware/nvidia.py b/studio/backend/utils/hardware/nvidia.py index dc5295c302..274d9beb48 100644 --- a/studio/backend/utils/hardware/nvidia.py +++ b/studio/backend/utils/hardware/nvidia.py @@ -6,6 +6,10 @@ from typing import Any, Optional from loggers import get_logger +from utils.subprocess_compat import ( + windows_hidden_subprocess_kwargs as _windows_hidden_subprocess_kwargs, +) + logger = get_logger(__name__) @@ -61,6 +65,7 @@ def get_physical_gpu_count() -> Optional[int]: capture_output = True, text = True, timeout = 5, + **_windows_hidden_subprocess_kwargs(), ) if result.returncode == 0 and result.stdout.strip(): return len(result.stdout.strip().splitlines()) @@ -85,6 +90,7 @@ def get_primary_gpu_utilization() -> dict[str, Any]: capture_output = True, text = True, timeout = 5, + **_windows_hidden_subprocess_kwargs(), ) except (OSError, subprocess.TimeoutExpired) as e: logger.warning("nvidia-smi query failed in get_primary_gpu_utilization: %s", e) @@ -135,6 +141,7 @@ def get_visible_gpu_utilization( capture_output = True, text = True, timeout = 5, + **_windows_hidden_subprocess_kwargs(), ) except (OSError, subprocess.TimeoutExpired) as e: logger.warning("nvidia-smi query failed in get_visible_gpu_utilization: %s", e) @@ -220,6 +227,7 @@ def get_backend_visible_gpu_info( capture_output = True, text = True, timeout = 10, + **_windows_hidden_subprocess_kwargs(), ) except (OSError, subprocess.TimeoutExpired) as e: logger.warning("nvidia-smi query failed in get_backend_visible_gpu_info: %s", e) diff --git a/studio/backend/utils/models/model_config.py b/studio/backend/utils/models/model_config.py index a2d48cf009..a2b0c90e59 100644 --- a/studio/backend/utils/models/model_config.py +++ b/studio/backend/utils/models/model_config.py @@ -32,6 +32,10 @@ import threading import yaml +from utils.subprocess_compat import ( + windows_hidden_subprocess_kwargs as _windows_hidden_subprocess_kwargs, +) + logger = get_logger(__name__) # ── Model size extraction ──────────────────────────────────── @@ -579,6 +583,7 @@ def _is_vision_model_subprocess( capture_output = True, text = True, timeout = 60, + **_windows_hidden_subprocess_kwargs(), ) if result.returncode != 0: diff --git a/studio/backend/utils/subprocess_compat.py b/studio/backend/utils/subprocess_compat.py new file mode 100644 index 0000000000..bedf8cf2e6 --- /dev/null +++ b/studio/backend/utils/subprocess_compat.py @@ -0,0 +1,34 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +"""Cross-platform subprocess helpers for the Unsloth Studio backend.""" + +import subprocess +import sys + + +def windows_hidden_subprocess_kwargs() -> dict[str, object]: + """Return Windows-only subprocess kwargs that suppress console windows. + + On non-Windows platforms returns an empty dict so callers can always + unpack the result into ``subprocess.run`` / ``subprocess.Popen`` via + ``**windows_hidden_subprocess_kwargs()``. + """ + if sys.platform != "win32": + return {} + + kwargs: dict[str, object] = {} + create_no_window = getattr(subprocess, "CREATE_NO_WINDOW", 0) + if create_no_window: + kwargs["creationflags"] = create_no_window + + startupinfo_factory = getattr(subprocess, "STARTUPINFO", None) + startf_use_showwindow = getattr(subprocess, "STARTF_USESHOWWINDOW", 0) + sw_hide = getattr(subprocess, "SW_HIDE", 0) + if startupinfo_factory is not None and startf_use_showwindow: + startupinfo = startupinfo_factory() + startupinfo.dwFlags |= startf_use_showwindow + startupinfo.wShowWindow = sw_hide + kwargs["startupinfo"] = startupinfo + + return kwargs diff --git a/studio/backend/utils/transformers_version.py b/studio/backend/utils/transformers_version.py index 36c3a4c22d..f36bdcd6e8 100644 --- a/studio/backend/utils/transformers_version.py +++ b/studio/backend/utils/transformers_version.py @@ -36,6 +36,10 @@ import subprocess import sys from pathlib import Path +from utils.subprocess_compat import ( + windows_hidden_subprocess_kwargs as _windows_hidden_subprocess_kwargs, +) + logger = get_logger(__name__) @@ -499,6 +503,7 @@ def _install_to_dir(pkg: str, target_dir: str) -> bool: stdout = subprocess.PIPE, stderr = subprocess.STDOUT, text = True, + **_windows_hidden_subprocess_kwargs(), ) if result.returncode == 0: return True @@ -520,6 +525,7 @@ def _install_to_dir(pkg: str, target_dir: str) -> bool: stdout = subprocess.PIPE, stderr = subprocess.STDOUT, text = True, + **_windows_hidden_subprocess_kwargs(), ) if result.returncode != 0: logger.error("install failed:\n%s", result.stdout) diff --git a/studio/frontend/package.json b/studio/frontend/package.json index a2eebd5cb5..c5cb949ccd 100644 --- a/studio/frontend/package.json +++ b/studio/frontend/package.json @@ -41,6 +41,10 @@ "@tailwindcss/vite": "^4.2.2", "@tanstack/react-router": "^1.159.10", "@tanstack/react-table": "^8.21.3", + "@tauri-apps/api": "^2.10.1", + "@tauri-apps/plugin-opener": "^2.5.3", + "@tauri-apps/plugin-process": "^2.3.1", + "@tauri-apps/plugin-updater": "^2.10.1", "@toolwind/corner-shape": "^0.0.8-3", "@types/canvas-confetti": "^1.9.0", "@xyflow/react": "^12.10.0", diff --git a/studio/frontend/public/studio.png b/studio/frontend/public/studio.png new file mode 100644 index 0000000000000000000000000000000000000000..4e531499b7cf115cfa016e1b3976f6741fa6f0a2 GIT binary patch literal 27420 zcmeFYRaBfy*DeY~u;A{YvEc5O;7)L9+}#@w65OG2Pk`VM+}&M6<8Fb*-PygqZ~gn6 zG0x4o+!y;D0~l0wS50|pR=v-h74cO`8V!XQ1r81lO;$!i6%Gz50S*pc6X`9i#VcG> z1NP^msf?;39Go{T99%#!9NYt}Dc~<09Ec4L?$8JhP9O~qj=(9aMMVhKfnXvpEdlo$ z7B|)j`w!VkM#mKn4i)F0A9%QoEJ9c#qMNLuB;w&)cw}n+R8;sbSQEUXi;_D0WS&O` z9GvxDz=ora3+Q1LEn4 zUUGZkW+=!p3}W;VQ$Lc_q4!kc=BKM`x9yPY)=D4$b%RML|6IrXL#6HMH;hhOv;qd#eB&v!>e%`W65P)e~)zwr+~ZwG-w^sGB-XH_NO={|Ep?OpB5He`Qby z{7>7L|3}3C+xU-B{$B?95eb6B!b*tj!((SuodrGFL8l{NPBu%oNcbOT7fbvH2C!dw z*#GqX^B^(&@9rP}r!;=;7YO)AHkm#3pOlJ=0oTRS!iOrk-s+ZU4Aq4oF06#&y{aTb z?IGo-zPP78W#2(w-+nH9p!8_29QA)xG_S@7&Va`kf(N+%JX&Fu{D8bY#J#+YN*^#2 zu_Vo9k4%TKyXF_U|6{`h@Z1k(vW3_NuBuo70BoXRrC=)Xd|u>upJx2Q^et+6P_iTD ze@3mr)=E@5>T$0#=7ur>0O_*<@1IcbV&c4VDL?FLL1oN;{?~#-E_$3dW`^#0Eg)>H zgfUs-H4kncC&2|{-9W?01*5iSE%YKdZ6HVg`z`bB_!SkpbPa%DE%x1Vg5HN6!bXN zue?*(Q_9exgIN@hYg>ud6~3NH{9nl7eJ|Ub*pm0IO@S$kMGw)v+LQNQVh&XQZ+Jn; zDckZsd)HVM`=9PPft4|Ws~?e)`~NBs=&Da7NkOc=kBODQVf~q6V2b-bR^vZmApv1e zw`HF2vpkd(rW*@0Vb?}MZu|fA?b@_JiH1)g{IIY`*UAni0SoO*{Wplv%Q$!pIEiRf zxWa%oQR+f&3T6bA{da}D8=M0@dnOnD=X;KdUdeuc6!RGWRkf?>nMm0q3+53l#au?|SPuag-XAC;)o0hohW=r_{J%4Stxv>q%s^0F7|e_3yuhg9yrus| ztXWFg*2(6QC&_I@xQq>dp=n={RPA zUz>hRpley)w9?XsZ`mq>g*6;K(Kbl#OHjz3EJMAk$J|iB4_oXOTzP8@oiDr{=ESr2 z84(IbdAat&-^-omtd(4eYvD;|;z7mAa~vWrJe+s2r|A>89b1eYSDiX>Qd=(9)=Hci zGimR+%${pBPF4{*wt*eyD25ynvVNu4d=ioS5Hs71TK4gUw9vAbdv{UJY-@#Gd0LK~ z-|Boyk<0Pjc6Qlf5)xzeTg>j3a^Z#QfP^HM%;|)Nx*|#b)SwC}wG5W|2{#c7J}SZ! z{|0h3;^y~tKhc2I{4uTugZdI!%$impVjRL=!hwtQTt{IkSH5_iu zhvTOZ_UK);P);cd`u?1J34Ybr#VF~^Y29{K z|45{2Y`SLR(J|CXxnB_ms6lAt(O-zs*I{+)&6#eS$ow!T?G87erF1kDYlqpK|4U)x zP3R@1>Fm8q_jSHcQfTJgFvJwKS0kTHa4O=sn5-gkov^$0!>;ga)V1fftK?YZAGhqf ziA?#&({x}i%h7VCznPyOSDx1={ z?_*gsbvRr;k;scS#z03sgvKfN-eGHJe`^Y&cc^Qfu!n*lodD)r#4V^Rp9Wk)j04{i zJHK~i8(-a7(l*4Rm#B<@j&tQ0rM^X_{+c>_Hx%P+*&HB^qbX5|Tu(okTl$}=Yk(~TW27y(%wn(L# zSXc3sim(Y?Y+L2;LM{J-{|aB%SohoeLQhueA`Q-Yw5N~tcTZY|=hLTN z)RP>cJD2H5mw4)-dWfnugFzLENqGW;>@)4VHw5TmqTb2ng(Fm9N6?28X@3=>Ts+9{z z1KSF&TCH6E>a1)IjJI65&UTfol*r2wse}jUAn#qUi`+4zvbmC3UhCLg6*`!xu{sL$ z78VMF^_yrK5f5VusfYMI5ODq)i}W{}wQ?@49@+p0hq7HyJZYW121cm-%O?AMw;x(7 z%7bu4=4XU^>D=TuXEJ8&x=o`&h}Jw2Ng9WSUN%nr+xib9y!1QX&XJ(^|Abn#zsaM$ zuz1$jl+rdXjH|P+9Ws(NtUbRdAmyOPxVF8f&YJcqs+6>!&LMsoA|SKrg0>PAt+)wu z$YK)Rec!PKnRv}ds^3B+7tuZ=I`$tku_{UCbuD2rL7_RtVb?!ijv9=#ztZeGf)3#y zDaQKU;>Ti7*H!)mibaET8h$}DW4q6>`;Di!_Er@(} z`QwpK*r1pPySh&P1G)9E-QGGDaiOjaLltKF4Tw1=uZ3A-kSJ(Bk%>$Ss(Xpmt}2)y zq~&wgLGqWUI#xwSkC)a7Ii&jX3xQN~W=$KLlF7&hqq06Z=vhxQRaR=(h7<0&GAdlg zt3u^vK8^vB{_CuH)hn1uiNaxo&4awr$C1oTBWs(qa{Lcs;Kf2*`wE%ffu#=Ou*iI{ z$dPe2pm-!}+__2=zL(b{0g zyCs2?aG3~(>7(kJ(jb}G%aAd{G(n*A-r zOmq@^HK{u&lkDoMBky30BT!8rqa9aYJa|a6(yBcow0HhyoiqFQeK4=K6`7s{Lpj=F ztfIA?_~F;Er9Uj2yR(<5uIc@Dh3DMcL=*dv>CsA(eHYWkPCXjzh#`$_|17I)fO6PO z-|?xX_TZ<8<>-BDn`{6)s3|jlMb_^-DBjLx+%sHkZQk5apt<(>8-$%24P_&_i1y7& zzg|(^8Am0W7SQsOKG}^aVuKOYVafrNQ6+P!{B=H;;F1}Kup2W}LQeZmRE}WA2hWgQ zbV2xgkf*>B-}@TFA1OJE^Q*N^T9$M8)oJ}qCRna%{iGJ>!ij{#h)jx1;`e6TmPKt4 zOTz|ZS00w_OME^Xwan1b=W#t+_7tqP9%|@j`O@5n&Fvb$YB_4`7+@m&kx4cn;O2ve z&m%j8J#zbICjzBTz(-oD*|rJ7+JFw`J~4aATr~YLYJ+wHdUVcvT@wgjU`W%-v9&r; zx{4|{U2x_0;%i5h6`YAVw!WSh%UDr27Uwa;VA-=2G5PeI?jX#TU{(5jjoSW#;{Phh znbxB%ViNE722eV(=~gJL_~yu8KeJ*nBR{ajx6g>&q~rQbPBSLV8VCu{$qr>G*@u|O zd}&jBXVnL+YzKw=yU<;#zGH}oQ{P5e1Ci(q4qXaF>M6* z{Y+wg1r=VJu3pzyze0+zT9u7AJ;cs%-|iAHnuOZd8}V1vA*`_2m*-^59$ZnO_EtTN zS8rs^y*27DkkUVBnR5ozh@ODwASl9omTjY1hyS}}P3e+~vG}{s?qX(Yt^1R&)rn!2 zS+=oxGJl-%eL$i$c1ajR-9AI%g z2CC~-n$^6b&??lZPY~9ept@o4J9s;c%F+g9Lz389sXpbL<;9d9mt?$C0%v3kTYrp{ zbIeKGXYN(RsaqKAhD&_AtQY? zYdv_0=h-~2Xq{2L8vIB&8G)QjvkHc$^_Vjx%7pA%^?NB8A%*Em4`*p!S#s>p4*fW- z=LP3eT7)#ow+E>TGFq~F4wvBrjmPq_Tp%;mc6C&Pi=V^K%gCRx&u}J!jwkWX zYxiJRsVX*}y3ttxlAJZlCZH3ht%MhALb_UR8ct~2wW}ZjwSKim40Y9Cbf;EaILsR5 zyQz+?R#4Wgru|HVjXa>Se}LlRYU-1jPFjgSnZ`n|kgD~}21Pp9j@{sQgG^quoJmQF zU=^Hc{Lsn~sRS_RwhsH%9_R}7#?Y7ZISy@`FFSrM_+j5C-X?|$C8SqP7ipW|fw0R> ze-9LrB7+is1!*Yf!L}N{dG#OrC8pW^nrYt_#=JM^U4DWBuAHT(3~QrbjDZHOk}%9< zL|%@lg%s&UPIyENgBm*Ln(%U+0zl!|>YH|JQkI$WlKTsZaZM8fQgUu)T*9i=k22N_ zuAiu8&sJJ^v*jNxVXt#ZWc=a8hpr+; zau)oYD6}G$41QH{ETTzTo~gO?71P|7P2EtEanOIz#o(6|A^=Luka+N691^ulWL`$5 zEaMhL-i*|`hIpO`PCsP(VdV5V8QyR#h0h<+Er%C;CJEInwz(g%ITzA0w@C)!d0$js z^DJb#m<+!b^Zvn>O8^D*ac>X2^=YL__F>iW8uPaxqtsvN{9Cktsan5nZoi@%5Ly0) z`UHC(hC97V+SCPFz7Qbig>k+}TumnkDFL^5Tv5onGcX}S^TXs$$?fuG$F;=*LF$wG z@5VoXX6R-M^=!hjFxu#is_Aek2DDUrp5dQX4OO|e{L;?VgyT-*+vZ zUpo3Csf$&w8cXSFE3ZGQnNiN`NXsPqEKCT9?X8?PRR1&X&9CFWT;o9?IrSwHiB6oT zC9_wbz;_ur^?Y6eov`o|p~`%xiGb5#MNR$rA|Ge}3hW4(jwL6zwgG*-7kl9@_2zMZ zOEOB~6(UmeY|5Zg&aMh`GBIjpt!D0Ws^Gm+?X{=lf_C{=aGI%$ z=QxF2uZc0_)bp(ih&drHFq7EHvO=f#kHKR(wv^bq=9Sw_P|Ru4K1JLm_;0aMe7%KG zi~j6%J*#=GJ~8o$=!W^J^oTeM^nAi`0tI7~bPybrm#;GRXMreoct;Wb1x=~+qdVev+)+izwrx%dzUDVY=4xs=LjC| z$1EABUgUIQ6s^TcmZmc10JK^C%n|xANv^Sv)<)TYSr#|(ClUyy#KqUd(Z9xI0Mhvs z`-%sV6eUP6h6&xomL?PvK(7)bc%SkU!j1q;eZhq}tw7*gdSCI5;K}FZPSH80{16@h zW%)3@%91c`!c`6de|-JZmyXj?RO)^q-vo-?)7zqZdwLT~mQ6JLl+I+1rtV3l9dEPl zgMLNSBo`;40y0_Z?UvT-a;mpx4e<6fEOq2QZ2R~-tL6zSfA%Odw8M~&O1_g!UmRYh zjk_+KGroqvDEwgO%GNnnq_xA%X;;=w5r?b99#Gz>U|Em23ISL0pxK%!-*(zY zr@r|~OP!h8Qk8J#W_i$?sX4M;iW+oEa2R=|N8r(v8=D2)6=|+N4!sML7TY{bNSnHS z2c;@>tfB6N!tx?{-U-@Ngp*1?FqucHEda79fd&)87R zdlzPF$GKZ)72W4_55f^tU!9{)$mU1)pHxt(gGU(_mLDPS0QaS1F&~UGK+$d2zv6jv z85HdwXkWoq%*^*kr=l?G`hYF!OIx4OtN0?tmcJR2co)BJEr$N$h&N^yd5y%D%?yj~ zOzuLrvcg6r(_oxgL2J19--OaKp&RI4;c6X$N+ zt=uznWE+S{=eED|4dX>@&z%aUN$&^S!qNsLPAU1<&#_t0Efy!KE_3J=HYJ_rtR_48 z0tyFA7XrQ!S!GfBhiQXWn0io;-=6I9$dv&mw;WCiu?JlQDuQm;<4=Sz+cXl6bTc0p zdIcnMV)qRruItkdd?!iwE*YbWdk+5;@Yv1<*dWtb4dUsgEO@>;@x9*OQo!~|+s-pw zj7#?ukM6d8HIZG0`R}HD6K?WD6O}r}G0K7H>aYNW^(xo$xhC3hC`jnw-7kd7JFaDPE1!(Dha7s@n?Ga!0Ia zU5{Sb*3n~@b5lF zqkyIR((W;Sbz)V@GTT(Y2kE4Sa>@OD0#9U#Q9$Jr!w&Tlx@a9x?~*WxMr>(F6?D>9 z|4R5Y7+?R@@t)}n*72Ms9iVx-&VijctGS{Di7;*8LvlZ}dhmz2`c1j2kMvFpZvetQ zUB@b~dcjSkH0M-&3*6g!v5clzR=!`D0BBtm!ysC+PAOIjkWTSy#_Z~@Uf03}XoE!=?aLO7oZU1r#hppIuK`rYLd}nX}A&j#jE3P>) zP)Kg@2fup(dadSNW<1V3I@-;qG3MqCF*%u2FgOmwtA*51KmFT6I(l8KyVjX9!KF+n zFGmOeMTXg=w9sgFS(BK2N_PKe4W+G*?%@K@jXbaBuf?V>VTEOEB2bly&~W_tt#KBlEcRldgU-UZ%~Sk8tLSaK&-; z5MS-k;P-Hr#}9pmTys(sxZ+Nk(;^vlV600_(t=~skdw!bG@Y8 zmRB_XOV!sz$m{fz$$Fe(bM*R$0SMnl;BEz#dD|MR(vEI9_;r9nT3)jrqfaKMMkxcm z@?8<_%C7H3VZA?dGx{^{i`5dHpI{Zkt8A0B?3$RvuM$xj2td5EYh8@ffp?Ma(ke(q zTwthlyBDFdJa_nu*Gj$!*BR<6%NHO{ggtR;6@3_+kO_tKOt*I$e+&E8+vT{$M3xWZ z2MLI^-<0B*GUfDeDvq(48ozbINWr?)g&qoWr@>g0`*$yLx~A1xJK=2OO*ULnU`LiX z8c%M`VgHlXYWWh*MAjG|Hs?^i+_RLzpv9GS?7NRpjx&nsP*izsB$ClJhy8*eJ8B@w18C=ixKJ?k)8( ze~u%d-eBCS!lmN|x$Wbb<7HD)LtBnCD%C>z>^;3|$2cFO>L`uX&8;RH>_rLOVM5Uv z$9gKZN_vlc)Q1H>@n=NCu+oaqUal~k8{eNC4Ah1-srrj z?l4jt@{5$hsVEptTTmMakZsA`6Cykqe~djc6>cNd!#;S^P2jdy3U|En1yLlH>t+^E z56tUC7lARO;F>hX#j(yLvsAx`&a73frCO#2&>Q>OB|g=TTkKZs-3@}r<%O*7{-@b~ z$f9b#$Mr$PeTcAR}m@8PYC{iRh^{2e7DMVja4lR zpmvq2lr^NY1cwhEh@;y~jLHBnogsG8$s;7@)?H=Lcz6(iQ~{k0W=)FXib-p&N=5Hc z2UC{t8Hb3Qhf9ChId+&(TF^gT+5J!m&JXGM4lvDfi#jtBnlKY1dm@$;mpevI+Bu^K zZiJocnrFI?(>Jh}2o{#&>d4veLSEd^X{REz%$+2E5E+5VC##$N`%Fr5PA4beF`(M@ z%zE29(nR0Ouj1(G}Ji4Z`7cbNsh?T;T zJ#vgy!=vP0t%&xrkgfzHVC~5fA8MKR;-1DQ&E|_U&3+gEvL*bHtW5(>qChznwG?!C z-p2Ut9fjthHgt?{bq^_J+ErMeWvwPZy?8=up#1q9)ujEHfEzt&eM(UkvoSbJ-whac zXviN=-<)RLbO!`ZBGk{aYUhCvVwL)OsBfaHK(>!onYja)+aG(or%w>GhkLPf74L=` zL=6&Q8H6Ub2GK42m0Xn`!>ONoxOqS9J{SeaobzkLJ6H2F_pp-v65g^YOm=;FX#!x!P?H>V(0GMkrBp$C=Y<^?SGRvjZ*OfXW9AqA~6TzRKI` zU`SFs8qUPzzG*z*oAuTcT))jGW*jd?Kq7Kw?NTRx^D|U^iR^H>0h;E zlT^iR369-A6a7J1&}?eW!g*7lj~#&ea<_g|x1~E2@v= zVza&ZVtOEBKmXji5t*^MAKb8++{vFZr-u3?p&j5xdrRAOXp zb>WQ>1Y>1OZ6){f$^2iErheV5q|Yavu*HrgJTiT-bmdI+O3F(+lW?%yp$ck1|G4{y zD)8CV;dUN~;X3!Ea3f=67$&~D1ffdK%HG?~5=i?U0R! z|0Ct3Kr{Z8r>?00fW@?X)kV{Ze`Fl$_pVZUx~LFO;d zZ%6pgqg1hb^wrBBq%qZ@h!x*cQ5#vUA zawl)?v7gNue$?Ob{VaPgg*cf&ly(I(Q7C+c*upGc=%sJZz4V#O)J=ojg;)JAn5s+X zFQ`zYvb}w0ohzk_Y)6vX@LD_!f%c%$?+)rwOD~U4^WNdR`}@uc86zFLOSC-tu0hQ&r;0kVikU*eilbl}a14cPjlFHH6Q>X(7N|_?>x_q*aZ98N8E!K=p%`0oWKB7$K zLv`uy7}c5x0B}xJL?B?#rgxKKQVTukpMiZy>jAmvG~`G z+tP1MK1Php#kHy5j4+MeGj#t_h+TRKguF@OAdz|>TW&DmPwcsOE^2QhJm6w&NA~f` zftt}~h;n6ALCTe4P;Jt?3P8jgf$^q6Q+ST`%6L-Tf7%Myc>mchS?_EBrY@g6KO~Il zbN~`Zk=L-!H#w*r=l4#4ZG;SZMH+ALDD^Eg2iJ>MO5G1$IN!bMcS@dlpNZ0ibEzoNi&1`4V6bX^kM}EZCA_z=7xUDoRrd*M& zX(UzkF^S7D1(X_B=Q3i=!~SMuKZTwE4^qb;*Lh15ariQt!XJrF1SC(TU1qs$pq2%X zJ0%+Dljk;ld(f)HYouQa4p-YSYlrzqHFiQyzL zULM8@X8#sMew%jccIuf?<<|uf+2qW39^20jsL&{qSM4sV`X|*ek_~|$InF5titxC2 zwclT&-U}s^O@;EVwpK>DMqJUF@0Cig9-nek+zff6T1qifo!ANs(c_e5nl`sG+CX&} z5(+*-TWQf2+S{WOU`KXB<=U^w?<|6YYVh7KU``BODeT$3SOu+CQd%mg&(ixS+u;V~|y*T5| zM`MLkS05Nk{n@{|;nglgAN#*2GV!0E5K}_<-_fMf^?fW()pc|)zlK3jJ#UYpa(~G*~Q-bfr zBLquMBio)YP9=I9-MtE(3XEX*-i|Sw5eKNsX=DqDLSFx{q-%o6JrAq+W@tzwY1q>N zL}XPm**GI9mwhz>tTn7AdM0*p1P9>FfpcN8GREWMj+9fkiCHetaNh=1=yx z%K}xasopH7w z(}~`*SAq=lsv3u^6#pLbX6v^htg5hPsv}1?M6_e9*6-ksl3?KJx`O5Z@@EE@+x0VrczixE4=GjVOe= za~YHQpLPt>fwqLZg-wF12D-jDimo=&HeTVm4#G_vTV!i6W?y2nb3fB6#&;e z;(#Oo5+#bc6YbcP8>dY`u@rHA(~vU2X)l&y?^j3u;jM5`@u!k0+rwrJhL(hBUG#IQ zYxtSwPbP1aHfj7^CSi3{#D<17ENkcQtQlM0Z4BZ-gUQEJ!P;B)3mGsswK12BTGO|y zKBxj)>YIXD`Thum%Jf(u+Lc9I8%1KdDEbh~QF9_RB(IcHB+C@!kq}hu!jY&xS_@@v z)PMR-d^BpW;HI`BeDX1MU@=ms-mAbp_2T>WR30H=$?EmB8gqg#e#LHEswXY2WPjGa z=Zt_@Y{muBWPwX{fDDu9KR#(^9x)ydu*X{x%j`4Y;ez?6P4WH;N|4)!52NP!X=6Kc!UyfL~o{Cp!ARm4f=+f z0B{gfcI@aM#2N4=&B0U%)uCR8P1t;@Rci1<0=%Cpp@%~bSPyHn$^IG5Fr6dsnVGwS zbKLMTD8R7%&5i^t=KyniZ&RPZXa2c8Bl1l|-oEVJnWiDm>nziJ8AXg5 zav-Unb)A?AC$##%*t&Rc7IML2S-l`2F;VKu`C1L*xWf(KG$aosM^IYI75o%p7c~M7 zufBm@u%O-nH1EFiUc06^|7tQ1$YMAeWA@#ZYAsvkVc}lBFc<5Up>V;MT88{ttCBf} z(W}nBmSmq_N+`6pD#Ulj&K)1L!!TvLu%U27V3=KMVYBFMiot}AkJ?Ega8QoD3=-+2 zF2f_>%=)e8Sfo!jQfgh`E_z;lvs!)orNG_v8*9P$%cy*2^h6=O{y8ER@MnyZs@RX% zt33hszxIyKcZ0L&*(bf(Ft7Nzn?qD!Wpf3_tof?CHemB`4v8v?a#2(jmkl64Pp*7 z#BxKj)wbFgp;ha@`EWv`s?}kZRajxUs9g|EzJzyGOaDgL#WfGKT*EazmO1uw5UwgV zNtQAB#tVZPf2u&HSk4O3LaW^{jy1Y=X?^oNEFCC8DcV-(tfN0(kQGlaT=9by zhEP*Esy}PDsA@0USB}5)Juq<7ciS3AIh3>pWhiftgPmaGZu#;`a9oVwsFdzckL`br z-GC+S`49VaAXpN4I-D@O`T|~61^) zt@rX-7`urTVR(!42}`gNoh_t$7QReXf48yix{Xq+u(f`W@f0(&SA$;_xdOYG z5|MIm&LtWQTnl}#UN8)ph5LV3gOyfP^FQCuWN-A4C1sgY8a}`B^m=P2^goc|?6*<^ zh_nS-lH{$&>J{qIRUM#C6`;2Xuyef~lo8sEOSKbm2HT%QtAH&u?mYw8uqt|v{#*Jf z05aDW`9M>puABJyr;1SDamD=fgcI4pdx!8LF({mQj2AnJ6AA^j=ZYDUv@LlAZo%On z^aWjFUh7P}stPWqI%J)~^lbIN>;L4h(-2^IMt#rLXKzE+s|{0oT?d1arIT6}O9tjh zKI~R9LNUXJd^C^q7*IS%nY1z#34rB>v;g`3V>kS(SVua2T?mgTE#3p&KjIDEZk{15 zH-Wse33n@<@$Q4Hv&K8s+PO_Iol}ZK+w^Gw{-Y~n4Jubg;x~o*+S)e-Lls+XI&iO=9uF)Wq#{(f*Fn;$}-S27~$dnnd{A_ra$0Mb7J;$VmnH9jPxwq-v}NYaO6MPWS;xR z1(sy_PvVQg#wXZXup-S1y>iQ4NeERG%R~`gyIf>n!Zh|U2kUIM;zHJ_Csp&@`R)#1 z4-A%0B=78?K$v4}?WTNm?W3h@ZKoh23Jf=s-j{S>6+a<+B3KeNk3nx3voRJF-PfSj(l6qxhK^$_q{%8;k+1m znHq*tp~?3>4&;sX=h_Wu3lP#mYCK@Aj6}`oYfoU-xFduguoR)J=L1{P0Bk*zsF7`w zE~2B)WbS*!2Ijgp_#5h1?UPz{eA@E$fI>ZHZTS#>kei^`8vkPt?4XL&yqo}vpx!CH ze{1>d`x|DHP_I)*~m!(`OUg)LBCp9b?jRp6iGhyogDfrv^W%b3` z&7~Vw9z875xnL-b3Ja51)5#~5kxd}muOa@?HSTCw!(eL*P|#bnU6dP3oI_{o#!H8- z$7HVB4mf&j2gg{uBpDQE6f@?fcXrI}mgFB$ygddk6f&!7{nq3eZcGYcdy6XO6YtAM zfr}1Mtd&xpe+WIeFFmup@VY7wC@fbNa>t@Z{qD)6P{&ecLXUh?0hWbnb)^!=UM_PN zhXxi6l^Ze7#iiAJ(w3)ed{T(Q3F?@Nt0h;8==kukYK!9*wR}-b>^Ay0qz$ZoX8lwN z^G)}e<0HD8?1@BqFqwgIB(*a>Cc)j}=Smb?N$V!IRM)72ZZjwxzQMk)yE(If6L(Pl zB!kYRcN0B_ptk&Zr43oKj9k6p6lCVf-OHq_p>(R(3b*K8_Sz#}Cmo(I2aHxy{rdE= zlUeW#SH3(^!in!$U($0Tvi7$^7J`^}AF<))$pw`ObNl5A&Y4K+RQdzyuo@OBO^E2X zs-qsVpSZszYRFi$fs~?VMdFD+5QkVy0fdrSQthZW_kF{ii0-B)08C?rTe zMz&kGv_Ce1!vU-OS9${WQVF@IQV$~J4a3}7mj}NxH(_scupXIc9340+rv+QKWhUG# z%ka?fHmnw1nxi;C3B758%otVD=A!6Ew?7b*fX(xYXV$`FBEe3mg<5sUA-;1NFhfE-#0z>ZIaj$d zWzh*{G9R4_-$($k$1w^YJc!Weacw?hv<-ws866RS*!-v6s=SHQU1yP zUXf2-J~q(3ltE1MGkYlux32x*Lp6-CYQuMfL~pAaC~&=k0!5uZz|7tF6yJa?YrMXV z%f92k2T$%)R?Dlu7^VyHx=_ugTh$bqm~OH@voCN5;=4td&B`2y0y|g$jE{YV5;G!U ziR}2bNZNfSM)2~1_icE_4-Z~;#BQ$r#N9X+^M8zLxf&wCr||(4!5!h?sfp{rxLu!d z@6Tm!6!VDLf>mgAu39Dsw|VkYH-7tpeKo8;zq2ZTF*AUoBzE`j>7IW6**09EI1ci} z&TuIL1&0?(!Vu=uqV6=ez#-QwdM61n#i2KnDsq^Z++^1%mQ{>|-PR(mrX-o+n9=*5 z!}MW6cc`J}1Zu?0{lhfamr_(cHX?pf2^fpKvL*fxT}jhM)YLDJy?j-RJ&(8=_EuzT zG`Qw+&{^z&A)(xs>~jI&tn;4W>AZsyI1}|#mgNNXFA{-!f@ww{Lqc66xwb?F6*Fbj zbvq^&+cG=*pFKUayn@WWs(=dX!Gq3_EFEXFU|&H*+u=)tU|;u#_iJkVmw#Am;XN6g ze9-=8e!U=B5ORuQrAZ={F^`~z{EPvpeh^(UJoP9f*KZaKD(YR`w9%Cg-zk$hcGyMn zSnsf>swaURHvw?HIwsTh#j_&BQ`B-D@g&@(5*Ll}^PtlWil4E_+AfbgL5_!VHxx^6 z9nSHfl*z-UV@NAolqzyQjLXTh2#rkvBFH;0ydN!?lXOZ&h-v-z#e#jbE<(>GjvL)uwK(0=N_F=_<#v#`IX+8JQN%_pR?mgYT z5D&OH-WI0$`A?9(YE*%Q8I51w={AO$y{_~7Hyr1chwCgyC;If8!ifNGGWXl^KTaBd zNlOmImd}-_?bjOqa;@ax`bZ9^jvCPSk51^vQIBEVWrxiMSuDq7BQ)>`SnXL_aR;<;TfJZ;;GM_D{|DAdHqYDhHXCr7%$i2u3s-# zizhX+#$5~USJHH^YGN^mgJIAYqNP-9upF0R9(+&=AL5_|BKUUAd}anAb?lLgk)IV~ z4@XM9w<|NBx{PK)7pQ){4zSl*3NI?AXi}U!hV(Mj68aR{4PRp6h<34bFF$cDUWUCa z(D3Tl%KXp(&wrMX+e&>%c){-!1z8V)^NoQr7`jz#oe9<($QRQMAsauvN&(wX4sjaGRxQD|m`k6{$SidD)>aM2%e=+SNEZRnBoFbfjxYhS>x-2QwQJQLR2EQbluSa(&L-m`fPkBl&-rBxXytbHv*pYUB%#gD3 z;EU77sL1k`w!Z#okgwTw!*Fhugn+z)XQyM9{=^N>*l2lenD@?BE1tqb0wpwe-|$M< zmrD22zb*IZQnNZ^YFa?d^Cik^c^9@I9V-pmY9_-V;y5L62K9fhLIUjVdtbj>2vRMa zpmuV8vZ@io^q!e9&&r(NF5^hE|4yq5ZU%IXl)7MGya)Ex2~DCwf*2c%2+4n~t`d>k zS@cM?4H~PcDk(h(-pn`9!8XG3pTBqjIvdjWw!VmvpMuKoR(wSXoO6;ZV!|O8)ev#i z(YcKctC+z@mOj-*fdCn=`v`nG>GlZ>c-Rp)qaQw$2a?(k*hM1)D3A59Fr3W8- z69*b_+GJBJtrv|1HN}hc`Z~p!F6j`CMF{wf9;F{sG_U`rCxMH@ul9WUNQiqDke59~ z5J{YxUs=cP!LO@q5PIo3DF8hha#=$?O=ZqXrIzmV)3w(ZIDboj}gF)3Vt!Tr=xO`JV2CzYPV9zsD6;d2&TlgtHKlliJ!Yn*6li zgLy6D{3W?s6IfH_F}<4~B0jztrgEEF(&N`Q>XRi2yAqs-J;<4+%*Sz<8LI_0z`w?$ zVD{H=!E(iy1>I=-WkiLWMdWtZSMvHep|{dNtm&XCVf*{d3DZh~_vxYrS79{G!G5L+XcI z=H(QNKbyBOn8VqtM(#r|f9}f_+s>I*RL%?I^3n3$!TXx4V%%cs>bOC~ll#A`=|^VY z**f)Iyih%RGL0U}yW+$2Nk@Kp`uukdTa3M?4x+CjqMjm(?_5RV3U~~!9j8*>J;QQ4;qBO zvBp8-r{+WsC%U4xS-hP>@?8N_w&)Lgb&Y&v!1JK|9DK?$3ACz}>U`mA!N>;R1?<2E z4w;B}#cvG}OY2n(v|*F@s{Z^!(+L-%^JUAj z1)E^Q`%yjXP!D$Qj*{t_R>qAdrQq1lvQ|D(Gd`cE?>F;J#R%@Q7S_uYm?~#PZpf%i z{)E72DU6BLzOu^zOW4vyHH9C~v;jP0a@w;t_qH5YX|#grQ6kKxM(wqG54Ad+e50JR zLj8JSA58+A9Ozi9|HV8SwKS1(#}Mq3YYxuQc2D;k?w4(sX9C>9jjV{+3pxF`Ntd-R zUzJyz`YqS?SZ0MLjeS)g8@oX|M3Z^_hvIO!8F9Svv>k|fGMu|)p#h9&P<3pg{39?R zBf0F+2LGscY#M*g?IZH%HSCVjbJ;lEgagk)mawK&HTSuT`ckbi{fM3)!}_m~>*vcd z%Q0)$onqkz=;qgd(dSY4#rvzD>_;lbHgNO4(b%`}#)of+g<#GBE(O%;I2jYLn_Slj zirx51ByLLn4TXdQo16A_LR)_nQJZbQS(ZaRBXG7MEI)rIh{|5(|Q9%PeK`WO_|EMU-t!$a^= zx~fwqe@cmf(%u3Av#iM|HS55f=btbA1MAs*Z)-FzKooy3+Jhc6I+y}mjbsIk^v>Nb zxo-KJ-Bebu(-+nBhE?-*V=xx%J|30W$ZXLF+H%N#%DISBGG(X>4n=cBui6C0&HCEx zyQjA?{i-Jor0tE&Iny|CEud{;^?pI0Y|kRmpeDzCa252ZGj8xuv;vTPv<0@hf*w|y zh=&z0PRS%S3T>hwF%)GSu)i{@CZ2u=gF#5g2y4^zPc4n)M-?h}%LU`~!)3I=*rucj zDw8Si#&v76lh2_TbVfq^oI4@U5?x%c#PU~2BGM?{@I%4by zX#>xXx+IdmyV;nlg64%yjtOBtgpXA0eKU#^j#LFV1cf*{%lBk046D4D$Hz)>#65Tw zQJ}HD4nA6Nn%d)?&~Kk>%n>T%ufkYoi^@MU8))$Pu>J`M#<`m4Kl-7qHzq9R@S9RAE14sdkeN$NdbHU~{W0B{q6h z{+I{0zj`z<C>2!iHek(DRasLAB!`H4EYSO+7u^;e?RkX;7%4s#-Re zX%8X#VV-vs+Z_qQ-`Y8^CW;T~9d+DT{%i&g_M~mSclD~vKSMGgC}#}qC{gigQcjVj z#0lc;(T~T_(|7 zF(W4s_}!TF4`66{mkX{Gs>Zn~XaG%(X!`-{&WxNd=RN}_!jw1jTjxIt$16XNKEa8Q znm&S-^DoPY8PsvN`W^}@h2fjQL@E%$GgtOY4Hl*>~>xTqe`x@9*U(Ycw@?YQvk8HAwCESvs9f?Wd07dJP6VgpmG8PqwBO za!CFp(9A@pezC|_0)8e;O8G;AO#+)8r%Em3(s@4A#FLF|w#0 zV3u&1hJ|S+cbw9b_ePcVF+p{Z*S5TuJcrgSCI?$I6r|ePOEt0a3}c04HRMQ9jji57 zps8C@hVI#OqXe>Sew+pw{<~ma>_bn|dF8SQyWr)SiF`QSp!JY$lq73l-1Ay1Da4%l zh`;nlsVc*BoQY{N<4S!U8|Ir#e?X5A21gx`X!_1IZ|P4|lSH+eDdMp0dB2$SgH`UQ z>fZ4hr5x7dI^?5&VmT@34&4(L_*bSs$PHfRmaleyFnok*n%k!>KfszF;p06bdd9R0 z7TVdg8~I57=n9t81?xRP7Bu}fyfc|<7M`}M3~K*GDz59ByBy<~$==R14x-cDDP3iTU4eb) z78;h4DY+s)GwA!J*JJcl-Tr9c)o1V$M)0}>0^H@SXEq`C2wJ%L}sG)wAuk;kXKtXxNp>$)+;h}-2J#hhpZ zUQ-(cNLyzF5~inCT(io?$sp1rQBVHT1)f)D+Ic{OEBNTOQZhz4GwNH5U;j1{$3_BA zS)j$w9;^XTES!?`$jW_-R2z*z984wQX<~r z7#tIHZkYGCMRpF6kgsW-1(E#yOW`7+86$PD5IUS#aRK%*+79)TpV>p0e!d z0GK_A?CG0K2u*TtcSp%{0 zA0r*u@P*>HEfhB}|o*dQ|2GMW0}>$((!_YO?7(DR#jXl-Eg? zPk3M)_UH$=ywDNMl4SFp67shEG;A-)JA0&Y`69w2Ipvt&WOi)ovMotl|4BPn0tL08 zWlkrH;E`g8Z1|Y;!|hkfDIRPksmB#{jqkP1T91{aiIp9(EIXfjG0=^O+bx$29E;@j z9A1u%s^0O&Bwp!%MrJve!k}B%$%kKH9}aUpqTr#woKQj79}tzQz%|LAMXetbpHIyq z@yU|-rD;#{UzR(RUytth*Err;_*7@z;UQx-UsKXGy0(vnN2gc{OVQ6 z;B98`lCEZX#^=>`)pEHj3Y#jz+VcqfWLi}1!WWpqcqFl|1mQk{sPex2)RFwj zAH&mMsz4adlJFeok-Y+*9)@$lN^HrKqUC<#&iG4@O<^PlH5<>LQySnMZ6QHsuUz!35pQeV0Y+|| zcl~J?tsHK1>aOFRrSoSy8~@-~W~FzRR3<{J6#yQ9P8x$Ta<4%gbnO^OhP%tRK?4Wz z7%Tg+cY3>;)k$5R1uVHn#8}-RqKb>CV}9g0m)slC#^wc-J>ttyLys<5Lq1C*-F0kZ3k7boPa+AK zTrw>Fp{S85F5&E?UfM9saDPEGn(-{G$}^u(TX;felBV4aZkO8gV?*jo>pH_x855q4AHap((XjPcDdD1 zO(#1!22sOAuly^Cp-+@~3v9bgDk)qI%|FteKAQKfYh4z^BBO;_F)9mvWHZZfFP{xG$xF(D%Lgzn5tPv(tVqZ%ed2O7K50uE!Mgy3Y=@bm1d;*u!D+Og}P9? z7ow%?yVM>U!61_E=U^dT|7*c+sw%fXio~md2yy&*9%Wk!ZaN}tnumNpPI>#-c`k;B z<|mz{$bkoQjI{0sl@cK!2l}J^_F1Bxc}!5A}I~07|)XZdEthn#IQeH|u)mSZ?}9J|So} zxJ*;^fe!yng3^goo**8xyw9<}=?0y*CR3JszDpQkUc;DvA79=wv|IiLuC)%6O1Sjt zCiW@bdQ(Z!Us=4}+SZ%3vPT~3okd8qS0Uiq^ck0$dcU7vM`3eZGyn7opTuiAf2CJri0D;kwkfiVCn@giXD`C0 z<`T=orF#9TtQa+yYXg@RHnXY&9m1(mYq+r6T4_chVDdHLx63CY`Dp_Tnl_>MenE+H zX2%+Z5&JG%r%`TnmGUz;0YNp{`O5nZjj7&|3;}r>)MmtN>aVwZO4MUf3)tPEi_ohE zjHHVj`ys2)Mb_Dp`%abAYaOchkC6ta#Paf25ko7FaHu&JeAnmq!R{TpC$7TUz7jOK}#WXn+7> z&A(3Cx@%OpZL#wD-6f&7aqf{%`$w=8c(>d`x$ozlc%2J|7Go!rzSEFPDXH9;b#RZ) zdu-ey-Ypm&xhk+B!bXU;397deW8U;1drLBf&C>Qsd}^OykRkBvA2tHXb#o^rjf{V# zD$wm48Y)MEW5ug~kkuA$ zt^Iooz!wyI+qR%QaWSB)2{I;-&m9JuOO?#tq#pnER_G8;WEy@g%HdPD+aML=Oy%?! z_KasORR(yb;wo%C3z%5O-!)I?O9C$yU$}7HW5XD~B9Ra`GA-1Z`~m|0vR!iSLJ}H+ z6@y01U!R)`IH;0ljK@Ptject%-ogln8EI}lZeLmBU(9FijM=~*KLB>V*?z*${bHl6 zbLZd7=SjQ@(ehk{y4<_L+WC*0tt>RNem8>2U0HKcaG`a5vqs8Q>^hPP6hLJh>nrzM z15In$w?n5!#TB_E^W`!j4CzJ5_fUQa*35M0ZBxNS{hzOEAelO0%}?NOMBt(J4<#oi+}9xH75*0f^spZ&f_DQwTSJ&}<+#xfNibp}S?UDW-2KWtK^QgN4!! zk-&y1;|BfdMIN?<#cHq29)pzWZRtt=U_RdFo{rLERoBD=(pFF6GdgzBEDEM=hV`i| zlT1NY#Z6c)Y5TJ&iyif&j>hjks$)$)zF|A190YI0;*3-urGj?|bf~aR`veNZRtk;j zA!!DdToxMm7veySK@(X+EG1fqwR7QEp7Y;`9&N8H8MH4B9MZj)vn7u$65kMhU3Hl? z#@NruEo$@fk_MMtN1ZtyTJI??!}Kjz$lzt;zvpL*_`-Fe$pUkV^8w_Hqf(g+D( zc1lGg33Lh6pBPa_1OUPmS7c(3HUj?rV#N8DH>}VTN{!rY8Z4OMt1Z~b4U2C!(zM`% z9@0QhXgZEag>wh>h|vY%0KSUI2}bihk%)e>ztq)Hs!}YwOv&2ttdexteUqUj^V*m) z>khzVDabq4Zi&)krl&%>H-;AjjKJ4i)XF5T)TYbVOxI8bh0s9Y2km_twHZ?3nwh!d1 zTV%zqTp&K`)&viYK3s4Db$Omcr(W``Z>(>4Ud3YXm^x&CX007p;m;nW1j2j32_#C= z0u97O!0XD~pXzpU@ST9Iw%C{-K-n#u{QEi(#HB6_|aJ`JJ`Fb|a+76MSPbm`_j z|3p`50esP~QlW%~N%Kldqu}?K=babE;ntZY-8N_@QOk#TJ5X0n5Wm^UL(vtUzkh4W zluCz%F#bDaHu^VrAPEhVhnoV#*Q7VpW@(8#!NDnNI2~W>zi9}hr*r~9hZ0@YhxS4g zfgqrK;K#aD^M&5%%psxo4~Prlc~9wB&P;|kIO3_vv@9f78aJR{DHkUrhZ0= zbI4Reux>P-O3Y(E@av)XSQS~l7LrOsrn|!mln1k0hk(Q_fPR7DXuWJhZ-}Gng-pAe zucUMcQ`O!LF*$q5z^eD90rTQO_|#;BVRxiCOiBSb1W7U04hZBQ4niWIn?wN)gLk$my2xv%1*I zdoy-<(}f|D3HJiYTI{(2OmgXKqL%fJuZVLa&hrUVCpUS`Ho*OTuNzegSPW?bF|<2( z9oFosqa=m-e{3k6YSiE?V^^hiXiM~f2DN5_revS@K|;dA3;Luy$=s%dflc+f5Mg2) zb05Z|7JeJ0(9o=9&zn!vw$FMUoQoRucgY6Vm7f|Isy-rx=H#H6^uJb{EJ8jEh+dO= z<$Ykk76gNX(t5NkmPl23k=|n>1;@kh)J9LuO;qD=jeh_*A=cd^f$!iSJ&&hmpAI=k z!at7oJ4}p4F;3Fq$|MW8H5t~<3X~a@u8>D6{5J3H zz}qdyi9oCP#gPL1l+;|5V05Jx60%tk6fts7%eEE%@#Q06)E!IVL9&M#|AE7vGRq#D zedz-yt98aOH>)YmZs4_4VrNwp9^DcQWw2YK33YcGo)U$dO)w3YW+6@W`aJA%$y^9m-;yMA4RVbLAMz4S@gkRy0}eC ztp{5I)8SmyhYcPk3y&@LVbGtD547~qG4?@q{@PKbV#m}-kR=FAiUT)0?wG&62l7|3 zn?AQ-&0h6MyScz?l=;xZ;hLLy7Ln2a*_5Su;tLn`C%dQG0j;yeK_>F32SF6h*kOqB zAb(*CyEO>>xb7z3=mio^fLuK(mivSy}HFu^uN-_Et-->S%UVll|#}Umg4#Tl|7)m4JG*YbmIHp;5IK;LqCZf?)EF?z>~m2s#&JR!w7c&?M~ zuzo$1z0ox)S;w(IH+fa9WwBxqdsKX>j21cGyd3992FA{f@2zLdHc0Sp3%3~nlifrJ zzJ}WNJIMJ)+{Rgw3p8#>2FSeHoKCL^y;*br063_4V)j)O`eF5uAKtv)E&(3fvyi+8QoBY$2d}@*f6yUy=G?&bJB=&? zc5xVIq~B~toRwF+TEy+vGdyNjPm59T?BB+LvuQGF^;rD$LCe?=d z%K$>rb9pGG z)p5cjR-ve@V+!(dAQy*dXNZxGLZQ{c-eCe)bqWFfS@dKb)5}U?TNO|saEt%matW=tKTRbLzdyB`o|B-)S?13^V8z*39)861TzDzB%;vDk}q7d*N zkXAvS!YO&7cz!@DvwcJKF3>FU;1lqX#Kr>ry_M7pS^H5d{j1c8C&m{h{$5ZOwWlW< zINd-4NsNlEu^nn&PSAn|A5T856`8vlXQ7)ju;M@=CY@+|&1P z%a^{+aH$}^<^&?*2Pe2g-;$4P1*3-DYp6-y;nD1)mUpSLx!%IF`D#@N30Vt|9Grn~_?;~7udNtA&J2pzZ!k6f8uOYJ<^UTI^0@|fJJ z;OV9^I-da*mb+PDr=(?*aD#$3>f+S0Q7X5~vAKIfP2zn(Tum2t^c9`}jmEq+J}(yk zWnSHf-@3fWo{jflN{LWJ8Dwc&1tCWxt1-pcSK{k1g(@wb59U7a%C2Isc?JATk>H~jO{tnH-kryBVv6z;XoiR{s^+b5&-I!ppTv zs_e>&4iVF0otyg`ztdm`id}1(W~*pR+T8B$yH2IJmSxTueOa-dw3{zMHm?LyNaSg7!4V@r+_hKzP!Rc$c^NA zYLtVY&K+#t9ZUR6=g<9?cLe8k?E;;FJiEYF;FQI!rS{_jIS|=&;2Ar(W1$J%1 zQMJ7#Z!kDy*J||BKmD05N*xJ&=zsJvJRVIgA7WX%0!x$CQlq?M`hEI@YS~)U>;wN1 zI-+ib&&DinsnHAx`*(ZD{qCr3=hK07gl~F}{`e#YL`V8Y5E~A@%~>-7;Z0u!lDHm* z?sSpwet%Vacd1K<=;Q%mElV^I=oy7Sf;IsB7{-{gDInv(-9(>gRb8*g3xQVt-n%L{@u%| z|8MPQ-w%t|Jr7=4#cOYfL|w!@iYe(q@1k6shTf2Pp3%9AgwF(AG{lY&%*2DX2maQQ z37dN=RGfeTyG82p)|r#dMEQW?;vp)|qjr;^d14v@(D1u1BJCJE!yFc6MS8gsI>6;a zf;TB3?$-0$ldy}W%`g=Oa}-*-zjQ#+JKcPKA|XebuNl94=aLw=r@+f0&T|o7PTw!O zSj-316f7AJE*#Kn{IGIvlkp6Fr*3ZX?&P)49b)fA$m|$%--HQ%wKpYguH=#B@>O~Y zE2tD$cP2WzM^hS{BuEIHKCgTfU2EfEJpKV9BVHLUbpDabe^?E=>?;m-_=PT#8L{u< z7SC7D;d$v@B$neAZN?6ky)L>b|R8#Dm`{*EjMU3Q5&kC%mP{pW9RV5c;Eksap^WW0FZ z^$KAV|Ct(fuWF};>c0IGC3Mj!yXz_6i%jS;pw31FuBo=$aYIFuh0=PaOSSsx)MHDY z5~A$_&~^gof~`2b3CJ*p8HZ*0%R>6j2RF(cabUqt!_JH3nLX7pkzp&0Wf^lZddDDyJ_5z7Va5ou95dPn-i*yLY z2LCJR1QqWKpT9CdJ2{cURibZlX8=Vl6p%Olcln>(E8&1(;J*tHrvBgM!~g&DrX~I# zX^>kuXf&_Baj~Wu9!UGd<*97wX>H+Y1F>?q0lo-Cg+(O=gvAAfMf8MaAYzgbapA|p z!VqEMa;@r{OjQ3Y!r-QSNho^<}+yAqUgw%hX { @@ -16,55 +18,64 @@ async function hasActiveSession(): Promise { return refreshSession(); } -async function checkAuthInitialized(): Promise { +interface AuthStatus { + initialized: boolean; + requires_password_change: boolean; +} + +async function fetchAuthStatus(): Promise { try { - const res = await fetch("/api/auth/status"); - if (!res.ok) return true; // fallback to login on error - const data = (await res.json()) as { initialized: boolean }; - return data.initialized; + const res = await fetch(apiUrl("/api/auth/status")); + if (!res.ok) return { initialized: true, requires_password_change: mustChangePassword() }; + return (await res.json()) as AuthStatus; } catch { - return true; // fallback to login on error + return { initialized: true, requires_password_change: mustChangePassword() }; } } -async function checkPasswordChangeRequired(): Promise { - try { - const res = await fetch("/api/auth/status"); - if (!res.ok) return mustChangePassword(); - const data = (await res.json()) as { requires_password_change: boolean }; - return data.requires_password_change || mustChangePassword(); - } catch { - return mustChangePassword(); - } +function authRedirect(to: "/login" | "/change-password"): never { + throw redirect({ to }); } export async function requireAuth(): Promise { + if (isTauri) { + await tauriAutoAuth(); + return; + } + if (await hasActiveSession()) { - if (await checkPasswordChangeRequired()) { - throw redirect({ to: "/change-password" }); + const { requires_password_change } = await fetchAuthStatus(); + if (requires_password_change || mustChangePassword()) { + authRedirect("/change-password"); } return; } - const requiresPasswordChange = await checkPasswordChangeRequired(); - if (requiresPasswordChange) throw redirect({ to: "/change-password" }); - const initialized = await checkAuthInitialized(); - throw redirect({ to: initialized ? "/login" : "/change-password" }); + const status = await fetchAuthStatus(); + if (status.requires_password_change || mustChangePassword()) { + authRedirect("/change-password"); + } + authRedirect(status.initialized ? "/login" : "/change-password"); } export async function requireGuest(): Promise { + if (isTauri) { + await tauriAutoAuth(); + throw redirect({ to: "/chat" }); + } if (!(await hasActiveSession())) return; throw redirect({ to: getPostAuthRoute() }); } export async function requirePasswordChangeFlow(): Promise { - const requiresPasswordChange = await checkPasswordChangeRequired(); - - if (requiresPasswordChange) return; + if (isTauri) { + await tauriAutoAuth(); + throw redirect({ to: "/chat" }); + } + const status = await fetchAuthStatus(); + if (status.requires_password_change || mustChangePassword()) return; if (await hasActiveSession()) { throw redirect({ to: getPostAuthRoute() }); } - - const initialized = await checkAuthInitialized(); - throw redirect({ to: initialized ? "/login" : "/change-password" }); + authRedirect(status.initialized ? "/login" : "/change-password"); } diff --git a/studio/frontend/src/app/provider.tsx b/studio/frontend/src/app/provider.tsx index 68ce3061bd..b75998a169 100644 --- a/studio/frontend/src/app/provider.tsx +++ b/studio/frontend/src/app/provider.tsx @@ -1,18 +1,181 @@ // SPDX-License-Identifier: AGPL-3.0-only // Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 +import { StartupScreen } from "@/components/tauri/startup-screen"; +import { UpdateBanner } from "@/components/tauri/update-banner"; +import { UpdateScreen } from "@/components/tauri/update-screen"; import { Toaster } from "@/components/ui/sonner"; +import { useTauriBackend } from "@/hooks/use-tauri-backend"; +import { useTauriUpdate } from "@/hooks/use-tauri-update"; +import { isTauri } from "@/lib/api-base"; import { ThemeProvider } from "next-themes"; -import type { ReactNode } from "react"; +import { useEffect, useRef, type ReactNode } from "react"; interface AppProviderProps { children: ReactNode; } +// --------------------------------------------------------------------------- +// Tauri window helpers (only imported in Tauri mode) +// --------------------------------------------------------------------------- + +async function showWindow(): Promise { + const { getCurrentWindow } = await import("@tauri-apps/api/window"); + await getCurrentWindow().show(); +} + +function easeOutQuart(t: number): number { + return 1 - (1 - t) ** 4; +} + +async function animateToGoldenRatio(abortRef: { current: boolean }): Promise { + const { getCurrentWindow, currentMonitor, LogicalSize } = await import("@tauri-apps/api/window"); + const win = getCurrentWindow(); + + // Ensure window is visible before resizing + await win.show(); + + const monitor = await currentMonitor(); + if (!monitor) return; + + // Convert physical pixels to logical using scale factor + const scale = monitor.scaleFactor; + const screenW = monitor.size.width / scale; + const screenH = monitor.size.height / scale; + + // Target: 75% of screen width, golden ratio height, capped at min 900x600 + const targetW = Math.max(900, Math.round(screenW * 0.75)); + const targetH = Math.max(600, Math.round(targetW / 1.618)); + // Don't exceed screen height + const finalH = Math.min(targetH, Math.round(screenH * 0.85)); + const finalW = targetW; + + // Check reduced motion preference + const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + + if (prefersReducedMotion) { + await win.setSize(new LogicalSize(finalW, finalH)); + } else { + // Read current size instead of hardcoding — stays correct if tauri.conf.json changes + const inner = await win.innerSize(); + const factor = await win.scaleFactor(); + const startW = Math.round(inner.width / factor); + const startH = Math.round(inner.height / factor); + const steps = 15; + const stepDuration = 23; // ~350ms total + + for (let i = 1; i <= steps; i++) { + if (abortRef.current) return; + const t = easeOutQuart(i / steps); + const w = Math.round(startW + (finalW - startW) * t); + const h = Math.round(startH + (finalH - startH) * t); + await win.setSize(new LogicalSize(w, h)); + await new Promise((r) => setTimeout(r, stepDuration)); + } + } + + if (abortRef.current) return; + + // Apply constraints and finalize + await win.setResizable(true); + await win.setSizeConstraints({ minWidth: 900, minHeight: 600 }); + await win.center(); +} + +// --------------------------------------------------------------------------- +// TauriWrapper +// --------------------------------------------------------------------------- + +function TauriUpdateLayer({ isExternalServer }: { isExternalServer: boolean }) { + const update = useTauriUpdate(isExternalServer); + const isUpdating = + update.status === "updating-backend" || + update.status === "downloading" || + update.status === "installing" || + (update.status === "error" && !update.dismissed); + + if (isUpdating) { + return ( + + ); + } + + return ( + + ); +} + +function TauriWrapper({ children }: { children: ReactNode }) { + const { + status, logs, error, isExternalServer, + currentStepIndex, progressDetail, elevationPackages, + startInstall, retry, retryInstall, approveElevation, + } = useTauriBackend(); + + const hasResized = useRef(false); + const abortRef = useRef(false); + + // Show the window once the frontend mounts (for pre-running states) + useEffect(() => { + if (isTauri) void showWindow(); + }, []); + + // Animate resize when backend becomes ready + useEffect(() => { + if (status === "running" && !hasResized.current) { + hasResized.current = true; + abortRef.current = false; + animateToGoldenRatio(abortRef).catch(async () => { + // On failure, at minimum make the window resizable so user can fix manually + try { + const { getCurrentWindow } = await import("@tauri-apps/api/window"); + await getCurrentWindow().setResizable(true); + } catch { /* swallow — window may still be functional */ } + }); + } + return () => { abortRef.current = true; }; + }, [status]); + + if (!isTauri) return <>{children}; + if (status === "running") return <>{children}; + + return ( + + ); +} + export function AppProvider({ children }: AppProviderProps) { return ( - {children} + + {children} + ); diff --git a/studio/frontend/src/app/routes/__root.tsx b/studio/frontend/src/app/routes/__root.tsx index 69aeb11748..19b5763557 100644 --- a/studio/frontend/src/app/routes/__root.tsx +++ b/studio/frontend/src/app/routes/__root.tsx @@ -3,8 +3,8 @@ import { AppSidebar } from "@/components/app-sidebar"; import { Navbar } from "@/components/navbar"; +import { fetchDeviceType, usePlatformStore } from "@/config/env"; import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; -import { usePlatformStore } from "@/config/env"; import { SettingsDialog, useSettingsDialogStore } from "@/features/settings"; import { useTrainingUnloadGuard } from "@/features/training/hooks/use-training-unload-guard"; import { useSidebarPin } from "@/hooks/use-sidebar-pin"; @@ -33,7 +33,10 @@ function isChatOnlyAllowed(pathname: string): boolean { } export const Route = createRootRoute({ - beforeLoad: ({ location }) => { + beforeLoad: async ({ location }) => { + // Ensure platform info is fetched before checking chat-only guard. + // fetchDeviceType caches after first call, so subsequent navigations are instant. + await fetchDeviceType(); const chatOnly = usePlatformStore.getState().isChatOnly(); if (chatOnly && !isChatOnlyAllowed(location.pathname)) { throw redirect({ to: "/chat" }); diff --git a/studio/frontend/src/components/assistant-ui/markdown-text.tsx b/studio/frontend/src/components/assistant-ui/markdown-text.tsx index 2a0517a44a..7eb4b21ba7 100644 --- a/studio/frontend/src/components/assistant-ui/markdown-text.tsx +++ b/studio/frontend/src/components/assistant-ui/markdown-text.tsx @@ -5,6 +5,7 @@ import { copyToClipboard } from "@/lib/copy-to-clipboard"; import { preprocessLaTeX } from "@/lib/latex"; +import { openLink } from "@/lib/open-link"; import { INTERNAL, useMessagePartText } from "@assistant-ui/react"; import { Copy02Icon, Tick02Icon } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; @@ -32,9 +33,13 @@ const STREAMDOWN_COMPONENTS = { }: React.ComponentProps<"a">) => ( { + if (href && openLink(href)) { + e.preventDefault(); + } + }} {...props} > {children} diff --git a/studio/frontend/src/components/assistant-ui/sources.tsx b/studio/frontend/src/components/assistant-ui/sources.tsx index da97ff66b5..81c8b0c213 100644 --- a/studio/frontend/src/components/assistant-ui/sources.tsx +++ b/studio/frontend/src/components/assistant-ui/sources.tsx @@ -1,5 +1,6 @@ "use client"; +import { openLink } from "@/lib/open-link"; import { memo, useState, @@ -93,8 +94,8 @@ function Source({ variant, size, asChild = false, - target = "_blank", - rel = "noopener noreferrer", + href, + onClick, ...props }: SourceProps) { return ( @@ -109,8 +110,14 @@ function Source({ > { + if (href && openLink(href)) { + e.preventDefault(); + } + onClick?.(e); + }} {...(props as ComponentProps<"a">)} /> diff --git a/studio/frontend/src/components/tauri/startup-screen.tsx b/studio/frontend/src/components/tauri/startup-screen.tsx new file mode 100644 index 0000000000..af5f241416 --- /dev/null +++ b/studio/frontend/src/components/tauri/startup-screen.tsx @@ -0,0 +1,401 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +import { ShimmerButton } from "@/components/ui/shimmer-button"; +import type { BackendStatus } from "@/hooks/use-tauri-backend"; +import { AnimatePresence, motion } from "motion/react"; + +interface StartupScreenProps { + status: BackendStatus; + logs: string[]; + error: string | null; + currentStepIndex: number; + progressDetail: string | null; + elevationPackages: string[]; + onInstall: () => void; + onRetry: () => void; + onRetryInstall: () => void; + onApproveElevation: () => void; + onStartServer: () => void; +} + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +const INSTALL_STEPS = [ + "Detecting your system", + "Checking dependencies", + "Setting up package manager", + "Creating Python environment", + "Installing ML framework", + "Installing Unsloth", + "Finalizing setup", +] as const; + +const EASE_OUT_QUART: [number, number, number, number] = [0.165, 0.84, 0.44, 1]; + +// --------------------------------------------------------------------------- +// Sub-components +// --------------------------------------------------------------------------- + +function TealSpinner({ size = 24 }: { size?: number }) { + return ( + + ); +} + +function Logo() { + return ( +
+ Unsloth + Unsloth Studio +
+ ); +} + +function ActionButton({ + onClick, + variant = "primary", + children, +}: { + onClick: () => void; + variant?: "primary" | "secondary"; + children: React.ReactNode; +}) { + const base = "rounded-lg px-5 py-2.5 text-sm font-medium cursor-pointer transition-colors"; + const styles = + variant === "primary" + ? `${base} bg-primary text-primary-foreground hover:bg-primary/80` + : `${base} bg-muted text-foreground hover:bg-muted/80`; + return ( + + ); +} + +// --------------------------------------------------------------------------- +// Per-status renderers +// --------------------------------------------------------------------------- + +function CheckingContent() { + return ( +
+
+ +
+
+ +

Checking...

+
+
+ ); +} + +function NotInstalledContent({ onInstall }: { onInstall: () => void }) { + return ( +
+
+ +

+ To install Unsloth, click Get Started. +

+
+
+ + Get Started + +
+
+ ); +} + +function InstallingContent({ + currentStepIndex, + progressDetail, +}: { + currentStepIndex: number; + progressDetail: string | null; +}) { + const stepNum = Math.max(0, currentStepIndex) + 1; + const stepLabel = INSTALL_STEPS[Math.min(currentStepIndex, INSTALL_STEPS.length - 1)]; + + return ( +
+
+ +
+
+ +

Installing...

+

+ Please wait a few mins, then you can start training. +

+ {currentStepIndex >= 0 && ( +

+ Step {stepNum} of {INSTALL_STEPS.length}: {stepLabel} +

+ )} + {progressDetail && ( +

{progressDetail}

+ )} +
+
+ ); +} + +function RepairingContent({ + logs, + progressDetail, +}: { + logs: string[]; + progressDetail: string | null; +}) { + const latest = progressDetail ?? logs.at(-1); + + return ( +
+
+ +
+
+ +

Updating existing Studio install...

+ {latest && ( +

{latest}

+ )} +
+
+ ); +} + +function InstallErrorContent({ + error, + logs, + onRetryInstall, +}: { + error: string | null; + logs: string[]; + onRetryInstall: () => void; +}) { + return ( + <> + +
+

Setup ran into a problem

+ {error && ( +

{error}

+ )} +
+ void navigator.clipboard.writeText(logs.join("\n"))} + > + Copy Logs + + Try Again +
+
+ + ); +} + +function RepairErrorContent({ + error, + logs, + onRetry, +}: { + error: string | null; + logs: string[]; + onRetry: () => void; +}) { + return ( + <> + +
+

Update failed

+ {error && ( +

{error}

+ )} +
+ void navigator.clipboard.writeText(logs.join("\n"))} + > + Copy Logs + + Retry +
+
+ + ); +} + +function NeedsElevationContent({ + elevationPackages, + onApproveElevation, + onRetryInstall, +}: { + elevationPackages: string[]; + onApproveElevation: () => void; + onRetryInstall: () => void; +}) { + return ( + <> + +
+

Permission needed

+

+ The following system packages need to be installed: +

+
+ {elevationPackages.map((pkg) => ( +
{pkg}
+ ))} +
+
+ Cancel + Allow +
+
+ + ); +} + +function StartingContent() { + return ( +
+
+ +
+
+ +

Starting server...

+
+
+ ); +} + +function StoppedContent({ onStartServer }: { onStartServer: () => void }) { + return ( + <> + +
+

Server stopped

+
+ Start Server +
+
+ + ); +} + +function ErrorContent({ + error, + logs, + onRetry, +}: { + error: string | null; + logs: string[]; + onRetry: () => void; +}) { + return ( + <> + +
+

Something went wrong

+ {error && ( +

{error}

+ )} +
+ void navigator.clipboard.writeText(logs.join("\n"))} + > + Copy Logs + + Retry +
+
+ + ); +} + +// --------------------------------------------------------------------------- +// Main component +// --------------------------------------------------------------------------- + +export function StartupScreen({ + status, + logs, + error, + currentStepIndex, + progressDetail, + elevationPackages, + onInstall, + onRetry, + onRetryInstall, + onApproveElevation, + onStartServer, +}: StartupScreenProps) { + function renderContent() { + switch (status) { + case "checking": + return ; + case "not-installed": + return ; + case "installing": + return ; + case "install-error": + return ; + case "repairing": + return ; + case "repair-error": + return ; + case "needs-elevation": + return ( + + ); + case "starting": + return ; + case "running": + return null; + case "stopped": + return ; + case "error": + return ; + } + } + + return ( +
+
+ + + {renderContent()} + + +
+
+ ); +} diff --git a/studio/frontend/src/components/tauri/update-banner.tsx b/studio/frontend/src/components/tauri/update-banner.tsx new file mode 100644 index 0000000000..98652c4db1 --- /dev/null +++ b/studio/frontend/src/components/tauri/update-banner.tsx @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +import { Button } from "@/components/ui/button"; +import type { UpdateInfo, UpdateStatus } from "@/hooks/use-tauri-update"; +import { AnimatePresence, motion } from "motion/react"; + +interface UpdateBannerProps { + status: UpdateStatus; + info: UpdateInfo | null; + dismissed: boolean; + isExternalServer?: boolean; + onInstall: () => void; + onDismiss: () => void; +} + +const EASE_OUT_QUART: [number, number, number, number] = [0.165, 0.84, 0.44, 1]; + +export function UpdateBanner({ + status, + info, + dismissed, + isExternalServer = false, + onInstall, + onDismiss, +}: UpdateBannerProps) { + const visible = status === "available"; + const show = visible && !dismissed; + + return ( + + {show && info && ( + +
+ {/* Close button */} + + + {/* Header */} +
+ 🦥 +
+

+ New version: v{info.version} +

+

+ {isExternalServer + ? "Run `unsloth studio update` from your terminal" + : "A new app update is available"} +

+
+
+ + {/* Actions */} +
+ + + +
+
+
+ )} +
+ ); +} diff --git a/studio/frontend/src/components/tauri/update-screen.tsx b/studio/frontend/src/components/tauri/update-screen.tsx new file mode 100644 index 0000000000..a8b77ab1fa --- /dev/null +++ b/studio/frontend/src/components/tauri/update-screen.tsx @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +import type { UpdateStatus } from "@/hooks/use-tauri-update"; +import { AnimatePresence, motion } from "motion/react"; +import { useEffect, useRef } from "react"; + +interface UpdateScreenProps { + status: UpdateStatus; + logs: string[]; + progress: number; + error: string | null; + onRetry: () => void; + onSkipRestart: () => void; +} + +const EASE_OUT_QUART: [number, number, number, number] = [0.165, 0.84, 0.44, 1]; + +function Spinner({ size = 24 }: { size?: number }) { + return ( + + ); +} + +function Logo() { + return ( +
+ Unsloth + Unsloth Studio +
+ ); +} + +function statusLabel(status: UpdateStatus): string { + switch (status) { + case "updating-backend": + return "Updating backend..."; + case "downloading": + return "Downloading app update..."; + case "installing": + return "Installing update..."; + case "error": + return "Update failed"; + default: + return "Updating..."; + } +} + +function statusSubtext(status: UpdateStatus, progress: number): string { + switch (status) { + case "updating-backend": + return "This may take a few minutes. Do not close the app."; + case "downloading": + return `${progress}% downloaded`; + case "installing": + return "The app will restart shortly."; + case "error": + return "Something went wrong during the update."; + default: + return ""; + } +} + +function LogViewer({ logs }: { logs: string[] }) { + const scrollRef = useRef(null); + + useEffect(() => { + if (scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }, [logs]); + + if (logs.length === 0) return null; + + return ( +
+ {logs.map((line, i) => ( +
+ {line} +
+ ))} +
+ ); +} + +export function UpdateScreen({ + status, + logs, + progress, + error, + onRetry, + onSkipRestart, +}: UpdateScreenProps) { + const isError = status === "error"; + + return ( +
+ + + +
+ {!isError && } +

+ {statusLabel(status)} +

+

+ {statusSubtext(status, progress)} +

+
+ + {/* Download progress bar */} + {status === "downloading" && ( +
+ +
+ )} + + {/* Error display */} + + {isError && error && ( + +

{error}

+
+ )} +
+ + {/* Error actions */} + {isError && ( +
+ + +
+ )} + + {/* Log viewer */} + +
+
+ ); +} diff --git a/studio/frontend/src/components/ui/shimmer-button.tsx b/studio/frontend/src/components/ui/shimmer-button.tsx new file mode 100644 index 0000000000..d675cc0979 --- /dev/null +++ b/studio/frontend/src/components/ui/shimmer-button.tsx @@ -0,0 +1,96 @@ +import React, { type ComponentPropsWithoutRef, type CSSProperties } from "react" + +import { cn } from "@/lib/utils" + +export interface ShimmerButtonProps extends ComponentPropsWithoutRef<"button"> { + shimmerColor?: string + shimmerSize?: string + borderRadius?: string + shimmerDuration?: string + background?: string + className?: string + children?: React.ReactNode +} + +export const ShimmerButton = React.forwardRef< + HTMLButtonElement, + ShimmerButtonProps +>( + ( + { + shimmerColor = "#ffffff", + shimmerSize = "0.05em", + shimmerDuration = "3s", + borderRadius = "100px", + background = "rgba(0, 0, 0, 1)", + className, + children, + ...props + }, + ref + ) => { + return ( + + ) + } +) + +ShimmerButton.displayName = "ShimmerButton" diff --git a/studio/frontend/src/config/env.ts b/studio/frontend/src/config/env.ts index 91e17f6bb9..72bb3fa815 100644 --- a/studio/frontend/src/config/env.ts +++ b/studio/frontend/src/config/env.ts @@ -1,6 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 +import { apiUrl } from "@/lib/api-base"; import { create } from "zustand"; export const env = { @@ -21,9 +22,21 @@ interface PlatformState { isChatOnly: () => boolean; } +// Client-side platform detection as fallback when backend isn't ready yet. +function detectLocalPlatform(): DeviceType { + if (typeof navigator === "undefined") return "linux"; + const platform = navigator.platform.toLowerCase(); + const ua = navigator.userAgent.toLowerCase(); + if (platform.includes("mac") || ua.includes("mac")) return "mac"; + if (platform.includes("win") || ua.includes("win")) return "windows"; + return "linux"; +} + +const localDeviceType = detectLocalPlatform(); + export const usePlatformStore = create()((_, get) => ({ - deviceType: "linux", - chatOnly: false, + deviceType: localDeviceType, + chatOnly: localDeviceType === "mac", fetched: false, isChatOnly: () => get().chatOnly, })); @@ -33,16 +46,22 @@ export async function fetchDeviceType(): Promise { if (fetched) return usePlatformStore.getState().deviceType; try { - const res = await fetch("/api/health"); + const res = await fetch(apiUrl("/api/health")); if (res.ok) { const data = (await res.json()) as { device_type?: string; chat_only?: boolean }; - const deviceType = data.device_type ?? "linux"; + const deviceType = data.device_type ?? detectLocalPlatform(); const chatOnly = data.chat_only ?? deviceType === "mac"; usePlatformStore.setState({ deviceType, chatOnly, fetched: true }); return deviceType; } - } catch (err) { - console.warn("[platform] Failed to fetch device type, will retry", err); + } catch { + // Backend not ready — use client-side detection so chat-only guard + // still works on initial load (important for macOS). Keep fetched=false + // so a later call retries against the backend. + const deviceType = detectLocalPlatform(); + const chatOnly = deviceType === "mac"; + usePlatformStore.setState({ deviceType, chatOnly, fetched: false }); + return deviceType; } return usePlatformStore.getState().deviceType; diff --git a/studio/frontend/src/features/auth/api.ts b/studio/frontend/src/features/auth/api.ts index 3bb2c9139c..97d815950d 100644 --- a/studio/frontend/src/features/auth/api.ts +++ b/studio/frontend/src/features/auth/api.ts @@ -1,6 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 +import { apiUrl, isTauri } from "@/lib/api-base"; import { clearAuthTokens, getAuthToken, @@ -34,7 +35,7 @@ async function redirectToAuth(): Promise { let target = "/login"; try { - const res = await fetch("/api/auth/status"); + const res = await fetch(apiUrl("/api/auth/status")); if (res.ok) { const data = (await res.json()) as { requires_password_change: boolean }; if (data.requires_password_change || mustChangePassword()) target = "/change-password"; @@ -46,12 +47,32 @@ async function redirectToAuth(): Promise { window.location.href = target; } +async function retryWithCurrentToken( + input: RequestInfo | URL, + init?: RequestInit, +): Promise { + const retryHeaders = new Headers(init?.headers); + const token = getAuthToken(); + if (token) retryHeaders.set("Authorization", `Bearer ${token}`); + return fetch(input, { ...init, headers: retryHeaders }); +} + +async function retryWithTauriAutoAuth( + input: RequestInfo | URL, + init?: RequestInit, +): Promise { + clearAuthTokens(); + const { tauriAutoAuth } = await import("./tauri-auto-auth"); + if (await tauriAutoAuth()) return retryWithCurrentToken(input, init); + return null; +} + export async function refreshSession(): Promise { const refreshToken = getRefreshToken(); if (!refreshToken) return false; try { - const response = await fetch("/api/auth/refresh", { + const response = await fetch(apiUrl("/api/auth/refresh"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refresh_token: refreshToken }), @@ -78,6 +99,7 @@ export async function authFetch( input: RequestInfo | URL, init?: RequestInit, ): Promise { + const resolvedInput = typeof input === 'string' ? apiUrl(input) : input; const headers = new Headers(init?.headers); const accessToken = getAuthToken(); if (accessToken) { @@ -86,14 +108,18 @@ export async function authFetch( let response: Response; try { - response = await fetch(input, { ...init, headers }); + response = await fetch(resolvedInput, { ...init, headers }); } catch (err) { if (err instanceof TypeError) { throw new Error("Studio isn't running -- please relaunch it."); } throw err; } + if (await isPasswordChangeRequiredResponse(response)) { + if (isTauri) { + return (await retryWithTauriAutoAuth(resolvedInput, init)) ?? response; + } void redirectToAuth(); return response; } @@ -101,25 +127,24 @@ export async function authFetch( const refreshed = await refreshSession(); if (!refreshed) { + if (isTauri) { + return (await retryWithTauriAutoAuth(resolvedInput, init)) ?? response; + } clearAuthTokens(); void redirectToAuth(); return response; } if (mustChangePassword()) { + if (isTauri) { + return (await retryWithTauriAutoAuth(resolvedInput, init)) ?? response; + } void redirectToAuth(); return response; } - const retryHeaders = new Headers(init?.headers); - const newToken = getAuthToken(); - if (newToken) { - retryHeaders.set("Authorization", `Bearer ${newToken}`); - } else { - clearAuthTokens(); - } - - return fetch(input, { ...init, headers: retryHeaders }); + if (!getAuthToken()) clearAuthTokens(); + return retryWithCurrentToken(resolvedInput, init); } export function logout(): void { diff --git a/studio/frontend/src/features/auth/components/auth-form.tsx b/studio/frontend/src/features/auth/components/auth-form.tsx index d9190429bd..090a3081a4 100644 --- a/studio/frontend/src/features/auth/components/auth-form.tsx +++ b/studio/frontend/src/features/auth/components/auth-form.tsx @@ -1,6 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 +import { apiUrl } from "@/lib/api-base"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -49,7 +50,7 @@ async function loginWithPassword( username: string, password: string, ): Promise { - const response = await fetch("/api/auth/login", { + const response = await fetch(apiUrl("/api/auth/login"), { method: "POST", headers: { "Content-Type": "application/json", @@ -96,7 +97,7 @@ export function AuthForm({ mode }: AuthFormProps): ReactElement | null { // (e.g. tokens from a previous install attempt). The server's // /api/auth/status is the source of truth for requires_password_change. try { - const response = await fetch("/api/auth/status"); + const response = await fetch(apiUrl("/api/auth/status")); if (!response.ok) throw new Error("Failed to load auth status."); const result = (await response.json()) as AuthStatusResponse; if (!canceled) { @@ -147,14 +148,15 @@ export function AuthForm({ mode }: AuthFormProps): ReactElement | null { }; }, [navigate]); - // Seed password from bootstrap credentials injected into HTML + // Seed password from bootstrap credentials injected into HTML by web CLI. useEffect(() => { - const bootstrap = window.__UNSLOTH_BOOTSTRAP__; - if (bootstrap) { - if (!isLoginMode && !password) { + function loadBootstrap() { + const bootstrap = window.__UNSLOTH_BOOTSTRAP__; + if (bootstrap && !isLoginMode && !password) { setPassword(bootstrap.password); } } + loadBootstrap(); }, []); const blockedByState = @@ -241,7 +243,7 @@ export function AuthForm({ mode }: AuthFormProps): ReactElement | null { accessToken = bootstrapToken.access_token; } - const response = await fetch("/api/auth/change-password", { + const response = await fetch(apiUrl("/api/auth/change-password"), { method: "POST", headers: { "Content-Type": "application/json", diff --git a/studio/frontend/src/features/auth/index.ts b/studio/frontend/src/features/auth/index.ts index af629cfb9a..9cc1599195 100644 --- a/studio/frontend/src/features/auth/index.ts +++ b/studio/frontend/src/features/auth/index.ts @@ -15,3 +15,8 @@ export { resetOnboardingDone, setMustChangePassword, } from "./session"; +export { + clearTauriAuthFailure, + getTauriAuthFailure, + tauriAutoAuth, +} from "./tauri-auto-auth"; diff --git a/studio/frontend/src/features/auth/session.ts b/studio/frontend/src/features/auth/session.ts index 6012174077..49a2722bdf 100644 --- a/studio/frontend/src/features/auth/session.ts +++ b/studio/frontend/src/features/auth/session.ts @@ -2,6 +2,7 @@ // Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 import { usePlatformStore } from "@/config/env"; +import { isTauri } from "@/lib/api-base"; export const AUTH_TOKEN_KEY = "unsloth_auth_token"; export const AUTH_REFRESH_TOKEN_KEY = "unsloth_auth_refresh_token"; @@ -78,6 +79,7 @@ export function resetOnboardingDone(): void { } export function getPostAuthRoute(): PostAuthRoute { + if (isTauri) return "/chat"; if (mustChangePassword()) return "/change-password"; if (usePlatformStore.getState().isChatOnly()) return "/chat"; return "/chat"; diff --git a/studio/frontend/src/features/auth/tauri-auto-auth.ts b/studio/frontend/src/features/auth/tauri-auto-auth.ts new file mode 100644 index 0000000000..d67730d2d6 --- /dev/null +++ b/studio/frontend/src/features/auth/tauri-auto-auth.ts @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +import { isTauri } from "@/lib/api-base"; +import { + hasAuthToken, + hasRefreshToken, + mustChangePassword, + storeAuthTokens, +} from "./session"; +import { refreshSession } from "./api"; + +type DesktopAuthResponse = { + access_token: string; + refresh_token: string; +}; + +// Concurrency guard: multiple route guards can call tauriAutoAuth simultaneously. +// Without this, the first-launch password-change could race with itself. +let pending: Promise | null = null; +let lastTauriAuthFailure: string | null = null; + +const TAURI_AUTH_FAILURE_FALLBACK = + "Desktop authentication failed. Update or repair the managed Studio install, then restart Studio."; +const BACKEND_NOT_READY_MESSAGE = "Backend is not ready"; + +function authFailureMessage(error: unknown): string { + if (typeof error === "string" && error) return error; + if (error instanceof Error && error.message) return error.message; + return TAURI_AUTH_FAILURE_FALLBACK; +} + +export function getTauriAuthFailure(): string | null { + return lastTauriAuthFailure; +} + +export function clearTauriAuthFailure(): void { + lastTauriAuthFailure = null; +} + +function setTauriAuthFailure(error: unknown): void { + lastTauriAuthFailure = authFailureMessage(error); + window.dispatchEvent( + new CustomEvent("tauri-auth-failed", { detail: lastTauriAuthFailure }), + ); +} + +function isBackendNotReady(error: unknown): boolean { + return authFailureMessage(error).includes(BACKEND_NOT_READY_MESSAGE); +} + +async function doTauriAutoAuth(): Promise { + // Desktop must handle password-change state internally in Rust. + if (hasAuthToken() && !mustChangePassword()) { + clearTauriAuthFailure(); + return true; + } + + // Try refreshing existing session + if (hasRefreshToken()) { + const refreshed = await refreshSession(); + if (refreshed && hasAuthToken() && !mustChangePassword()) { + clearTauriAuthFailure(); + return true; + } + } + + try { + const { invoke } = await import("@tauri-apps/api/core"); + const tokens = await invoke("desktop_auth"); + storeAuthTokens(tokens.access_token, tokens.refresh_token, false); + clearTauriAuthFailure(); + return true; + } catch (error) { + if (isBackendNotReady(error)) return false; + setTauriAuthFailure(error); + return false; + } +} + +/** + * Silently authenticate in Tauri desktop mode. + * + * Delegates bootstrap/password handling to Rust and only stores returned tokens. + * + * Returns true if authentication succeeded. + * Concurrent calls are coalesced into a single in-flight attempt. + */ +export function tauriAutoAuth(): Promise { + if (!isTauri) return Promise.resolve(false); + if (!pending) { + pending = doTauriAutoAuth().finally(() => { pending = null; }); + } + return pending; +} diff --git a/studio/frontend/src/features/settings/tabs/about-tab.tsx b/studio/frontend/src/features/settings/tabs/about-tab.tsx index 417922139c..bb4454aa43 100644 --- a/studio/frontend/src/features/settings/tabs/about-tab.tsx +++ b/studio/frontend/src/features/settings/tabs/about-tab.tsx @@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button"; import { ShutdownDialog } from "@/components/shutdown-dialog"; import { UpdateStudioInstructions } from "../components/update-studio-instructions"; import { usePlatformStore } from "@/config/env"; +import { apiUrl } from "@/lib/api-base"; import { removeTrainingUnloadGuard } from "@/features/training/hooks/use-training-unload-guard"; import { ArrowUpRight01Icon, @@ -28,7 +29,7 @@ export function AboutTab() { (async () => { try { - const res = await fetch("/api/health"); + const res = await fetch(apiUrl("/api/health")); if (!res.ok) return; const data = (await res.json()) as { version?: string }; if (!canceled && data.version) { diff --git a/studio/frontend/src/hooks/index.ts b/studio/frontend/src/hooks/index.ts index f889dc7445..cfbd8e5115 100644 --- a/studio/frontend/src/hooks/index.ts +++ b/studio/frontend/src/hooks/index.ts @@ -11,4 +11,5 @@ export { useHfDatasetSearch } from "./use-hf-dataset-search"; export { useHfDatasetSplits } from "./use-hf-dataset-splits"; export { useHfTokenValidation } from "./use-hf-token-validation"; export { useInfiniteScroll } from "./use-infinite-scroll"; +export { useTauriBackend } from "./use-tauri-backend"; export { useCollapseScrollLock } from "./use-collapse-scroll-lock"; diff --git a/studio/frontend/src/hooks/use-gpu-info.ts b/studio/frontend/src/hooks/use-gpu-info.ts index 3eb81c35e9..64caf06b50 100644 --- a/studio/frontend/src/hooks/use-gpu-info.ts +++ b/studio/frontend/src/hooks/use-gpu-info.ts @@ -1,6 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 +import { apiUrl } from "@/lib/api-base"; import { useEffect, useState } from "react"; export interface GpuInfo { @@ -27,7 +28,7 @@ async function fetchGpuOnce(): Promise { fetchPromise = (async () => { try { - const res = await fetch("/api/system"); + const res = await fetch(apiUrl("/api/system")); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); const gpuData = data?.gpu; diff --git a/studio/frontend/src/hooks/use-tauri-backend.ts b/studio/frontend/src/hooks/use-tauri-backend.ts new file mode 100644 index 0000000000..ed697fe31e --- /dev/null +++ b/studio/frontend/src/hooks/use-tauri-backend.ts @@ -0,0 +1,487 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +import { useEffect, useState, useCallback, useRef } from "react"; +import { isTauri, setApiBase } from "@/lib/api-base"; +import { + clearTauriAuthFailure, + getTauriAuthFailure, +} from "@/features/auth"; + +export type BackendStatus = + | "checking" + | "not-installed" + | "installing" + | "install-error" + | "needs-elevation" + | "repairing" + | "repair-error" + | "starting" + | "running" + | "stopped" + | "error"; + +type DesktopPreflightDisposition = + | "not_installed" + | "managed_ready" + | "managed_stale" + | "attached_ready"; + +interface DesktopPreflightResult { + disposition: DesktopPreflightDisposition; + reason: string | null; + port: number | null; + can_auto_repair: boolean; + managed_bin: string | null; +} + +export function useTauriBackend() { + const [status, setStatus] = useState("checking"); + const statusRef = useRef(status); + const [logs, setLogs] = useState([]); + const [error, setError] = useState(null); + // Guard against double startServer calls + const startingRef = useRef(false); + // Guard against React Strict Mode double-mount + const mountedRef = useRef(false); + // Track the discovered port from server-port event + const portRef = useRef(null); + const [currentStepIndex, setCurrentStepIndex] = useState(-1); + const [elevationPackages, setElevationPackages] = useState([]); + const [progressDetail, setProgressDetail] = useState(null); + // Track seen step names to deduplicate (Strict Mode, event replay, etc.) + const seenStepsRef = useRef(new Set()); + // True when we attached to a server we didn't spawn (can't stop it) + const [isExternalServer, setIsExternalServer] = useState(false); + const externalPollRef = useRef | null>(null); + const externalPollAbortedRef = useRef(false); + const authFailureRef = useRef(getTauriAuthFailure()); + const elevationResumeRef = useRef<"install" | "repair" | null>(null); + + function setBackendStatus(nextStatus: BackendStatus) { + if (authFailureRef.current) return; + statusRef.current = nextStatus; + setStatus(nextStatus); + } + + function setBackendError( + nextError: string, + nextStatus: BackendStatus = "error", + ) { + if (authFailureRef.current) return; + statusRef.current = nextStatus; + setStatus(nextStatus); + setError(nextError); + } + + function clearBackendError() { + if (authFailureRef.current) return; + setError(null); + } + + function setRunningStatus() { + setBackendStatus("running"); + } + + function setAuthFailure(detail: string) { + authFailureRef.current = detail; + statusRef.current = "error"; + setStatus("error"); + setError(detail); + } + + function clearAuthFailure() { + authFailureRef.current = null; + clearTauriAuthFailure(); + } + + function stopExternalServerPoll() { + externalPollAbortedRef.current = true; + if (externalPollRef.current) { + clearInterval(externalPollRef.current); + externalPollRef.current = null; + } + } + + function startExternalServerPoll(port: number) { + stopExternalServerPoll(); + externalPollAbortedRef.current = false; + let failures = 0; + externalPollRef.current = setInterval(async () => { + if (externalPollAbortedRef.current) return; + try { + const { invoke } = await import("@tauri-apps/api/core"); + const healthy = await invoke("check_health", { port }); + if (externalPollAbortedRef.current) return; + if (healthy) { + failures = 0; + } else { + failures++; + } + } catch { + if (externalPollAbortedRef.current) return; + failures++; + } + if (failures >= 3) { + stopExternalServerPoll(); + setIsExternalServer(false); + setBackendError("External server is no longer responding"); + } + }, 15_000); + } + + // Keep ref in sync for event listener closures + useEffect(() => { + statusRef.current = status; + }, [status]); + + async function checkInstallAndStart() { + try { + const { invoke } = await import("@tauri-apps/api/core"); + + const preflight = await invoke("desktop_preflight"); + switch (preflight.disposition) { + case "attached_ready": { + if (!preflight.port) { + setBackendError("Desktop preflight found a backend without a port."); + return; + } + setApiBase(preflight.port); + portRef.current = preflight.port; + setIsExternalServer(true); + setRunningStatus(); + startExternalServerPoll(preflight.port); + return; + } + case "managed_ready": + setIsExternalServer(false); + stopExternalServerPoll(); + setBackendStatus("starting"); + await startManagedServer(); + return; + case "managed_stale": + setIsExternalServer(false); + stopExternalServerPoll(); + if (preflight.can_auto_repair) { + await startRepair(); + } else { + setBackendError( + "Managed Studio install is too old. Run `unsloth studio update`.", + ); + } + return; + case "not_installed": + setBackendStatus("not-installed"); + return; + } + } catch (e) { + setBackendError(String(e)); + } + } + + async function startManagedServer() { + // Prevent double-start race condition + if (startingRef.current) return; + startingRef.current = true; + + try { + const { invoke } = await import("@tauri-apps/api/core"); + // backend/run.py keeps the existing 8888-8908 fallback via + // server-port/TAURI_PORT. + await invoke("start_managed_server", { port: 8888 }); + + // Wait for the owned backend's server-port event. Do not attach to an + // external backend if the managed start does not report a port. + for (let i = 0; i < 120; i++) { + if (portRef.current) { + const healthy = await invoke("check_health", { + port: portRef.current, + }); + if (healthy) { + setApiBase(portRef.current); + setRunningStatus(); + startingRef.current = false; + return; + } + } + await new Promise((r) => setTimeout(r, 500)); + } + const message = !portRef.current + ? "Managed server started without reporting a port. Check the logs for details." + : "Server started but is not responding. Check the logs for details."; + setBackendError(message); + } catch (e) { + const msg = String(e); + if (msg.includes("already running")) { + startingRef.current = false; + setBackendError( + "Managed server is already running but did not report a port. Restart Studio and try again.", + ); + return; + } + setBackendError(msg); + } + startingRef.current = false; + } + + async function startRepair() { + elevationResumeRef.current = null; + setCurrentStepIndex(-1); + setProgressDetail(null); + seenStepsRef.current.clear(); + startingRef.current = false; + portRef.current = null; + setIsExternalServer(false); + stopExternalServerPoll(); + setLogs([]); + clearBackendError(); + setBackendStatus("repairing"); + + const { invoke } = await import("@tauri-apps/api/core"); + try { + await invoke("start_managed_repair"); + + setBackendStatus("starting"); + elevationResumeRef.current = null; + await startManagedServer(); + } catch (e) { + const msg = String(e); + if (msg.includes("NEEDS_ELEVATION")) return; + setBackendError(msg, "repair-error"); + } + } + + async function startServer() { + setBackendStatus("starting"); + await startManagedServer(); + } + + async function stopServer() { + if (isExternalServer) { + // We attached to a server we didn't spawn — can't kill it, + // just disconnect the UI. + startingRef.current = false; + setIsExternalServer(false); + stopExternalServerPoll(); + setBackendStatus("stopped"); + return; + } + const { invoke } = await import("@tauri-apps/api/core"); + await invoke("stop_server"); + startingRef.current = false; + setBackendStatus("stopped"); + } + + async function startInstall() { + elevationResumeRef.current = null; + setCurrentStepIndex(-1); + setProgressDetail(null); + seenStepsRef.current.clear(); + setBackendStatus("installing"); + setLogs([]); + clearBackendError(); + const { invoke } = await import("@tauri-apps/api/core"); + try { + await invoke("start_install"); + // Install completed — this is the ONLY path that starts the server + // after install. The install-complete event listener does NOT call + // startServer() to avoid a double-start race condition. + setBackendStatus("starting"); + elevationResumeRef.current = null; + await startServer(); + } catch (e) { + const msg = String(e); + // NEEDS_ELEVATION is not a real error — the Rust side also emits + // install-needs-elevation which sets needs-elevation status. + // Don't race with it by setting install-error here. + if (msg.includes("NEEDS_ELEVATION")) return; + setBackendError(msg, "install-error"); + } + } + + const retry = useCallback(() => { + clearAuthFailure(); + setError(null); + setLogs([]); + startingRef.current = false; + portRef.current = null; + setCurrentStepIndex(-1); + setProgressDetail(null); + setElevationPackages([]); + elevationResumeRef.current = null; + setIsExternalServer(false); + stopExternalServerPoll(); + seenStepsRef.current.clear(); + checkInstallAndStart(); + }, []); + + const retryInstall = useCallback(() => { + const resume = elevationResumeRef.current; + elevationResumeRef.current = null; + clearBackendError(); + setLogs([]); + setElevationPackages([]); + if (resume === "repair") { + setBackendError("Repair canceled before system packages were installed.", "repair-error"); + return; + } + setBackendStatus("not-installed"); + }, []); + + const approveElevation = useCallback(async () => { + const resume = elevationResumeRef.current ?? "install"; + try { + const { invoke } = await import("@tauri-apps/api/core"); + await invoke("install_system_packages", { packages: elevationPackages }); + // Packages installed successfully, resume the flow that requested them. + setCurrentStepIndex(-1); + setProgressDetail(null); + elevationResumeRef.current = null; + if (resume === "repair") { + await startRepair(); + } else { + await startInstall(); + } + } catch (e) { + setBackendError(String(e), resume === "repair" ? "repair-error" : "install-error"); + } + }, [elevationPackages]); + + // Initial check on mount (guarded against Strict Mode double-mount) + useEffect(() => { + if (mountedRef.current) return; + mountedRef.current = true; + + if (!isTauri) { + setRunningStatus(); + return; + } + checkInstallAndStart(); + }, []); + + // Listen for Tauri events + useEffect(() => { + if (!isTauri) return; + const cleanup: (() => void)[] = []; + let disposed = false; + + import("@tauri-apps/api/event").then(({ listen }) => { + function register( + event: string, + handler: Parameters>[1], + ) { + listen(event, handler).then((unlisten) => { + if (disposed) { + unlisten(); + } else { + cleanup.push(unlisten); + } + }); + } + + register("install-progress", (e) => { + setLogs((prev) => [...prev.slice(-499), e.payload]); + }); + + // install-complete is informational only — does NOT trigger startServer. + // The invoke("start_install") success path handles that to avoid races. + register("install-complete", () => { + setCurrentStepIndex(999); // all steps done + }); + + register("install-step", (e) => { + const stepName = e.payload; + if (seenStepsRef.current.has(stepName)) return; // deduplicate + seenStepsRef.current.add(stepName); + setCurrentStepIndex((prev) => prev + 1); + setProgressDetail(null); + }); + + register("install-needs-elevation", (e) => { + elevationResumeRef.current = "install"; + setElevationPackages(e.payload); + setBackendStatus("needs-elevation"); + }); + + register("install-progress-detail", (e) => { + setProgressDetail(e.payload); + }); + + register("install-failed", (e) => { + setBackendError(e.payload, "install-error"); + }); + + register("repair-progress", (e) => { + setLogs((prev) => [...prev.slice(-499), e.payload]); + }); + + register("repair-needs-elevation", (e) => { + elevationResumeRef.current = "repair"; + setElevationPackages(e.payload); + setBackendStatus("needs-elevation"); + }); + + register("repair-complete", () => { + if (statusRef.current !== "repairing") return; + setProgressDetail("Repair complete"); + }); + + register("repair-failed", (e) => { + if (statusRef.current !== "repairing") return; + setBackendError(e.payload, "repair-error"); + }); + + register("server-port", (e) => { + portRef.current = e.payload; + setApiBase(e.payload); + }); + + register("server-crashed", () => { + startingRef.current = false; + setBackendError("Server stopped unexpectedly"); + }); + + register("server-log", (e) => { + setLogs((prev) => [...prev.slice(-499), e.payload]); + }); + + register("tray-toggle-server", () => { + if (statusRef.current === "running") { + stopServer(); + } else if ( + statusRef.current === "stopped" || + statusRef.current === "error" + ) { + retry(); + } + }); + }); + + const onAuthFailed = (event: Event) => { + const detail = + event instanceof CustomEvent && typeof event.detail === "string" + ? event.detail + : "Desktop authentication failed. Update or repair the managed Studio install, then restart Studio."; + setAuthFailure(detail); + }; + window.addEventListener("tauri-auth-failed", onAuthFailed); + const authFailure = getTauriAuthFailure(); + if (authFailure) setAuthFailure(authFailure); + cleanup.push(() => + window.removeEventListener("tauri-auth-failed", onAuthFailed), + ); + + return () => { + disposed = true; + cleanup.forEach((fn) => fn()); + stopExternalServerPoll(); + }; + }, []); + + return { + status, logs, error, isExternalServer, + currentStepIndex, progressDetail, elevationPackages, + startServer, stopServer, startInstall, + retry, retryInstall, approveElevation, + }; +} diff --git a/studio/frontend/src/hooks/use-tauri-update.ts b/studio/frontend/src/hooks/use-tauri-update.ts new file mode 100644 index 0000000000..5863ef7aa5 --- /dev/null +++ b/studio/frontend/src/hooks/use-tauri-update.ts @@ -0,0 +1,219 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +import { useEffect, useRef, useState } from "react"; +import { isTauri } from "@/lib/api-base"; +import { toast } from "sonner"; + +export type UpdateStatus = + | "idle" + | "checking" + | "available" + | "updating-backend" + | "downloading" + | "installing" + | "error"; + +export interface UpdateInfo { + version: string; + currentVersion: string; + body?: string; + date?: string; +} + +export function useTauriUpdate(isExternalServer = false) { + const [status, setStatus] = useState("idle"); + const [info, setInfo] = useState(null); + const [progress, setProgress] = useState(0); + const [logs, setLogs] = useState([]); + const [dismissed, setDismissed] = useState(false); + const [error, setError] = useState(null); + const updateRef = useRef + > | null>(null); + const checkedRef = useRef(false); + const updatingRef = useRef(false); + + useEffect(() => { + if (!isTauri || checkedRef.current) return; + checkedRef.current = true; + + async function checkForUpdate() { + setStatus("checking"); + try { + const { check } = await import("@tauri-apps/plugin-updater"); + const update = await check(); + if (update) { + updateRef.current = update; + setInfo({ + version: update.version, + currentVersion: update.currentVersion, + body: update.body, + date: update.date, + }); + setStatus("available"); + } else { + setStatus("idle"); + } + } catch (e) { + console.error("Update check failed:", e); + setStatus("idle"); + } + } + + const timer = setTimeout(checkForUpdate, 5000); + return () => clearTimeout(timer); + }, []); + + async function installUpdate() { + const update = updateRef.current; + if (!update || updatingRef.current) return; + updatingRef.current = true; + + const cleanups: (() => void)[] = []; + let phase: "backend" | "shell" = "backend"; + + try { + // ── Step 1: Backend update ── + setStatus("updating-backend"); + setLogs([]); + setError(null); + setDismissed(false); + + const { listen } = await import("@tauri-apps/api/event"); + const { invoke } = await import("@tauri-apps/api/core"); + + // Listen for backend update progress + const unlistenProgress = await listen( + "update-progress", + (e) => { + setLogs((prev) => [...prev.slice(-499), e.payload]); + }, + ); + cleanups.push(unlistenProgress); + + // Wait for complete or failed + const backendResult = await new Promise<"complete" | string>( + (resolve) => { + listen("update-complete", () => resolve("complete")).then( + (u) => cleanups.push(u), + ); + listen("update-failed", (e) => + resolve(e.payload), + ).then((u) => cleanups.push(u)); + + invoke("start_backend_update").catch((e) => resolve(String(e))); + }, + ); + + if (backendResult !== "complete") { + setError(backendResult); + setStatus("error"); + updatingRef.current = false; + cleanup(cleanups); + return; + } + + // ── Step 2: Shell update ── + phase = "shell"; + setStatus("downloading"); + setProgress(0); + + let downloaded = 0; + let contentLength = 0; + await update.downloadAndInstall((event) => { + switch (event.event) { + case "Started": + contentLength = event.data.contentLength ?? 0; + break; + case "Progress": + downloaded += event.data.chunkLength; + if (contentLength > 0) { + setProgress(Math.round((downloaded / contentLength) * 100)); + } + break; + case "Finished": + setStatus("installing"); + break; + } + }); + + // ── Step 3: Relaunch ── + const { relaunch } = await import("@tauri-apps/plugin-process"); + await relaunch(); + } catch (e) { + console.error("Update failed:", e); + const msg = String(e); + + // Shell update failed — restart backend on updated code + if (phase === "shell") { + try { + const { invoke } = await import("@tauri-apps/api/core"); + await invoke("start_server", { port: 8888 }); + toast.error("App update failed", { + description: + "Backend was updated. The app update will be retried on next launch.", + }); + setStatus("idle"); + setDismissed(true); + } catch { + setError(msg); + setStatus("error"); + } + } else { + setError(msg); + setStatus("error"); + } + } finally { + updatingRef.current = false; + cleanup(cleanups); + } + } + + async function retryUpdate() { + updatingRef.current = false; + await installUpdate(); + } + + async function skipAndRestart() { + try { + const { invoke } = await import("@tauri-apps/api/core"); + await invoke("start_server", { port: 8888 }); + setStatus("idle"); + setError(null); + setLogs([]); + setDismissed(true); + } catch (e) { + setError(String(e)); + setStatus("error"); + } + } + + function dismiss() { + setDismissed(true); + } + + return { + status, + info, + progress, + logs, + dismissed, + error, + isExternalServer, + installUpdate, + retryUpdate, + skipAndRestart, + dismiss, + }; +} + +function cleanup(fns: (() => void)[]) { + for (const fn of fns) { + try { + fn(); + } catch { + // ignore + } + } +} diff --git a/studio/frontend/src/lib/api-base.ts b/studio/frontend/src/lib/api-base.ts new file mode 100644 index 0000000000..664b9e58bd --- /dev/null +++ b/studio/frontend/src/lib/api-base.ts @@ -0,0 +1,24 @@ +// Central API base URL for Tauri vs browser mode +let apiBase = '' + +const isTauri = typeof window !== 'undefined' && '__TAURI__' in window +const isViteDev = import.meta.env.DEV + +if (isTauri && !isViteDev) { + apiBase = 'http://127.0.0.1:8888' +} + +export function setApiBase(port: number) { + apiBase = `http://127.0.0.1:${port}` +} + +export function getApiBase(): string { + return apiBase +} + +export function apiUrl(path: string): string { + if (path.startsWith('http')) return path + return `${apiBase}${path}` +} + +export { isTauri } diff --git a/studio/frontend/src/lib/open-link.ts b/studio/frontend/src/lib/open-link.ts new file mode 100644 index 0000000000..b7e6a0fa6e --- /dev/null +++ b/studio/frontend/src/lib/open-link.ts @@ -0,0 +1,31 @@ +import { isTauri } from "@/lib/api-base"; + +/** + * Open a URL in the system browser (Tauri) or new tab (web). + * Handles anchor links and mailto: natively without the opener plugin. + * Returns true when the caller should preventDefault; false when the + * browser's native navigation should proceed (relative URLs, empty, etc.). + */ +export function openLink(url: string): boolean { + if (!url) return false; + + // Anchor links — scroll within the page, don't open externally + if (url.startsWith("#")) { + window.location.hash = url; + return true; + } + + // Relative URLs — let the browser / router handle them natively + if (!url.includes("://") && !url.startsWith("mailto:")) { + return false; + } + + if (isTauri) { + import("@tauri-apps/plugin-opener").then(({ openUrl }) => { + openUrl(url).catch(console.error); + }); + } else { + window.open(url, "_blank", "noopener,noreferrer"); + } + return true; +} diff --git a/studio/install_llama_prebuilt.py b/studio/install_llama_prebuilt.py index 1ad1ccc851..23e6b7c0e4 100755 --- a/studio/install_llama_prebuilt.py +++ b/studio/install_llama_prebuilt.py @@ -46,6 +46,28 @@ EXIT_ERROR = 1 EXIT_BUSY = 3 +def windows_hidden_subprocess_kwargs() -> dict[str, object]: + """Return Windows-only subprocess kwargs that suppress console windows.""" + if sys.platform != "win32": + return {} + + kwargs: dict[str, object] = {} + create_no_window = getattr(subprocess, "CREATE_NO_WINDOW", 0) + if create_no_window: + kwargs["creationflags"] = create_no_window + + startupinfo_factory = getattr(subprocess, "STARTUPINFO", None) + startf_use_showwindow = getattr(subprocess, "STARTF_USESHOWWINDOW", 0) + sw_hide = getattr(subprocess, "SW_HIDE", 0) + if startupinfo_factory is not None and startf_use_showwindow: + startupinfo = startupinfo_factory() + startupinfo.dwFlags |= startf_use_showwindow + startupinfo.wShowWindow = sw_hide + kwargs["startupinfo"] = startupinfo + + return kwargs + + def env_int(name: str, default: int, *, minimum: int | None = None) -> int: raw = os.environ.get(name) if raw is None: @@ -2469,6 +2491,7 @@ def run_capture( text = True, timeout = timeout, env = env, + **windows_hidden_subprocess_kwargs(), ) if check and result.returncode != 0: raise subprocess.CalledProcessError( @@ -4357,6 +4380,7 @@ def validate_quantize( text = True, timeout = 120, env = binary_env(quantize_path, install_dir, host, runtime_line = runtime_line), + **windows_hidden_subprocess_kwargs(), ) if ( result.returncode != 0 @@ -4444,6 +4468,7 @@ def validate_server( env = binary_env( server_path, install_dir, host, runtime_line = runtime_line ), + **windows_hidden_subprocess_kwargs(), ) deadline = time.time() + 20 startup_started = time.time() diff --git a/studio/install_python_stack.py b/studio/install_python_stack.py index 2b9fd084d4..b56b737cfa 100644 --- a/studio/install_python_stack.py +++ b/studio/install_python_stack.py @@ -359,6 +359,28 @@ def _ensure_rocm_torch() -> None: ) +def _windows_hidden_subprocess_kwargs() -> dict[str, object]: + """Return Windows-only subprocess kwargs that suppress console windows.""" + if not IS_WINDOWS: + return {} + + kwargs: dict[str, object] = {} + create_no_window = getattr(subprocess, "CREATE_NO_WINDOW", 0) + if create_no_window: + kwargs["creationflags"] = create_no_window + + startupinfo_factory = getattr(subprocess, "STARTUPINFO", None) + startf_use_showwindow = getattr(subprocess, "STARTF_USESHOWWINDOW", 0) + sw_hide = getattr(subprocess, "SW_HIDE", 0) + if startupinfo_factory is not None and startf_use_showwindow: + startupinfo = startupinfo_factory() + startupinfo.dwFlags |= startf_use_showwindow + startupinfo.wShowWindow = sw_hide + kwargs["startupinfo"] = startupinfo + + return kwargs + + def _infer_no_torch() -> bool: """Determine whether to run in no-torch (GGUF-only) mode. @@ -412,9 +434,11 @@ _UNICODE_TO_ASCII: dict[str, str] = { def _safe_print(*args: object, **kwargs: object) -> None: - """Drop-in print() replacement that survives non-UTF-8 consoles.""" + """Drop-in print() replacement that survives non-UTF-8 consoles and detached stdout.""" try: print(*args, **kwargs) + except OSError: + return except UnicodeEncodeError: # Stringify, then swap emoji for ASCII equivalents text = " ".join(str(a) for a in args) @@ -495,7 +519,7 @@ def _step(label: str, value: str, color_fn = None) -> None: if color_fn is None: color_fn = _green padded = label[:_COL] - print(f" {_dim(padded)}{' ' * (_COL - len(padded))}{color_fn(value)}") + _safe_print(f" {_dim(padded)}{' ' * (_COL - len(padded))}{color_fn(value)}") def _progress(label: str) -> None: @@ -509,10 +533,13 @@ def _progress(label: str) -> None: bar = "=" * filled + "-" * (width - filled) pad = " " * (_COL - len(_LABEL)) end = "\n" if _STEP >= _TOTAL else "" - sys.stdout.write( - f"\r {_dim(_LABEL)}{pad}[{bar}] {_STEP:2}/{_TOTAL} {label:<20}{end}" - ) - sys.stdout.flush() + try: + sys.stdout.write( + f"\r {_dim(_LABEL)}{pad}[{bar}] {_STEP:2}/{_TOTAL} {label:<20}{end}" + ) + sys.stdout.flush() + except OSError: + pass def run( @@ -525,6 +552,7 @@ def run( cmd, stdout = subprocess.PIPE if quiet else None, stderr = subprocess.STDOUT if quiet else None, + **_windows_hidden_subprocess_kwargs(), ) if result.returncode != 0: _step("error", f"{label} failed (exit code {result.returncode})", _red) @@ -627,6 +655,7 @@ def _bootstrap_uv() -> bool: ["uv", "pip", "install", "--dry-run", "--python", sys.executable, "pip"], stdout = subprocess.PIPE, stderr = subprocess.STDOUT, + **_windows_hidden_subprocess_kwargs(), ) if probe.returncode != 0: # Retry with --system (some envs need it when uv can't find a venv) @@ -634,6 +663,7 @@ def _bootstrap_uv() -> bool: ["uv", "pip", "install", "--dry-run", "--system", "pip"], stdout = subprocess.PIPE, stderr = subprocess.STDOUT, + **_windows_hidden_subprocess_kwargs(), ) if probe_sys.returncode != 0: return False # uv is broken, fall back to pip @@ -773,6 +803,7 @@ def pip_install( uv_cmd, stdout = subprocess.PIPE, stderr = subprocess.STDOUT, + **_windows_hidden_subprocess_kwargs(), ) if result.returncode == 0: return @@ -798,6 +829,7 @@ def patch_package_file(package_name: str, relative_path: str, url: str) -> None: [sys.executable, "-m", "pip", "show", package_name], capture_output = True, text = True, + **_windows_hidden_subprocess_kwargs(), ) if result.returncode != 0: _step(_LABEL, f"package {package_name} not found, skipping patch", _red) @@ -868,6 +900,7 @@ def install_python_stack() -> int: [sys.executable, "-m", "pip", "--version"], stdout = subprocess.DEVNULL, stderr = subprocess.DEVNULL, + **_windows_hidden_subprocess_kwargs(), ).returncode == 0 ) @@ -1140,6 +1173,7 @@ def install_python_stack() -> int: [sys.executable, "-m", "pip", "check"], stdout = subprocess.DEVNULL, stderr = subprocess.DEVNULL, + **_windows_hidden_subprocess_kwargs(), ) _step(_LABEL, "installed") diff --git a/studio/setup.ps1 b/studio/setup.ps1 index 218be7e045..24ef3ef1eb 100644 --- a/studio/setup.ps1 +++ b/studio/setup.ps1 @@ -1077,10 +1077,13 @@ if (-not $CudaArch) { } # ============================================ -# 1f. Node.js / npm (skip if pip-installed -- only needed for frontend build) +# 1f. Node.js / npm (skip if pip-installed or Tauri -- only needed for frontend build) # ============================================ +$SkipFrontend = ($env:SKIP_STUDIO_FRONTEND -eq "1") if ($IsPipInstall) { step "frontend" "bundled (pip install)" +} elseif ($SkipFrontend) { + step "frontend" "bundled (Tauri)" } else { # setup.sh installs Node LTS (v22) via nvm. We enforce the same range here: # Vite 8 requires Node ^20.19.0 || >=22.12.0, npm >= 11. @@ -1207,6 +1210,9 @@ $NeedFrontendBuild = $true if ($IsPipInstall) { $NeedFrontendBuild = $false step "frontend" "bundled (pip install)" +} elseif ($SkipFrontend) { + $NeedFrontendBuild = $false + step "frontend" "bundled (Tauri)" } elseif (Test-Path $DistDir) { $DistTime = (Get-Item $DistDir).LastWriteTime $NewerFile = $null @@ -1539,7 +1545,7 @@ if (Get-Command uv -ErrorAction SilentlyContinue) { } else { substep "installing uv package manager..." try { - Invoke-SetupCommand { powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" } | Out-Null + Invoke-SetupCommand { Invoke-Expression (Invoke-RestMethod -Uri "https://astral.sh/uv/install.ps1") } | Out-Null Refresh-Environment # Re-activate venv since Refresh-Environment rebuilds PATH from # registry and drops the venv's Scripts directory diff --git a/studio/setup.sh b/studio/setup.sh index c2044b5141..142d253554 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -183,7 +183,13 @@ if [[ "$keynames" == *$'\nCOLAB_'* ]]; then fi if [ "$_LLAMA_ONLY" != "1" ]; then -# ── Frontend ── +# ── Detect whether frontend needs building ── +# Skip if SKIP_STUDIO_FRONTEND=1 (Tauri desktop app bundles its own frontend), +# or if dist/ exists AND no tracked input is newer than dist/. +if [ "${SKIP_STUDIO_FRONTEND:-0}" = "1" ]; then + _NEED_FRONTEND_BUILD=false + step "frontend" "bundled (Tauri)" +else _NEED_FRONTEND_BUILD=true if [ -d "$SCRIPT_DIR/frontend/dist" ]; then _changed=$(find "$SCRIPT_DIR/frontend" -maxdepth 1 -type f \ @@ -195,6 +201,7 @@ if [ -d "$SCRIPT_DIR/frontend/dist" ]; then fi [ -z "$_changed" ] && _NEED_FRONTEND_BUILD=false fi +fi # end SKIP_STUDIO_FRONTEND guard if [ "$_NEED_FRONTEND_BUILD" = false ]; then step "frontend" "up to date" @@ -484,9 +491,9 @@ _PKG_NAME="${STUDIO_PACKAGE_NAME:-unsloth}" if [ "$_SKIP_VERSION_CHECK" != true ] && [ "${SKIP_STUDIO_BASE:-0}" != "1" ] && [ "${STUDIO_LOCAL_INSTALL:-0}" != "1" ]; then # Only check when NOT called from install.sh (which just installed the package) INSTALLED_VER=$("$VENV_DIR/bin/python" -c " -from importlib.metadata import version -print(version('$_PKG_NAME')) -" 2>/dev/null || echo "") +import sys; from importlib.metadata import version +print(version(sys.argv[1])) +" "$_PKG_NAME" 2>/dev/null || echo "") LATEST_VER=$(curl -fsSL --max-time 5 "https://pypi.org/pypi/$_PKG_NAME/json" 2>/dev/null \ | "$VENV_DIR/bin/python" -c "import sys,json; print(json.load(sys.stdin)['info']['version'])" 2>/dev/null \ diff --git a/studio/src-tauri/Cargo.lock b/studio/src-tauri/Cargo.lock new file mode 100644 index 0000000000..62d520437c --- /dev/null +++ b/studio/src-tauri/Cargo.lock @@ -0,0 +1,6320 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[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_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.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[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.117", +] + +[[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.117", +] + +[[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 = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +dependencies = [ + "serde_core", +] + +[[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 = "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.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +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.11.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.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.2.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +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 = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.0", +] + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +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.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[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.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", + "core-graphics-types", + "foreign-types 0.5.0", + "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.11.0", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +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" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.13.1", + "smallvec", +] + +[[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.117", +] + +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +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.117", +] + +[[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.117", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[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 = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.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.117", +] + +[[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.117", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser 0.36.0", + "foldhash 0.2.0", + "html5ever 0.38.0", + "precomputed-hash", + "selectors 0.36.1", + "tendril 0.5.0", +] + +[[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 = "elevated-command" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54c410eccdcc5b759704fdb6a792afe6b01ab8a062e2c003ff2567e2697a94aa" +dependencies = [ + "anyhow", + "base64 0.21.7", + "libc", + "log", + "winapi", + "windows 0.52.0", +] + +[[package]] +name = "embed-resource" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47ec73ddcf6b7f23173d5c3c5a32b5507dc0a734de7730aa14abc5d5e296bb5f" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 0.9.12+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 = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[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.117", +] + +[[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.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +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 = "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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fix-path-env" +version = "0.0.0" +source = "git+https://github.com/tauri-apps/fix-path-env-rs#c4c45d503ea115a839aae718d02f79e7c7f0f673" +dependencies = [ + "home", + "strip-ansi-escapes", + "thiserror 1.0.69", +] + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +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 = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + +[[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 0.3.1", +] + +[[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.117", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[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 = "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.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[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.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "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", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.0", + "wasip2", + "wasip3", +] + +[[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.11.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.117", +] + +[[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.117", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.13.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[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 = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "html5ever" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" +dependencies = [ + "log", + "mac", + "markup5ever 0.14.1", + "match_token", +] + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever 0.38.0", +] + +[[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", + "h2", + "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", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +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.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +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 = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[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.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[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.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[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 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +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.11.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 0.29.6", + "html5ever 0.29.1", + "indexmap 2.13.0", + "selectors 0.24.0", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[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.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[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.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +dependencies = [ + "bitflags 2.11.0", + "libc", + "plain", + "redox_syscall 0.7.3", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[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" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[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 0.8.9", + "string_cache_codegen 0.5.4", + "tendril 0.4.3", +] + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril 0.5.0", + "web_atoms", +] + +[[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.117", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e" + +[[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 = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.11.0", + "jni-sys 0.3.1", + "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 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +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.11.0", + "block2", + "objc2", + "objc2-core-foundation", + "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.11.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.11.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "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.11.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.11.0", + "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.11.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.11.0", + "objc2", + "objc2-core-foundation", + "objc2-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.11.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.11.0", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[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 = "openssl" +version = "0.10.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[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 = "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" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros 0.13.1", + "phf_shared 0.13.1", + "serde", +] + +[[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_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", +] + +[[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_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] + +[[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.117", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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.2", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher 1.0.2", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +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 = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[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", + "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 = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[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.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.5+spec-1.1.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.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "process-wrap" +version = "9.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e842efad9119158434d193c6682e2ebee4b44d6ad801d7b349623b3f57cdf55" +dependencies = [ + "indexmap 2.13.0", + "nix", + "tracing", + "windows 0.62.2", +] + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +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 = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[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.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.0", +] + +[[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_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.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + +[[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.11.0", +] + +[[package]] +name = "redox_syscall" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags 2.11.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.117", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[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", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "reqwest" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +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", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[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 = "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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +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.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[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 = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +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.117", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[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 0.29.6", + "derive_more 0.99.20", + "fxhash", + "log", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc 0.2.0", + "smallvec", +] + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.11.0", + "cssparser 0.36.0", + "derive_more 2.1.1", + "log", + "new_debug_unreachable", + "phf 0.13.1", + "phf_codegen 0.13.1", + "precomputed-hash", + "rustc-hash", + "servo_arc 0.4.3", + "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.117", +] + +[[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.117", +] + +[[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.117", +] + +[[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.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.13.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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.117", +] + +[[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 = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "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 = "simplelog" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0" +dependencies = [ + "log", + "termcolor", + "time", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.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" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.13.1", + "precomputed-hash", +] + +[[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 = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", +] + +[[package]] +name = "strip-ansi-escapes" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" +dependencies = [ + "vte", +] + +[[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.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +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.117", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9103edf55f2da3c82aea4c7fab7c4241032bfeea0e71fa557d98e00e7ce7cc20" +dependencies = [ + "bitflags 2.11.0", + "block2", + "core-foundation 0.10.1", + "core-graphics", + "crossbeam-channel", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "once_cell", + "parking_lot", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows 0.61.3", + "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.117", +] + +[[package]] +name = "tar" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" +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.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da77cc00fb9028caf5b5d4650f75e31f1ef3693459dfca7f7e506d1ecef0ba2d" +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 0.13.2", + "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 0.61.3", +] + +[[package]] +name = "tauri-build" +version = "2.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bbc990d1dbf57a8e1c7fa2327f2a614d8b757805603c1b9ba5c81bade09fd4d" +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.12+spec-1.1.0", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a24476afd977c5d5d169f72425868613d82747916dd29e0a357c84c4bd6d29" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.117", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d39b349a98dadaffebb73f0a40dcd1f23c999211e5a2e744403db384d0c33de7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddde7d51c907b940fb573006cdda9a642d6a7c8153657e88f8a5c3c9290cd4aa" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "toml 0.9.12+spec-1.1.0", + "walkdir", +] + +[[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 0.61.3", + "zbus", +] + +[[package]] +name = "tauri-plugin-process" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55511a7bf6cd70c8767b02c97bf8134fa434daf3926cfc1be0a0f94132d165a" +dependencies = [ + "tauri", + "tauri-plugin", +] + +[[package]] +name = "tauri-plugin-single-instance" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc61e4822b8f74d68278e09161d3e3fdd1b14b9eb781e24edccaabf10c420e8c" +dependencies = [ + "serde", + "serde_json", + "tauri", + "thiserror 2.0.18", + "tracing", + "windows-sys 0.60.2", + "zbus", +] + +[[package]] +name = "tauri-plugin-updater" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806d9dac662c2e4594ff03c647a552f2c9bd544e7d0f683ec58f872f952ce4af" +dependencies = [ + "base64 0.22.1", + "dirs", + "flate2", + "futures-util", + "http", + "infer", + "log", + "minisign-verify", + "osakit", + "percent-encoding", + "reqwest 0.13.2", + "rustls", + "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-runtime" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2826d79a3297ed08cd6ea7f412644ef58e32969504bc4fbd8d7dbeabc4445ea2" +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 0.61.3", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11ea2e6f801d275fdd890d6c9603736012742a1c33b96d0db788c9cdebf7f9e" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows 0.61.3", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219a1f983a2af3653f75b5747f76733b0da7ff03069c7a41901a5eb3ace4557d" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dunce", + "glob", + "html5ever 0.29.1", + "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.12+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.12+spec-1.1.0", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "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 = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[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.117", +] + +[[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.117", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +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 = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[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-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.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +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.15", +] + +[[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_datetime" +version = "1.0.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.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.25.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1" +dependencies = [ + "indexmap 2.13.0", + "toml_datetime 1.0.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.0", +] + +[[package]] +name = "toml_parser" +version = "1.0.10+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" +dependencies = [ + "winnow 1.0.0", +] + +[[package]] +name = "toml_writer" +version = "1.0.7+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d" + +[[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.11.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.117", +] + +[[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 = "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.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[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.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsloth-studio" +version = "2026.4.7" +dependencies = [ + "dirs", + "elevated-command", + "fix-path-env", + "libc", + "log", + "open", + "process-wrap", + "rand 0.10.0", + "regex", + "reqwest 0.12.28", + "serde", + "serde_json", + "simplelog", + "tauri", + "tauri-build", + "tauri-plugin-opener", + "tauri-plugin-process", + "tauri-plugin-single-instance", + "tauri-plugin-updater", + "tokio", + "windows 0.62.2", + "windows-sys 0.59.0", +] + +[[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_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[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 = "vte" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" +dependencies = [ + "memchr", +] + +[[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 = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.13.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a9779e9f04d2ac1ce317aee707aa2f6b773afba7b931222bff6983843b1576" +dependencies = [ + "phf 0.13.1", + "phf_codegen 0.13.1", + "string_cache 0.9.0", + "string_cache_codegen 0.6.1", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +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-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +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 0.61.3", + "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.117", +] + +[[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 0.61.3", + "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.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core 0.52.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections 0.2.0", + "windows-core 0.61.2", + "windows-future 0.2.1", + "windows-link 0.1.3", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", +] + +[[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-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[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 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", +] + +[[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.117", +] + +[[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.117", +] + +[[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-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[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-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + +[[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.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" +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" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.13.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap 2.13.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "wry" +version = "0.54.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a8135d8676225e5744de000d4dff5a082501bf7db6a1c1495034f8c314edbc" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "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 0.61.3", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[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.117", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" +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.15", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "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.15", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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.117", + "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.117", +] + +[[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.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zvariant" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 0.7.15", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "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.117", + "winnow 0.7.15", +] diff --git a/studio/src-tauri/Cargo.toml b/studio/src-tauri/Cargo.toml new file mode 100644 index 0000000000..ccb07cb9e2 --- /dev/null +++ b/studio/src-tauri/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "unsloth-studio" +version = "2026.4.7" +description = "Unsloth Studio Desktop App" +authors = ["Unsloth AI"] +edition = "2021" + +[dependencies] +tauri = { version = "2", features = ["tray-icon"] } +tauri-plugin-single-instance = "2" +tauri-plugin-process = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +reqwest = { version = "0.12", features = ["json"] } +tokio = { version = "1", features = ["full"] } +log = "0.4" +simplelog = "0.12" +dirs = "6" +regex = "1" +open = "5" +process-wrap = { version = "9", features = ["std"] } +fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs" } +tauri-plugin-opener = "2.5.3" +tauri-plugin-updater = "2" +rand = "0.10.0" + +[target.'cfg(unix)'.dependencies] +libc = "0.2" + +[target.'cfg(target_os = "linux")'.dependencies] +elevated-command = "1.1.2" + +[target.'cfg(windows)'.dependencies] +windows = { version = "0.62.2", features = ["Win32_System_Threading"] } +windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_Security", "Win32_System_Console", "Win32_System_JobObjects", "Win32_System_Threading"] } + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[features] +default = ["custom-protocol"] +custom-protocol = ["tauri/custom-protocol"] diff --git a/studio/src-tauri/Entitlements.plist b/studio/src-tauri/Entitlements.plist new file mode 100644 index 0000000000..10e6df3427 --- /dev/null +++ b/studio/src-tauri/Entitlements.plist @@ -0,0 +1,15 @@ + + + + + + com.apple.security.cs.allow-unsigned-executable-memory + + + com.apple.security.network.client + + + com.apple.security.cs.disable-library-validation + + + diff --git a/studio/src-tauri/build.rs b/studio/src-tauri/build.rs new file mode 100644 index 0000000000..d860e1e6a7 --- /dev/null +++ b/studio/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/studio/src-tauri/capabilities/default.json b/studio/src-tauri/capabilities/default.json new file mode 100644 index 0000000000..7b6033c721 --- /dev/null +++ b/studio/src-tauri/capabilities/default.json @@ -0,0 +1,24 @@ +{ + "identifier": "default", + "description": "Default capabilities for Unsloth Studio", + "windows": ["main"], + "permissions": [ + "core:default", + "core:window:default", + "core:window:allow-show", + "core:window:allow-hide", + "core:window:allow-set-focus", + "core:window:allow-set-size", + "core:window:allow-set-resizable", + "core:window:allow-set-size-constraints", + "core:window:allow-center", + "core:window:allow-current-monitor", + "core:tray:default", + "process:default", + { + "identifier": "opener:allow-open-url", + "allow": [{ "url": "https://*" }, { "url": "http://*" }, { "url": "mailto:*" }] + }, + "updater:default" + ] +} diff --git a/studio/src-tauri/icons/128x128.png b/studio/src-tauri/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..c1dae04e6dfaf806cbbb4dc6ea25c1e38276f07e GIT binary patch literal 9194 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 literal 0 HcmV?d00001 diff --git a/studio/src-tauri/icons/32x32.png b/studio/src-tauri/icons/32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..bd08d7bcbb6badbdb5abc9a230b5f45d357e1a46 GIT binary patch literal 1930 zcmV;52X**~P)wL)U(wuW>WG;7>4 z|5&MwS&WIZ$;ONzYgwnG)}D!!*E_j8?(O&6^L&r{2@0mOYx-O+_x*i; z-|zE$p7-Z;woCO2zCzM{Q3e|NFe!L4J)|c_uA!@n0=It>zEA*?M)TzZU?CB~gB}-T zorE>X=}4X*kI6s)dP(~~1JQk6!E9ld&x=Vpi22resL9!c4WSr(+W8M8FI|RLldNbR z=!3;zMU=tBzJaVLUmO5y>?Famq`}723>3$uV3psDp11yhijybd82$`(wLisgst=)g zK^i(-4z%3<5SoyJ7^4|MMSgq?JuVBhBz(wwAJ2Y$8IC69z}eD^znps;9o=2HOM;p7 zdOh?;BjTc>QJ9yHk{#c}{NiWu+TC8X-R?(%W!_^!%n5*nv!APTzJ=#MQBZy8$GG&JK{uLOf$S2%b-4*;Do`vl4|3h;E71KZ1X ztEOgR!K}H40#lMC@ypM=FU#_bvpXvGBD3=oRAv+kSI!1Pdbp@~gMOsh5>OdP#E#NZ zv2QRK5TG<;AxwFBc|v;@$iD{<9>mU_JH<1f;j@M*5HHYv+2jH&wJj99BZX^L09w+< z8Svp~dLiC;`4{le+GH}p@Ao4*IvS+}sK37-$B!Qu+Lx4+hjy-$! zz-%_dPayd2Bo(ROz5FVUq!++N{In0%2{j0L+aC%dDb|M9HOukswMEeBbqEH6SigQf znwy&iL#Ba&0l3|6csw5D=jY?rty@S-OH-Nh{+2CU(9+UEJoLmPfJ>cM@RIZqJ{=u` zQL9r6xAdUcNVrc3i<8pv_dowN>~9jDm6e6o)>aG-4&vIiYhph&H5HkenaIh>!MSti zkeHaL3gDAfUiLS0LR8>`pSg_meICbijc;|FF#h0nEu@PRcR{&eHW{o)4 zK|=W)6Z4EE-Yg@MEi(Xc-8ShBkneji?i>$;B4&}4;!7136&M>ELqbA=*h@)C5oTtx zSS-RKH8nNp>FE)f!@`*d)7bbpLQXgJ3M0vqXXyn>I1bon!Kq7}H4BfJdGFr6XlrXjLqmfQ%+`wV;8YEei~K&H$Q~u)^63CHqI=2E5pq3g0tyzs zxMUT-4Ro3IB73+`D%Vr%#_20LDq%(a}+C+qMl3 zheNF;2FfDYCC{h?(*RHrBGj5FGieo#F|S)_!VEa~%gf8vOPrRqwY6}$Tmq!cW+azIKb}rs|uLmQ8;f}|?A08gY zv17-?^Xk>B(bUu=+{3ZKg=adkg$^G+tj5v&1vcboWFufP&;;d!*dwB21C{&Ly3T&9uC7L5VIeMDxF9s=JtMW@i13Mt30%B*QCNqC^ZQ5uSC*Bd z;qBk!)SY)>q^U`ns~z*?!^21B;#Aa*#pAo*+@uB(M|h+c7)UVpg9I`))6mcmdU|`| zo19czHJ{Z`T~TBOfBIW98WP9R`|-aKYqHFklx9ps{36z9L7!s)a&{J0KeY^fH~)nd zD_7#f4?YkY@tL(bd1$BlFrA)TTL61+k65#7B6n7#EJbMfVqEXLjyRL`k>TW#{>a$m zkc@ZygV?`6?1rmt?Ko2P6Tx`*4_?3@?N@P(ToR!pWb!6^C(3s37H3}k;g8UDHC#>i z@7ss=&~SKum~HT}>?;2GOaKZjIz4WTI8eUfJ6Pt{LYt9{6~%=}@k-cIa}Y&+XYtba zcj4~kZlo<*jD3x-p|JZc{H&xDOM@nCI(`Ta=Lk}LdaSD2g;zdmheguQ`rl#Jk&ONB zqt0j}lO4@?4rgy(#~qq#UZla|{H=GUeBWxvQWu(2mESGgvh*B z+Vhb4U49R~X0#$id3B!xn%4%Z1#^w)kDZD&Q}uyUlrw?XoWK0xj0rat9?djD08cwS zaPS$ftTeERCZ{~<{{p@^J!+?~g+^1;O+-5e7L2TwaBcb0{u}n+a07*qoM6N<$f@%`GBLDyZ literal 0 HcmV?d00001 diff --git a/studio/src-tauri/icons/icon.icns b/studio/src-tauri/icons/icon.icns new file mode 100644 index 0000000000000000000000000000000000000000..e67a5e64744a7208847d39bca3943d1e99298549 GIT binary patch literal 263568 zcmeFYWmFtZyRh4XI|P?7KyY{WKycTPAR#~q?gR@A&fp;s+}+*XJrLX>XmATI^Ud>~ zwbwd(@8|dVb3RtDrn{?J>aMG*ZW}8zdlvu+Io8USmj?jQD9Z^!L`2DiNTh@qIK&Br z=)|OiAbcqf95Ui>gxJKyXaK%POM3fj0E3t~nh=*19aTBDBkUy*I1{ddhE5tu_<{sM zx-Q><0sxRPDBtBbz6Ozo6C#lS?ma>f0E7ez0FZ>cT|p#aglMFoz!CsJLPCT903rkl z832j{0MZab5>iCdTKN4)002S&0RX~3MV01=q(OuzM7Rw77yt_V4u1IKY~Div6chlh zj}ezBkdTZNNHzh`0PJ`3l?ckfG_1=Q9e!$*3?dC6#3jOIZUm43=^Geqb!rG$fxytN zWZ@SYS#gN`3DHRq?BNgPhHVT&U^oxOe_^n}O&DAvzIF&C{)AYh2x)HsKuiQcR=IzK zz%U=X{Yzk{c zYpU4E`tQtUY*#jgG?Fw`E|vaIW{Z|9n}QpW>w0IA{~NM!X(3Mc)&(_^HC66w{cmD5 z_A8qL8*%FT2jI;7zko%B1B(pw4b%m|+gQ2o@o&7ktyebrH)1taZwvn$uY%jvO#zMM zO-m&IF!n!jC0c4~@NcAOXdg%WH(ZfNJL>%#k?T8#kpBf&P+u3k6n^th^nc?Ob*!U4 zun~EQ=-+6?U;O9ANKMt7@+AL@RwC)G8hD#9o9g!OZ%P_2wVDr<=j@4rOF*o0F`;yh~1&iO!4WW%hP4(^jeXV=FRlO@~JyqR%9liVQ zb^p|Fs;{W8uc<Yi@$?{yDC1ih%Rh)Y#P6*woPQ&%Uv#X{WIq3JMQTh9X6T1q6pD zK+(e^!oG$Vr3Ocae}iI&hx>nx=$p>!E%*`;9v%(FjR*)V|2gkZ4EP=lmj?z!L_%Lg z_!cbYX%PRzE172Aa>3V#aQM@{_V#f77q8qsJ-!iPP_&4z!@2*R*X-f15g|~L2*0NC z|H*4fi*G~_6eT?P$0X{10~Q4tK%Php4iAKqMfgtZ{cmD*rhFp;p*X?GJ#eZ2FJik> zf&-xF5x$*X|3<7wr*DKm6f44SQvBbD6`6!Y1VG6`$I1VVSd#Iu04PORR4>NA0gKub z6Y38|4vXnP{TE;nTff0e;WzhT{u{99y)mHyP?XSd(*Hp$%0Kff;TZhIND+_;72^K| zETP<#f4DysGdw8k&%XfsCo3o%-lA~-+~)rYEC^^QfP@FZh2=lP^dDfw;J}Lh2Uxo4 zfQaCKI<7qmICKDv{tH;k6Ce~W!cZtd=#1UilEbg)H36aZs9%or6SlMAVNjy*kZ-@z zW5!bal7CGlLK4T~(tpJT!wU$)Lw$nbTSRa;G&mx}Cp6e6G&}+hTsRcI!7IRb5#iwx zJ5XC8I~zDfXt4kL_kShuev$|3r=zCn%w7$ij-5<-pT0sF0fE7=!tqwp}(|den;69-Xu$w+~J<9i1GFr9PYp-^8!q zrEj`d8m4Qgr3n=$;(t-z=!5bC@DnGLE+_%xPe90NvyTj5gkM2~7ZF$j@S;sD2m$;G z$Z_-k%gW|~c?;~)Pd`Xw-m1{Ul$Cn!JH=RcpXjn-r3y`+HSkXzc9`e

r@5j#1yz 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 z%0D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.!=!G~*URb_NKZ`~Di5 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 RT{H-IX{qVnFITY%`)_e+9*_V4 literal 0 HcmV?d00001 diff --git a/studio/src-tauri/icons/icon.ico b/studio/src-tauri/icons/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..acc6eb82b9b52829178ffd2943fcfcceec6ec0ac GIT binary patch literal 34589 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 z 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 W#)$R{xljq&_RAOaj{moB@P7dEKw~lh literal 0 HcmV?d00001 diff --git a/studio/src-tauri/icons/icon.png b/studio/src-tauri/icons/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..635900d8a7edc917157d5e1ac9552763950d128f GIT binary patch literal 46007 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�JyHt`&Cdf~3g zbnq*6|G~tHbhVb^-<}nj4(GmIo~>UW;ZsF=4^AqhQ0cC%pIwnR`l?sMh<~&a)O%cI znX3W&d|HTA_*QO2KV!thuix5;h%+bfl1RdT@M}ttTcDI@Ns&mG%4R1{2z-4nDLUb` zC!@oEC7GRdH{z@G0{a#Dg@$klvgz>w)vQ#Y=m0t&_omiy#$i;kq9=0MXC>&z)1Z9Z zAUyO11JpM=mvL+2N9;q-LB8>NS94aJ0ul>rzD4V{;UoC@k05$G^Fz9=cwO z*I0|74<5}y-9zzh_3$tr1R*Wd!zFQB0?w2EeiB#zBypsa$=fy=!W!Os9+i+Mxay=~ z>9Agw-;MJpmj2}YfFeigHknvbgMf{l-`}7)eQP6{+b(-#xcRrn^}#28B=T3_=5VH1 zlCb0SlT-=6s=-WggP!BF{RJvs1nkiCky~r_0V{%ooD=dR!{ful;EUhJlm+^w#;NBT zeu5A?;>C&sojJRC$Y_=Ix9|SNc(>(+MWV#}0gdz(`th_Y*SVL7!UJ2q<2N_!m0PEh z4V9*3sgnLzcOR=K&~utvkARAt{XY19 z{PL-2)7n!3zJosV0li)AsSEs7orNOs?U+s2lqB2}t-LSLG>a`)rlMK4k9)>%a^+MP z9>zQQ6p50J(U#gc`k|J}L?3*S`^2o-5ST}EB;j~LU%)HpbZH>Nd-bj>X2lS>_atNY zWksS86QBGu86iGTFwd8bsx`4CU+jt798#`8K=D#)BMo|UQ%V!#@Vfa*(OXRaMeuTB zF~>C*&N?ZY;9zT0BZXIX--oXW>9wo>;0R_TMkCfk{9A><6}$x<7TbL?MQS)^;TMJQ z#yq-b@N=r(xj)7b{P4B{^jkU93kG;@9xVwabn)Dwjd zetkIq$kRH?FRhE_-K`9jIy;Th>;-k>X_2>yFcv|2=mJXq@evS{SKz(KTFGN_i?t_h zZuQUFu^5*ij<_vyalRSbiuy=F*YZgrFt+^;&Sa~BL^XPw@$52)%&g#2`f7_=??`U) ze8CmTxA3^bj1oc0*NiIz8KrB}|K&yweoPoV1OwSYEdogIGWz4gSU~cN+1dkHxAccUe~Q!HQ`{nCkSmUk1|8 z$C1Q>yXS(a5J}XJ?BF6Epf#QW%l&FfDZ>Q{h5+MgGYOr9A?f>mM^4?f)X;X%qeaw8 zs&`vpwVPe>+`G@J&bNW?+%n9*l(t6eO>ipHRwwx=g?W1@Het+OAa!mYI;@(v_}89Y zO%n@9v;*eQ7Oz2UaC-`RrK*lGUn#mvAFHrED5HF2ad!+nTg4H(|jd$1_A68L`La_ z*rl_e0$VA-D=LbSCh0t=<8k+W_h-S?p#FpH&C^M=YxCjzXnJWgJq!t2zT|F5kzVo6 z*6Ir?({F#nrcFovT=AQN4JEki=1lYlI96Iq^TLg~g&DFTvbVWHv~`XW3hHAdogc`1 zQ&L`-y3g}AGDo|g@j&i9(z=g=M%=rk`Enz@%KfhbUNFo`RDd4Hk?(M7Nn!gOIcP$oJ$jon||rbpggPVwim_ z(^;Oo2j9%}u^vp}Ys@6`L<1{trT`~k7HaI74a|dT^>O>f`79P7E3PJ7!rWK7txJsx zT_G7149{ha*NJ0qgn4j&i(RBMAiD-neM^QPBhFC( znSvfe3G_jck*DZ-8ewGF)b7A+n1%rbtp5+)%AW0Vv)|FIRcv9^is-<|XDOz!c?b;e zIlJ#KQemZ;GE-I9qD3U)_-A_Sw(w7<<%p_w5kmcQr*W3sL|2o-bfw}8CChGO4U_-s zBGz^-?w9nEIr^FhU)uQHoX*s4x~hH$qQMkEuxj-~J#(#U;AuP(dxO$$@js1|5ACAG z`7JU++o1w&SWzVbP?*nCte_g_I96dZ|{OAx$x|2COx^O1HJ< z`Kz;QpHv$7Ll)Olu?gR+dUiTK6=nBF=uDwj&6C&%f-3)z*kHl4O8BsiBs5CqusSX~ zo981H&$KdFg6fgj+qk_wCzQaS_XKvt?bS}jK11F7qfdTcS^p(66^Kr{O&tnQN8&6h zO+RRTDoi8Zm}X9P;Zm#`12At6)6)JYb^uvP`yv=hp7Vg$s++{9-X*ifVM>Jy(r$S$14VOAHVE~H?}84xF9o?dsA?m zb72UBC>O#$oqJQud)sF5@vw_Q3VSmn7gzMVu&Y+hd#@JQE?k8VV13++FJ;VYU+V!XH_0VwT>3lC;9)3z|6w56bdDt%9rE?ZBin3HZ@L53l zB%y6bvbcp;0;~Nm^2+<#G+?=R!MfwE=p$9usZrc#cB2dhNeG>XA_ZoSeH^{X_Ou3^ zgzVEKw>3l?AMSM0)Ca{zh>;SBUTkh`6um>8&X||yt0tidnF75#KHlfn_E;*=VqW5g zsj135CQ>Vikg`B9f_<5gvo+e;ha{f}zkm+-m9!tA0hFd1SwN{;mc-BSSZP@p#UTfHmcR$#*h|Bgb|59I!Xqpo(io%B zoJIyP*W`1Nk1V%q+#7^vlY<3IbYHTa-AHe{k_7W|egjKJu9B$%aK@$iP=OV^cYas? zZCA)egU7nc(&Nz@dma0g)*#J4G8g`BD|xc4s*t3CH*yd|Lg0KU_%2ir$K(39Y52F} zj=!0l;-%q!qeUJhFx<%s4Tbn^=O?fIYeFRfh3(xdR)}Y-Ozg*j`ii@Xcz)SUNM}-xkVkGI9{9P`0)`P&)?n1q2ilaMIN(-*OsK!^L&M zoD%y^w&8kil`J27@cWm|TJL8wRdfH#yYfzPAOg2aMT_gR{qa&0VM=^FRmkWa5aER{ zjxvZ4#CT|Sq;dGe{!nt9-N1AGE8wA3@Zav3yzy*?KXK3!<64zq^r0dv znA@c8$>DjKvP_Hq(dYU{aSB9nZhT=%>_4Teu=rwk;yz976fV%qBkX0buJQseM&KBo z?pS)15EGHM*_QoV>T*(e;4 z#W+;uXqP%n3a)6UPw#@EAHPiNT-AUPHotTOBE3U7ny*SliPv~Yc#uJ0d_`~FjpOsc z^|O(QzH*&0-rW)qj8fb^h@}d4T)z_kK*0hE51&~<8pbgnDU3{fOukMr zGc|^_)7RzJFM51LpChsB8Pc*WLL|b#zagze1D!vLglnb2Ws-(|I+lYQPnJ9U+^|a&nXc4Z7+qFzEOhf zSr~@?l8=t+KB7kjX6*6p_uM!`o)-W!E74cvzQT!*Uh(pgMNddsOmK$D&20zvJL-FE zk5YwwX=V+q*!mIsdslpSVd)bO2_n~m;BqumPiOVgT` zz**rq;y87wFY~H}bnYnusQcRhhW)4P$@z+zt+M3bbMu@atggEN9<=$WpUVT)a_&Hk zXwL@tZcqxYWj?QxjK03U3`NcZmJ#bV_fxVji2-%}^$XKVQ#EQ&?=XQGI(V*B$7)+^ z`Kup&z_zuoT;|Aa!vR#b-KYsfieYaK(Z3psOU%N}{qm0Z9xqTHePkip%{qz^GcHf# zXRxo?aL!Z|95^{ZFA(y+^x9*4*fI66eA^fWx-NK;<73`v^rGj8vmlP?z#lI zq@hpk_GT-~-nHxBGn{$C5HRmV-^I`W0b&G%9UX+_GCBK9yYR3m748rFfL$L*-d#ii zZGO{uUlk^QM1vawwRi@jg#z5^u73anC)l}YQae7!K0P>|b364Fy&6O&{MWW?vR{F* zK_R#6xmdMtyT%9Tt6s}6EWoJ?UT9>BCE5U+gAl#**_&m&rH}d$-7&OHp2R1Skl*(j zIDBFUpM7oX&mgfNMzA9%%pd6hsk1uqt;lj}-t(@{2kheFD@t?=7IWR40+o$iNMdXT zuaYD9WQFzQxQL*mL=DY0Jf(Qw#yH}DQ6??}LnN^w*bVvZ? z&i4w^P+4dU4-&0?^IY@A+dNtouUTB_iBoyeLFxo&;ZoJxrtv(K`{oB{SnHg~jU}aR zFzf>y^Z`D2o%cg=j2>vT-`HWkyQTGOKtqqu$arX=EMG~NGipLu;Hr@5`I1uLU6qUj zh~T`Pe(IQ8bw-|uO4(f@nIA76&Sj$q8Ct^LIJ7(f@FOH|e@vgHQ_ zv(DMm9JOEN&$oV)262JHKl;IW?Id{5Ys4ExDgu%)4TKoO^Qcy7c?s{0F2L3eS-XT;ec(qZ1?!c2uf0J!kxa7 zIvy0bcLQ`N@0V=}96D=&SfOvK#mgqUUp$>?vOveUaO^qgrvL#OUc&sjbcwzv&BRT@ zAN-LQBZHsz2U7(U-;>d+$HJN3ynJ(@VhM^=g;*RO4e%86@}_u_HWa;ZrO~XE2>3?;ty(u^^;G81`^-8kg|Z4{OhHU z2_}1czX;!vGJOEZ;z4OE2vL*n>6T`djLflm z)}cA{qP1Y8p-P}H{MU+YJzulQGx6*x+ng)f;KzVE+z&MRJ?=b;%10OR`;yNQ)cn?w zE?=SKZfEGj-RNi%`{m}JC#V(k2SQE-o*T2zlcQ%iv%p|}r6nLWIJUR9LyASSUVg{f zsry1E{3PD%+FK?6nLd!$UwE%q-fX<4-x85Au)E)I5A*x?73LbXzJkTn15oFK6qYXw z7YEpN0&sLD*UP@F9*Amblch#4uP3!8^)6U!i7uHX0=wKclb zhzynelMuuE8@wd4u6z4m^Ije|`PnEv)i0`JCCgWyGWJ z@}zekz*?sYbrh@r$Oo@DX)4BIGdzdKvV|?oXFZ2t&y4F`xPgH?KH1D)87mnXa&umS z*D3U7D205)uC&AL?ym+!_BrHlG7SV@TkG0qFm)T1V@gH6h;?63mvwPA9*I@7}gFitfcAU&cFevvI6QOSWvTB&@q??|3#{zM-Q2nKYe zq;r3s&v|zi!{Vz<9m)%J)|Gh93cHYCAcw)8vxvAXiU>9uH@@8lC*EiBRsyj>vix^A zwnuZrh{SO;aPPq%E?eddE}|pQRgTrV?ei9`AntcoRP=Ck<*xHLeOP3Ubn{<^Q(TJ!TU-XqYHa4^ zPlwY0xpcXf`)w{Mg7QL!$Ha-x-oY!$w+`qSdKu+fPR3PljYbEbWG{&WHC3&&biQgx zG8`+8DzN5A;7P0Vu?CY0%fdctNL|V=`)xkj7X`dkR|srU!7L|8{|n#)JHAy&TF4fO zAhG4}xQx-9(rEK0Lx(rI<7WVw8K+66#42!gl*8dO^-KU8(v8Vzsjal?**PMm^Qg@I z*Ap@gz~M*xa((pc>YD)@WFpjeQ$dCda*8ilC}tjgl}RCLSB6pYl_0x4!OVG53w+xONTw>|HKas=SM3(AG_9XtU*Rm4_Lb0-Ymg&S;r$u5iiOOG9;@u6K?O!`TV*1}b? zo-8vp5%bQ#x1ZM?uFM&D?GjU9n@;h0*d;3Q9oym-4xpXDp_Xn5c_5%Yo8J`24ih(aSA9U zpN#91LA)QbJ&^FhyH_uJhZ!Lx9`l{p@)?p&fle0GzknrsN3hy*E7eRT-qFDU^c>wk zPKrFrcwfmP!IlSa*AopI3n1SdY))7_6|!u)^Rx(aL}7$7i@%CqjW~|$Ee_ty?a|XM zXh`4GT;&N|DDo`St}Q7nNfGNDORfn+N%ICiAu!bkK^p52Abk`-QLy$*MkVoDNzKJ5{Ad zD}z}61z=5%&x*#nqS9`t{H{$yZrxz1d{%foukYaygtB$dj@EZRkuGu4YJH-yO4UPc z@hz;96r=wG;Wg&DF&OnAB>aNxDT4Aw>Nzur0wGunBYA36CEqjgT`l#G!DAVIm?#B^ z0*}INCFndVU6~`}^HQ;J!UU~%LS+dg4%8WNWLt1JgaEtuU7Ak) zGuf>v(ZG_Xdx+s*u|c_+Aq08c60>$~hq{|mApr!LGcMqIzux9|$2;fMIAGRqw`7gge;I>AWTVQ2E(;pQNL^hTT79sH0)@22@>h9|DN9ff&1Mrb`;tkX z#AW#J6kcs!_`%ScgpnsR-Tme;P(TakQKKl}(+HLu2sx+bfikPjAfSO~bGB7>V@yc- zSZjtrs}Oyd-ODVMCh0Q+@Pl4^QgJ)VoKJ<2wRnO!kWz;8b=daE%|5>Go%=p>(i>^! ze(RJj&p*nH*3VGnIc%viU7YVs4K#I6(p)TE&Gx7qC}JOReI-ZlgFiY+7_<5jfMh|h zZ|K^}7it}U@a#Pl*)6GFYeKb7XrBVWQm^IT$mckAdYKyD+z_Jl2IIRd-MppQYW#YL zSJfhj^ z`GV65%|j;23+6+={gJ54YZI5BFH{$Y2Z@b-W*SH0f$bI>+z0j5(0aITK@~LwJR+iB zB@g!NSBidsx{3Dx;lZbvnHo6TK2UP4)y|krM5W1W$aLu;o>vRxw=ORIQ~DXy_pHz% ze6snB1>_Dx>sV(KVq&U_`e@!{ypVV;BRnVFjma@)3q@UvB_GX(eFY|kBB^dMv%!B> zMjC+lLe_hhr?t~#_ew+=5zp`%3%b9#>NuO-hqtbD7jzx8y4&C`A3fy_`%ZNb+Qh6f zT|ltvmm%z$^~AijIovEmbBPR`vC#W+Q`nP3K-RxF1#vnUki%=#r6!T`kB}}0ib5@Vs z9u5Jvw|!qd(z7x)j5~C-7}TK8Yxg|hIK}Fd7V13^IuY^Dm*%#dg+4r)PZP&~^R(|O zLtww=;GYOFq?~$*T*hbKQ`hxvz4U{s1n0XxT>=50#s#W^1`&D9WVL+3D2+lhtq!S& zzP+QYYWvVvI8csggWtAZ!x$4tl(u6{DF1SVT`lcbS-dA9&+9(S0MPt8X~M zdM9(KahEUD>CisqPChMkt6s6b15Qj4XddEg#`r)!zyg3;PUH?O z*ML3R4CMz8k=6%65I8&!RedyFZi(+gqB9vupOj+;@fE0wmT`(!3m_IO)O!?z$6{@a}23F z7oW}CGwDpJdq%b{t%BXU5xC}&JjhFix8qXTZ6H!zY=!nEtgXDf>17uhiINOXeCBMD z3%5pMR_}aO6v0vW0+Lw)LE^T~^N!7MwmWOzS+Vr87x^5t){A8Ub(CI4K0cE~gbKIi z;?KuMBzVLlhZ7VkIXp?%t$}M5lE^_iNKea987+YoY*DT9mPP%$0UWfd$5JR%+W?}W(7RETl*DRg&{QtRdl|qlGH{~ zLL(_H!4J9gz`8y;(joOAZ$*oY13(;^QieF4K>KW8V*e_DoJ|7BTES8gD+^@G#;ynYe zE1OYl7C921oV^Ml7GpZ>VEj+-m=&TmEzOPER|pB9V?v(^>V@O_SgjKKmGMSDb)4cU z|GN2D2-ZDchGT<_n!HQTFV){LS&Xy-cB)-@(6c8E**KSwRfp5EIm4@z8LSXCx9@%w zh)_oD1ar)&Ccq3QdrM6{`S97GLe2-9X(U!gzM^rbsCaU0_!FwjWRz!rG>niam<? z=C2Hb?5AW&{1M(s!S~OSxszn|6}x3k>NxqwGBLK6sW;1vy>f4;d&!nAU7Zf#z;rOR z(B`~>f;N*g{ro;N2T|I{dDFt`-0VvS*lp%pMU{!3&ww(U8nfwXSGi33h=2P*#ys z=5YXS&J%v11HGnMR`mpKha% z9GZhKqr9vP-5ZH^M5OxHJXeUND>b=!-iO|wfr8uay_;ZX&qySN3U7fT+vGv0-8e?M zwN%@zvGtdu_2Ea$Y(%TX2bY`Ks_N?MZH@>|&{@&5k+R$XeeaD;yu}KYIFS6?-4(N6 z&zn-G66fr<({6q=JP8W9{I}F3Y_ZKn9wvQ9f&lK)a;h&$@R`O4Fd|D4ILo}hzj`Z) zi3a=P%w|8L2Vj2z^VJP7&Q0no`9Z>4>T|SXCkyCWZJP|E+!Ub<_nXg`18!ZgJ6x;x z2vjJdz=xiBq!z`X5aZm!th;rJF$&rX)odyEWY{4k)Q8#Ihc?!Ig5BrRDYW$YSnB~y zrld3V1;fQJy`iTS%1I=>bp9eI*+FMUG}6J%WqDm0Qjlj=LDrG_Kh4dwt;?@v@gIFN z_XH#yiYN{M{;isrUl35t0!6VDuT74D1~*iM7$D@UZ8Z!ib#ruT{k7}0>q?fICT4Md zpipFhUa55#n@Ta6TbtHrS)2Zo?nds6?2vwt_shDh4<;jJQ?Z=Bqs|T5;Oi37CPifv zY&h5cXt&vikN--7m=JB}fL~-cl3l(6 zM)3HTVMR3&uVn|6M#L?L4c~?qhjO=s7thysf8(fo=kkk%Bc6)T{2X^MEVx{gpH8I~lR zTnNl1NS41A_W;vYvEBDLJAu}q-nEVJPvmM)fJgjr-378wcl&x%g^8|fU__eng z76DQ+Y~a$k2YRna6(-aV2-iG5i|!HnVP8U^vjKEWGI0pgChwm|4Io~B)5iiPM?{4< zK+ijBZ<#5;HqjoM1I%7x$oEE(n#w28k8+x!FW(+!lk#7PYW;bi_K0?GFSlFmP=*Os z==ge|#BDc>BgS;WB&*zZ#ax%$37aiczy289i$S$syJX&L@08)!gGk++=uG`m0`XZC z*-Fb4_t!oHFE|DoYWrmYiC>M59fHd}n4g3DVE+LIrE$qWQNf1utB7$S|HW8|qe#4L zYdW6~l;mvPWMf1Gwr8${xa4%EAgK=Ctq^=VO%EH~uqZQJnSguZki|Hi-FlDUcoz~3 z3ux>qmdfE0yCqiI48Om%VFnmt-(c-9@n5( zre0jGEO#~(=C-M?;m=);ngw63&yQFi#7fG9N7+ZaZSg9Bm}aWl6(b>*%+U_U{Ba;B zMXzdlTs=cWoce-Sa-T{jo|{w^Mq`0Bvtu_CHL>_lcP&fsTe!I}s&8NU{(i?Fc;R*$ zVGBGm6tAZXFrZ8?BynhHwr@suT7wtBh08YTw-i#kOCv|Y3oD8r&WK0i-dkxF!odV* zsHaZTF~TH-GH*k%bn!QNdvCR>|IrDzwRw&MMLr@BaF(e+DZI{B&Uh?e9|IIl^(Ot_ zwn0I&b=+QP(gN%=GuRHCLxd5|UXo9sj1AP9(w^c(?5mu-LJx zWL@E1_(x}UoMdb=kNH2!dX^(uT%_OZ%Xr@Ozq zdg=SI?9QxTFGlDzE6QFRphk0X!r&PoM~8?zkBagFbL$`#Z>aMe`ItxpeQY z50s7joa8aCC5D7X3>!)N+VptX3vx#@&mlpFJNE`}?6E<>je>8g$xoYew45p}4WOO1 z>!pjGSMyFaAYP&U`L!YyZVg(7Z21Hz1Aj4m%&M`%Uby;_hnZobM)>FptLhryZP1uy z`11KPqwu0zScZEy&D-;j6O}T98CFJdl$k#z-&Rgl0JpE2Q?$BQ+r!qAOkBo<)W7;e zEo4W5i%r{A{3a~71Q>ONxEBDW$)8};Td|d=#`rZDuO#f)NXPt&JS8?W;NMS9bqKiy zjRoe_X3 zWDc*5Ci90gsDpIekMPxEoS0`C=ce%fg9H&5fCp<_;?(NLo^7_hI-)dqmD{4kRJmsx zE4IVV>P2nR#eN?>=guSSJgE2|q}6Nz*eG(<|Pbzs3Tm_}TAxgVj0*aX>7E6_T@t zDP`Em`by{Gwl#}}O5QP7y|TF6CE*QRu?nj(4LY4F61-XsBcu4Y86H8zR>*q;D^MX` zjqWyT;q$4t{`Rd07~W3Rh-$FD6GuL7{gw zyCDRZXV1MY0Tdlz`tzFuOnPSwkYl#Fu;Vpzvy}bdHN-_G_JO zVeTZ11%%*hSQ%45Ez3Z!c^YU02?Vc_v44&%8FONbjdaU_-(9H(T{4GftBMz)#x zEDRK>Ig*_vpx%|uo^!COO|v&@xm1d{m*ilxKs`qzhKhh_dh*Zu$ZWSCdv!wQ8gwP% zl^FO22QmUI?R2JLM)L#SYCuQ;sTlR5t6lPHFxP(Y5i8_(Gpa-w$Eo4>5HH{?3O}H$ zToI?>+$!WfT~)wGR5uddzO$CwSXvwI5_qLshclL5C@IDA<+m(1)N+fL+LKp1uJF{P zV~fQK%P<$MSJiJ~>a-7l8)r!C-RaUQ+Yvg+RX2vs_4Vyd*_@u-Xm`Pf{dz^ZT4QPZ zmU2y_UtbVsTK2rA6b2)dKIOQs_?)Cu@tBqTwFR!IG?kb(=@Wwf2`@S_WDwN&yFKWL z3K;sL4ux;AFyzz_e@T00|Mk6c1AY*S``WL=Yh`XEq!8q^76_>J6dN}k0iZ`9<5=G^ zRdIL;;Pjy#-kS=jSIv)pCcxAikOgIsr&hhU{cj8VBeDI!U580d=8~R9u35k>{f7Bh zdv55j5{Wrgdwd=Y3I5YNX3!vZ_y01 z@+g_t_`$aC2+wI$277re#lmdQ0UCEI$2{wrRF?kyz8OOnuVKe=7}ds^tacD#2oK?% zbtBwEz~+3dSm(TOf5tXWlFKdWYf(rMJ%J2E%l+Xkx{blCI(t@Ee+tCWDRxZt!PZx- zYo)^!r=$!8$8}HIis!KC<8OP&nj9o>JDT($MW6>9T}aq`5CCsq_7iCo9nHW#suG5g zG?ySn?z}<2qPFk!t_5&=alKfS%pd$sRocZ=k^Xaq+;W6Exxfk#7rV^`0a{fq5e|O=!rIwyx zfH!0r8{4z-;cT)^twc)PrU?g|^ za_Gp5mbuaTp}>%W$^AbZorfcpZyU#-GweEcI;h5elkM#9+Azl2^k^D3YF|l zGAkL`W$%?ev)6fV?;n73j^{kjb$`d_Y8{AbRkVhcgQl7~$eMWWFlso|NDdCv%E>Vj zFJYMKPFN>38dhb`mMLyJ4lNk#8TR)!{ntBY`q=V5`{y7JdE^hr+a4)?(_&s6k5Ac! z7Wzf>QQ4X|5jUA->ou3Q1`T@^P!={P@a51|+3KH|QI^kNxc1Mmum6DoB@_1w)BF)L zr;EioBK;e>Q+BJ^r)w!tJxb_84wMHK@z1-Khg3V{3G&I~{;@2Hfb;#&|T^?g58-dNh|o{c22+}t_PXiH)+G;NAzMYT2oWvedit335c%*q?g;= zZ~w_8j5bq8hlduR)*@}OGv0x#)nR2DXm>+(uopt7FaNs=yr(%mLT~HCzMt9e1om1Led+V^l-95ERc~6D zZ)l}UPjR16YFwHH9;Iq!Nj~;3(b;-qFq@G2HefCXr&ZM;QrpoiU_W9y@MLCE&#Q5s zta6t<0S$AUUicnW+%Rx9+c6oq8gOs#E~~j>3a)mqCVz9mYhD%vXx2RveDy(#=GW1c zNq-JWtBMo>hDuL829kc0j#StzD6t(XbC7F#@!Z#MrT*qZR#RIE?tLvk_hgLS=i>(nq`{z0R<&e26%pkK`2|lzj0w z>G{rjZsNF^6GF-cshtMJj;!OrZ=F7kBv(nWn$f+u1+4oFx2{RcpNTRpKCZ#jp{&>z zzwBkfDP(B6KHt=U7EKAG9oTr-Y1AWi>V*Yg$#tJ(PNRQUBflR-64I$mXQ(j=|@4Gy5>BNw7IAP?-e!3(| z90Fs&nC`|@#SWAX*b_&s&w10T=bdj&p%Mu3;qs)chpFII@QUXS1TV#l_0-t^);Ro4 zG15MmEo4_861RkvK%iB{vc~NGgUpDw&5(5x>&3L_>;TtgCh4uERgd4VHVtdD|MZpI z{d(~H@ege_6nwr2t^(HP)#wzP1D1ZUj z*Eav1%mufmnW)2gWKX|yU|R>r!3ocVTWr&iaDm!)h(eW zF?z=4(Lh8|&(-*NHr{L4@V)!!Uu%%a5;|2&>dh@Q5>=oD>z)mj7D={Ce=IVOL~qx> zmwv^A=7>hjgU_ROdYAr?;(oPW4pBed%DEKxNR+U* zM9@>LJaC_o!p0|`=t{TMhQ!FG!kuv!gGx&W*@RJ! zD<;eO23U|J#NiIQLdhmeI~hH6;MDU(jL`2|M5wAD`N`b+-!_yS%}Q`ec+5+@%l;T7 z!?3_+NRH%jmGOz}r%2Z6DDluF)5dRF!Uuuoh0}cN_^K-%zRVL$jdF^`YO{*r7bnbc zlva=-`A%IGuRaX-69m_SwuJuWMbQnZy)N0XPCMLYMZY7f{=B!#mFH@RWEr=7g8zbg z{_&>=jE`}{GDFhavwd#Pr^V2QomJw(o?}HGDlWL?>Uhdw=;b zZw|=s&3%SBEq~{k0cFIS5_Ae`@!t|{XJEJ~^J!y4)Rvkln@^A$!-g9Q((x)at}iQI zEzKT+3UqPR;G~7v=w#h7hod9!GuT~pshOBxn}Nu{cNWPIW`1N)1Ld4d1$;8Dy8M5&ualhD|myO~Jr8Wz=KUaX8hPm%T0H4o9a`T5Mk+u&P z-)OGq$J@mm&`Dx-BibL=!S65!19G(MGRRf=>Hj$3Lpc2=n$Wa1Tv+HeWFL^A2KC5V z3F`e`Ni@MwhHh(6hSC~w8>ju){g=a;{($og3VZ^E$7fq=rvhXL;>jkfu!82pDLlT$E z`mLk|KlTpm-F#X}&%&}Vky6XRF8!sg7Ck>JMSB0!PSKw}bhcb1Zbh2?_Rf7Keotfp zc3ZH0_Y#D=T8N@*JC^TkPuR9p@?;7KkOtX~6(E|YfJU^5Hx&Z^)3Z3Rrp_|(&D6JIVWD?S{Zg- z@z{(OoMjWRB3>=iz}wV;R`f^}(mRs(_D;{`h#^BwX>zeqUk<%)2I0=-Z2z4hr@mJ+ z69}nsU4H-QdgmzvZ>CHTlUuo#4N;Dz`izRfEk7mUxB#ljNrp3)X4zJ2JNeri`^1Y~ zbcy7M=^Mcbs}Z7spTA)q&IQ+H3-g&v48W?64f_yGjz-^N8Bt!Hn++F_OFZKImK<*w zJkQ^J_~;Ax)-)6N44cH+H%k22|B^`d?IBaMDZQJ*m24#+nT$n-LIgFb^RpvoUo{T6 zNN<=aY(TnxFx+Hx`k!$s*_^D5uzy;6rX_$e2dLb2P5FG<|$bK&zG zkk$3Vz%^`3qBGk&&OXBbBD)mZbA4F3!yK2qy_fo;4kkb@4zNYLK6JY=d!(pn5nWR} zkyCb^r7PvHx2He&`IC47!jDVKna!(El_UsRjpj?;#R0b^94sW;O?dP?wWuKj-e$wo ziyel$XOW<_-l`H$$20B-`ZquR+f`^!aQ~(wB!1zVSUR~cz$VQ>YH60|Aca{sBehYUd|E;$;Z#0Fd(7#6y2I!Y1mDwyr^gFel1CeVnCu%fwOs9M&<=E4TI zC?$HdLfannxX#?Kx&H#UKd`H7`;QvOF8hUSegtv=pY6bkU^927##?uxBe^~-zI#e` z30qB`1rHTNU(i>ls*X4)in&TUC1)#aK+x4EB%7}YS|H>&9$RBv z97MmQc}u4vF;v}oItx-!%a8Zoct3W#ixo8esVlEi4M&cj_(9@-hHUvOuqbKLdkh5z zoXosRZIe5Z8qoX<^UekC7)2_ciRRspA?)0;q0);TE2yDjC|7w|^hu#3dDEeO85S38 zPmyffNA2Y=LJ}lep}Y%uFK#(?YnKKNNSNJg5DiYjbunaz?%bk`E zJohv`M_RIF!Wz8YwB4Pvlp@;YB$1HS)6jHeq0^69yE9Ndd6Z4g&lGeoD29+Uy@$dx zQ=Wj=>ZbB)I)|TgJq&NI*3j9(`L%T~`o*B9%~sdTn{?q!wpj3Wz?h}yO}YH0q;~Na zc=Bbh2IQ%AlQ`PWccP4BaK}WhK#JTNOt%LhIGobB+dipDF$4>3rT@+6zgbuMNVTfB z=cFkQ{s1JiQntu^`Ov12jJjCw>%_zev-eB|5AHLK);{xR>AC1voHWZNQJt@^tM1BX zUpF$OT9B{9fNGXQn!~Q{s^|~YPYJh~?*yCi^0;MRcO)sTRK9YWw?6RIKk|8?Th_VC zJs@(m8;s$-ky1T5-R;MI-%DrXD579%$5VP+m;|LLz(QFeTpc%Vb~bkSj{sM0&9uy@ zgWLEdkmr*h}xO;Sl>^p`e0c95FE)WaV~?o z_W(QCE{NvT5=T7Do$Bkzvk#L><kP5BatL z1+USt|2ms||AiJAiFc9!b>MLp;ewi(6BoYELpOu@yDt>A_U8)R@xh<2$42M3XFynu z>!c}ezxPqm(_!(O7r@gjByy^F4D}v_N$dZ4AMaIBaqCqL`80M7h^F=|CCHUUnR6)F zr6EzLFMx))QCm9_VF$G{gW|hKxCZ%0IR)A=)0TiQ1~OY-)_?i$uVY77e{+_iqJ-`t z*=UgG*O}!K}G|9BKd+`&_UXmN{ zxz7B2^(H@F))VikveR^2-g@2mfKKUOMqXZMkA}d90Dl<)n4rTS?rmzE#)Pdoc{g$Q zMWrmZ`E*w&#BP%Dr>>e?`q1#6%D->m^7cghq5rm?uz(+$3ami823Ial28kJ)I^CUOu$B;$>E{!;G}8@mRD2| zu{=4t3y0ng5(p*|qF+^e=Q`gROi(NKb2O3mKYif-{7M^c4aP*1lbYZ#_zg-TzjE@M zpk)ysj_lrpk~GLK&n51%n|!*_7p64&X@f49B(M@#5(lLJ#TlHXUvKJ9>Sk;{<=$3I>QNk_9Z3FCT__eJJL>fmn)A4>=&vCSE|YH~E2;*L^eFi@@sq zo?(b7Ii^(yWKJ0PS~y#Tp&e!P=VW`M<^Z#Z2k7^buthfrHm`k4hKxqQwx8i_to536 zsWV{=h1PZ@b+qUupfy&;tJ@k=iu4;+zpcFD7z*W%=q9ycOUGeHO6D`)g{@70slG{@ z8u{XOBz9fXlTz0pat}HKk-xdtXa|Q;yK{X(zHD_oPGPa#nF6}^GoV9u=AsqDJ9e?%lrSh*b+BF!sbWYIE6&FRt{Ka!(O1eSQ~ zCpb$13T(yhoQHw142>yNm)aq=HaH%?_t zteUGa$1`RY$c4O$t&^KY9zH)uGH(5|E!o1umhpw?)u^#+sv@z(k4;t5(z`V6a<^=o z+~C)j?|lxV!jQr)(}p`BSaDN&8*7VFdoX`RBAlGHxJt{W25HQ4DBq=dxqnD9ImF@agdf_09 zjUFTb)4n>>Wk_|uq-I&`wIAgNWhsD@VBKSWF>cYw@`Jitm?KMhF3U2H*>(-q z3*%1bfBXZ^t?*2PW@{8MjscJ>F%$y|0W9C6NXA$U$>ckOngJ#0mwPAUf2{JmKPVpR z!ygUp;wVMmlx9slx;Z&jE~A%y?n5o&S-?WqP&+)Hf#*lM^YGPO$-i5hnkGjBsj&%Eg$z@u*AEpHOcBS=tS>{e1< zzA4rLuctX&G;F|O(H7JG7y>5(k6Bu2eR8Qism?x?IGXqT;#VEXlhZ*7I*UseiXv@6 z!3&LYSuU|RSS3zL$`65Vif{okw=q-iZlM9`d_8)Q50zm{pmiL zLOqaFbj3*X<``r1ZH_Ot0Sdu^f~B`QwD2IHe3W+%d+^4FX9_&j6_G9C-VQS+Pt3|H zRgw4E-3ydb<&@w_PakmN==TM`@fcErC*NM1a)J}s_RE}@@Oc~OzcVEAdjNu4H>?f5 zFiUH}>7mQ3fRL)dssHMif^JHA$5)ldVx!gennpjCC;(#-=PH^gS;)5k@H*@DG-=E} z10bWV6kSqdKVKO3T_M^_YY;KSY`mV zu(mrwwXMN@>4qDYXa*?iV0h5`+axS_3GQ?bqtb5kD2bI6y{7!2N)s5KK2aON?ml1@ z!yjT>M$66k|3A&=$eO0zeZmGh5NUwBc20D9o$V#|1HH`Z zchd9~zwO#8fd~O^ROgt8I^@QpPCKl-Mb8TU_20nsy{88!C%8_e;T9Z(vHpn*&*E`<23Qej71Iiy!Gn%-OWRJiVJ`$Kh@y@ zFYTih%kA5$$AIth<;#~@KF?$8Zpsq-ETv!CZTn+E<`v{)e2`d1$*nrz^z|snlhUE9 zh;^~ghlwOJTkAs@#>h$T%d#c1t1U&8Fv|EDHa}Y@3QAx+JZ)CR!T@dPNiw>eESme&(Ut5X>FwLk{3ojc?*+Ws zVchLRvC?9ZQ^~KZJu|mptu{$nx8d?lTAZ4n@0Af3K z;4-07;a)zh^Gl?vDhXQ56AVwqdl9u-nFZfU7w1ka@Aj{>f>HP2kKV&>(2i#VLekOo z6}UgM<^&wXO#AOP50{&@7UD>m!KwKS`9lDyvy0Jtn)N2Aj4cvd-g2&iatKl`#jr{?@iIvMc9RexeAaeyliH%Bi0%KEN@6+oXBpPQ@(bUJ^pRU zbAOU)13z^rIU2^z&VScUggbOT3bN&eu760y;HID~gu|$B9fB1Gt4jm>-$d{@+ollr zlW-}6M!R3%{&i!eQbKbYNMg_Q;dv&wJQVWMvjSM#sL^hjqrwE2c5z!0TNhtY;Oz|7=P*SWo%QT1rKf5&wzH1t3kw2zs&K3>4HaMW}Z*`S~*f63VzhDv*dwE$hL_qF= zs7AfvVAy=ZMaRpgfbxNx$$v9qp_;jSwtjaNblv&q3=9gw_{zS6L`>OeQ@=*cX*z9t zC-YJU=)XQys8_p8vIt&oQzsNy2&GPAY+oQPg-zSC7=UVU@X;ifo^snuwCPOn-^OKH zcw*1#L%0qT5oBmvRRcLiAObb$FSp-s^AFzsDsra`)>GZp8?YT2SZ!f^M;z4nN8Ygv zhf1Z-u^1~=pgkC0-=axUqKCUgC$X^}KL@kqrvrd@^9c@`ds=sN8#O`CC38E|Ep$UA z**!l;?zU#|BsB*+xKoZ%8zBI8%HWadZK<=RQ7nOnyI*ce!d^pLV-SiaW78gX>q{Yo zcITQ33C}(PDPtZGB;d;;MKDa(iP3Y@{|rvZ-T@;>URmIvM!3-U!~m9{piM8$_LuKO zgD3ux1oPlzeizh>Ka+>51(2*+Ecv5z`^@%EuzIGtoQ6EuG{)KtO*?@6f8$GN71b4R zLUn9U&Si|JN0kX!{xgQ+FeW%#mWWJa!}&&dq875v#<8nM=c&?hm#-CGq1Fq<rmzryambqUjt-t$1{`<}ctaOH`=xj;^#TQQ1A!($6)%Dwi}hqqszGY1`r zcO}hmP03UAdVQlQptr6ng`6iH+_Fc))}rdhmGsZU{9zssRm zkL-X0b)u$)OWBGLR}diNdGvgT7uU1m>-IJmuM;KK4+nsPQ9moO*uY(_Y!HUQVQLmc zXk_j(P3~8Gw9McY1i+GZ2+ca`|_%!QCSBR}CQIHOj7*yYWn znFFpf$Sa@_Jwe=SJ)JVMJ(oWcCL7rBkvR9fvnI?~>Y2X`Aj%fK%_>bMy@n0tOl`ii zLqmdzic2sRl%K00<~D3^2wJA3R1-i#B|@}9CMi)#?z#0^eCS{VOG+7Q(RYTg#vaz6 zaLkF94Y;Yf6~8t4RQ$V2r$UV~y~0O&66LEV7*G_LSLu~(bvP_4O{&r01>%^%SuYXY znrT~ec+zGr7nlwhe@E)zJ0uI9CKMqrEv=K?xGlxG*p5iO09zSGI;Ckzn8ezttF|^s_^YG)J4wzxHqEqfz7mr7h5$SSUqr0Lo!nUpRAY9n|PqVYqvQi+&2@ zF(N+~ifB6Mw>$)>r)DX6Pn;gNzR?#2IYnYOM9RGWta(ni2|P#CIRg+Lv)fx!P?5dM zMa5+Y4lwx6U2ldh?swlk@id>^ZTIlc%6h)|zHlw6bZHZNm?-7gYyV0?(G6r0iUxu! z9<#-9WP1*)uW-pFBQ3uy49BT4e0|;c85*Z-dtO(P2EYW#Dv#9R^Tx0Wv50s1_=^+` z?wm_T=JbW6CQQMX?#@^oj@ab5GJc1Nfc#kW^oKV8)KlX)ujb=PM~^;{vBHkx=Vk#c z_j6vWnFl&opa7bWATFLR4@{Q(ilW7KUM+uzNZL{Ll zljL)~-x7(4DTOex8f6WO_!7VXGM&49Jo)SoxutLL#njyYDJ3L6pGvEQP9^ywo&OU4 zW_}lfGxas9kdTil5hf+Ow(BU_${bB&u@JjUJz>usDLf89S@xgyvGFS`$S=qRCf{z+ zF+@Y}J3Y+StmnPiN)IABuiw3SZ^%&9nG)IWI{CQjx1jzl!^ys9%nIW!0H~-}U1GJR zH<{x*QDrT8`~no5D{MDcWh;#-?!TgrGyH{Zz6A9*I2+}X2X71sF zMJ`%_3TG`zSNT=dAenk4q7*OgB=3`DMsXtRcgd}6`DYh&g+^yvUkfzcuWApD%EIk( zxWV&`kdflFh&3dz+#)hCd>d^$0FbNtwiIjA_bGZqSwq zp|Ix5r)R2)S^jgdu}WX{NnS}_GDF5!I<<64Ig zOYvO|d(C0JXt%a9YN6{F(*o(U%~!7ScHp0X?}mWK+Z@jEeu1P6Z`M*=RKYT-*IDes z358sXw3{s2X$<^&SYo>dzUiBkS%24G7BsA-0*_AY^EC9Uk*e!_UL1$nH@%)?=^(AD z2s-rvwbsq{cr99)pnBS>5#sS&!spCQ0bzx$C6a%(2A|BT`r{aMxCr^y%Xec7_;60U zeJ5%>yq4D$cizq39l;baoR~h{+N8lZnWaEtg{W)(Y`6crGFqkNr>rBaF zplKl&BwBPl*ir@hf*Xy@{X>U(PfPN)J}?kfTPRa7kBnxzHM4j8=FSjKq~9fYnlIX! z-{Ew{g)@WD%+02i<|nScezW_YyAv<4WEWRe<>*U=u`$CJ*MrXqnPsOP3xCY>KP5qf z8reyL7sf#g?cuI1fro<6?~+EG2e-c>n&eebX3=?z%UKAl{e$<15)~D5OkARaScAPC z!N*e|5`1)}dPZLg^WV;+L}tB43|;!2O2}tcW}jAeGU@5Eh6S&O)A1(B7v6rD-5a}z zz*Xggx$z!zfyc4e%N_xe=^TIWuDYg7v?R3e1EO(6%oEt$PpW>5PdeAnZ~;CKD<1a; zM)Z7CFEOZHygn`a8*jyOd_)}WIbUx^@*2r)&Hk@*pm*NvnC%0Y-qQku?*y89>L7EY zjlm<|a}o5E#QCaN5#$F z(b#=qY|&5*Z!sIVr~2)$B=_}9nXC5bzUaQw8A2D|<{8J^GBOo&ce6@33j>1i4KY;A zMqpZUx0VtjMOzsYFvMBXj$*U_j_8@{bC|Z`fZ?9IwSGT0HsN>j4m%^sgaUUZ>ai7C z^5id8ahuZDTi5bp3-mrcD7uTI8}>P_T4U)#Lg6Zdr4tNuZ@_=l=qBXecP_C(-Ec@E zbUXd3?J9{<_|9VlDdCOK4Bti!8=#6bzZ&2+u&lAcaHqc`javaSq&XF0y2g3+yx+t_%#1J$@ZT; zY8u!K4~&M21;2eBvN-e)>XhW1l>3`*nVBE*jQtfaG3__0!#Q?|UVjDg;*Bew=Sq2) zzjUuH*1#P{Vm0%*77)B=Y-42~j+9MgO4p`fx{*LH<~j_N^loeTFZhK$G-XvZlor z20Hp`n=+A(^5&Ejt+=lhwcp{|f(>(`1$vA7J9D;(Vnac!&PG7r_~u6( z4dek8K}i^F`YXaG5_1gGANBHV^mSG~#uGyXx`i3Hq9bLj#2$UK&G!8`-1gWL_cTM| z^K+(x{?&n;U2OjM+X)PJ@A$_F3HG+c4OUreQmr^_u;P}&e$eOS@zmxV9pBBnCy_aM-03xZE0BH< zx%1OMKjO@q|D;TIqf3Xiu!LhCKm&+zu=1o<=0r>F=}}<5B{y--KY+v0;bO^sD5p`h z#E|S>#Iu>AaY=Ch+vi>C=6g!#>yxDcMU$qx7tyxJih&g>~D(-UUaw*RX#0?vx>20TxPFbL00XX-9HSu|vYp zxxJ4MvNjsw<~gxaWOerEDS@7)`p1P@U*+^xzheR$PyS)g$(i+=*w_a%EZ&??bf*a> zCRgX5Msz{`>fRi)Z;u`oez=3fVtTuc%Ky1bOe&6V)hw(_DKU?23*0JQi2W=ze5>|{ z4*ve`@PG^MZkDg<&dm)7Cnt}G<55h@pz2j?ZvEC-h{^l2Q~cHC@+_n`Lmj8>8|A^6vC3F#M$`|ysI1t>x%dr3Y>^~EC<>WhagA>EEcxVb}J!aP1OqOi(iQU=| z|7NC3?3BybKJ`n{!-E`5K%yQ%maB}>X{^_9T`*+QNE!;_Z6PHf3nNJMoS$#%)}@$F zKI>6o5lv}y8_1h>Zh_jz$NL{p7B#;a_dwqCtGexmKNX$FJLZHC1WpDxz)i$l341Ov ziyU}!?UJL(8Eg3%V08`iiog$ftims#%0~8S38@FEw2BdVW>KwedL-v)0tZanw3Q z4n4-aL6D~KIM_(RxXPZ`h^9DKT80q(K98U7O*{?I=F!^~l^n|$vv>0NGNhdh1%YN3y1rY=-YhNd6hFlno^R%hVd;T*|7at_C>&_D5_)5tgk zpfsTyL>0Y$*j`*F^uA^E!nQACCE7KmUb^$c{W9x~qZFtKp_7GIblvprHU4MJ#Xsq} zoh7}kp#GM7WGoU8?DmH?-}rZyEA6q#R4p4e&^+nsJ-D=S6%-%1lnF{4?1cA;YYPer zbd|LgzPni^9d;yLb;?~Ya7l^Ug!1C2$~ttyx(Xur&!u|`dP~Ovx-2xRF@fjFa`+5B zfj&GDkq8sDb*ol@A11ebq$@I*X5nIWlNWtoIog^(xgYlfv^~n%wTFaeX#74zoV#8o z*#!Z#P2b3WX3i}j8B_ps?49AW5)b_I;C#+o+uD!+Vt+UrGPK5KS%Ak?gzxk%LR~O{ zb}wGXOXu5dg^vm94>*1+?|){0sEy5lE<~Wd#w9(Udj)_l)1iKm=R^D0)v5ROy*tm(3mM@oUY8pwie0ZFcM_CiPO|NzppN`tEMgBR zGz#9)JGh8-r@NqsTHK6w;U$cObq}b3*OBjFOOa8U*lj1>qBb9D*HCXQzDItUwf|e7 zSn%>e6-9V}VgAj!z32KlzJF=$jna2tNcyfn4dwKFC{4mrm>_q5{{Uu-xu;SHwjVy&?y(Xd>3MB=xw%It zl1k+g%?VQq3s9d2I4pISCZ{S7WcZ}$<<_hW49vfX1boGp%#j61ds!59X!r zPY8MjX$Z%<0ivLI+2ctT-bzL-UeooiX*$GR9F~0Lsc(L9dShjPI8M_&c#COEzkHRM z?u+GCGh!YSssH5TD<#XA8YpS>@qeb)OhKm>LZGg;OiJh6py0ek)FO@Lrc~a1Yy{A# zCmdsWf_lcb5_mR`B8Gu^+-hZPzV_4?oX^q`#NsS=a>v_2-(Rju-FtHy>;Va8U=1Q8 z8vE#bHwkWkSb?2JJP-~Bb$N+UP07%g7}J4lnb!xc*S2o(26>KfJzr6@5%0Slpe^s( z6uORAu)g&ek)ZHc2dTmnAn45Y?U|=1LO?Hd;X70+d!f%l>+ zcjT4Y4r`VB`@s6gJ|J!_`<3aEn}EHL3^3MW+S{W;r~6BN;3rlY`fup|y1^{tn|b7` zvVN*`=`y4St#iEj?SLwGvsTl0J_N_)lRj?yB#r~Zp!dGxwvL#%i9!eW?Qy4KE$?`a zJ?I~Rfi(W9V}K}>$c;dgmltO+D1sgPO=y)?&62|EqplxcmSJZp27kZ>Goy^T&Q9S{ z&bSgvqGj!p09q<<6V9FBM@;GB?s>pfi`TmrCJPk~z5pwI8sqE=r5ov2U2;yAds$y~ zj^q_~t?@jL2}?niN&(_Pa~AYgj%bZ9P=A zx8aMpxC+gFC zQ@!8?#bN))d>v~FMK9UxHuk_G9A?Hi*8;Jy=YITFRqcvaL zby^ij%nHBg^S*+Tzghl_yMME+vMTQnGa1eP7`Lz@`=5vT%+1Vi?wLB=96tXoL@{T8 z@Lu}OzSlilT2CRe-teCM|8QiFFjJoqPTRpR*T+j94|4n`PmG#}$G#O6%@PLF%UO5y zSl=(xf&Otkff4iCd>k~6wv2ICe+89XDNz+!0)VD<#fT+Qt^o)t?-zX=f$;O0o>h6S zXi-@!|Cm$OtnNMA*6W9Ri@z~ncxum_wxnV|rQfz87TtbV=P5_A3|UR!aeF^#3oeaP z&sbmi-v2`6)1U|X^It;eM(pSZg|-9}_p!N#^L~6;+0VgTc3LAz-8V*x#|(x)^x*$- zjY?3pxBtw3OhhBlMs`tnbde^6ceZ@*d_aFk>~(KR!sT3b9Ol>0rfpB7jJ#jDALS&V zFCOr^Y9_APzh~@o0{o0G(u~pKFe`YKh^yoz-e9fcf45!2;BTYRT$>v%*CvWo%QskJ zsVr=xL>j}JWon$C(z7FFf>u zM@*yrvE|F03NV&Iq_3~aV&3)B36U5*JJzd?q;s*L4VHjvGw^adto{@!|2=$<5%u<5 zx4$>FP5QgcKDN{ck*acvH|n59g_bp9ieq?;oqNmxyC!;)h{N&m21$Gq_`b)Ekf2E7 z8lYC|wwxmx@36i|`D1@-D?D^T(F_l*P(Tm3n)S|f~VejmIME7ocv1TdoI`v6#W-tt3b){c;%78&;H$Yqv@LY&relD{rOL2KTRZRxjm*z zc&D6fN_j9>iy`1XF!ivG?jTi4k_T=~r*%0&j1E9@4(+%e((D8P*r12_)*{=XA zb+|H9*D+jtJYSWBdS9T2_U)?O=Ug@f$0rt{FcR)C_v@I$C=%wKG8po~RQb8is$6Yg>R}cCuLlBYwa}QN%J`2Q;C7?ch5)Pq%Uf3YSYsn)z^|# zUadgO5f31O%D1VRPP%{o1j@N@3%>d2XWuUVOM_4AK|Y%?s>tDaNv~&pfBXHZ-6Wj~ zoci^FDSQPkUbKcf7TJib!$kB1|OKUL_S4 z{glfV#XT~gAKV^hpHQ>YJ{{G4v&L6UJhCxJ`Bg?-(2UuC4Cuu3;F>5*7U%0edFL0N zpMzB@25QK+0HPs{ZIe{El9Smg5U+yN_H|bR1G?GqTfjg$m8{56-h2gOnsv0Gm{&Ja+8S4m09 z7D(p_mCg`EGI~0xl?jsi(&_Kyl?FV;8F+^;1Ww%Rx*#7xGi4RClZfuvb2&!_XN>1j z@WVC5ub5wy)qNX%?|R!PpHs;3dtm3}_K=*e)LNZ_k2O$bekz_jt%}SjVeAL>o}DI( zFw6>tLdF;|eiLy@RtSNE30St2Zo{k*A9L=1NaT(YOn4Jh zW>;O(Td@-RJaOmrQNnL}$6a9%Z}=tgfF`iz+v3S8>9w+T&J&fnWxk7?t^XzugV~N* z`{evhW7rQ)k~=r+|A;i#KogT8qr zCBK?S!H>Uh5QFoYSba_d8=*wSeap4KvJj*M=>*U}hPp2d=<{vTJ)t6`!qG z>ZRv8jL|x@EPqyH)_C6fpC+wtRa-PUFHY|M^j*9IO)#9APMPD9KnN5@BCucKyF2dX z!2N9LOI#xoeOz~ZJGmcwOWZn^9Z%-v$6#{*T2BfTnf3^~*!Jk0dPy~!lS=J)I9CXm zX4f7iM1`vsF(DpGW(QmT_}uv~n$ELjRbsY%!S*qp;bW_)5V`KPQ}VDEErX|JH}1!E?;u;#twfm^Dk0o=&|qI>umr@#_^l)qku8 zsiF39e|_dc)geQc&U);a-7nyCC+GLMQ`AIE+JAzNbUAx_1ar*`OEJ_hVc>*Pc>q1l zl)HE8T8Q=g8M|nWPU>qNV-or3&{4U@Ct8z)b|xDq^t@@JUB_4TRmmGY@Gh90ryoqK zf(?Q07jZyW3fmhv)OW$Z;}84{=aVN5cM{J{;kJJqdBtpmi9*!YIlb1?NIe7B*qRGn zN*QTex&pZ7X4v9+P`W+*6Hsg@Kjo~LG^Z{}h(K{UTO^clJ^wg512wWEd@9c6O zfG^g0vmVT43?ge1+*b)hIgTsAx64ea$}ztXiHN~*|1-VtV@4kMSnu7AjngHk*G8I)#+a~Jop#U3Y|lpDN{cSCaWI3*Z>j_a|< zeEOx7#<_t3N4v^%%)dU-x~qpC!yZt0!Cg0tM^xe|jGEZcaVdBs&Yv(R--3%%Pckt6DUb*JjFw9M=B}qV{fHwW&OD_w^FF*ac0lc)Fou`QMIn4y zCWW{?!>~&kIBX&z2P1d@-olJ?*y^gcZKGXYusilNi+sPdSs(drC2ao_owsmW$jP;? z`8UUF%odUX+KKI+Yq<7L(D%%X?8Nr_S+Y6Y2U^Rn&x2TP!zRAZ&MLBGN#Y(0svEo~ zg!rH79Ds=^PS1~_;r~I2stu0@!@CPldc);nI+p+NXT>vnt`8O)R+H4RtNn{%;<~kf zgMB16U}YqbRCpy1rhC@ovPD70Ai#ywnkiI&zzAeuGlU8pT81Wlei%0a;JSMA4awZ} z?MBLe_|RwG_wseF8O6=N??8;v_`uAWXGlZMP()p5spj!pPk+tmCLF5jS9qCkQMxs= zf^eH*B$}!waBmzK5M2*u$5?e5dD!{vTkXbjgVP9xM>s-&@6f{D)uqq*zpQ|Avvh}$ z8z9rN1war)IyR832C&jpw6(3bKw$)P2Ka320}y_mDoTxSm=?!#0H{CYoB%K=+>_`O z#@&7_D=Rzr1Udcn+-CzYz(~E*!iQlaOJp=Nd=DLwb8usk2S&RY<)pCI1hP4+aY@)o zYU=Frcl*YUBa70m$0P)*qt!){#M4<>%cm;t~#Pjj@>{GkQ zQvJ06XWIhtsg8(}eCWTtZydRS9OoR^$RvjmFiv~X5-nYG$hJLRmJb0t;pWRB6Mjcc z-Y|WO89JN(OAjzK@)i~r%72u)A+5-vugU>yJW2AjYV*53+iCWr9L}wd+tMc4z^ymM zcO^*UE#!U`XUV4-p!I?%p5T;Xx;2<$+U^g(1!PdJS#__ENMV`jcdCJ0ScGe0L^8zxcP=HLg?c=(U4T40xpF*uZ!uZ0 zeCyhbS_0tp))VXo_*kU$RZFDL$qK+?HFLj4*k(^_@{qcERk-~JwjhimfFYn{oUpfW z6G8ICXk1oAgF|u=Z_gMdOHT+w`|V|ZPydI+pRl$%<5Ul? zvAp_gV6;O9_ZzVSK6O5^UFH*S@d)-Zw{VU5;hQ~YLm(N0CL^F=J0mmCq~$IXy;BuA zX-C)su-SZgzRp-sRM1X96sB)GAkOc91AWik^ZvpEs5a=ZJ~7nTYW@^J0WSKSK)!AD zQNf@85m-4i-E>kuu0*2i5d8g_Cyev>X8c)%MA*bYW4WCb>I%oxCTg~$S zJw?w=I|7dL)it1uTA2)8Uy0gnH13x@n%cEnMJWe4q z8H*!px0rnQu-;nf8cJ+^`$hf&jHtr&k`+cCyT|gRvztj;8-HK|i#-{Y=Tu!-h_`8| zWeNrN$Q5#|=N@qBH31n9)lw6$b|AvoxXkyUyt^V?_#g*I9fdk?mcG|{bKaeGdEQGK zQy-}zwB1uq8ozRv4obe!ddj3$-IVNPFVkzCk9V^m-S{btG5hv#Fu1PtZ(FS1(dJA$)w1{DH zjS32ohl>rOzuLZAKd0Y;gpR=H(mq9gi+l8@QCNnl&zWw)>SJ7jHoDkvs!-SOf6Sg@ zxn{$=Db~FUh@Y626Me|FL{kAIH7?xUw6-E zLiyAzVHP*<7`8AU$D{JM%AKUTy%o#xBUcG)j*H5u|JO8!9KTo!ndWq93MX&pD z99F&q&aAP)w*L4m=oh`!0Txa0x!HfP8U0BGk{&uj?!Qi~`N4kasQFwWNvF_m3GW5`_ecbPWbvKt)4imNIJ32PVy`6S81=ZA zd>?R2NTo#}b9B%TCN+|)C#mo1|91jN0Sqv;oaEaz-eg2&hgvnU7 z<$QUh?N#Ty16Tc)4fB{|SHNb}XF8XT{Zw{|GpiB~MA*w#c9X5uJ4{|c2^tlXLYKM|i^Plr=uQI=X2CjhHgXMX- zhf$quO+EtuYbgvfDm5uYK|SO|W9})1Ne55@+`6demKj)wE&*O$*)W;m$9p?=vYQ_b z)`Ncd-!m}M^PJ4PLg72@b1|~^o$bnoNi_&e={VlL=Am)>>bHjYm*>&PX>XKpXf4Vr zgW$y);&o&YJJLoF0isu?2Oc$D|hiREi3zb}!dXA{U4!DYcQrSGxk zon&s00s|Fkr_8;>I7`&5f{ioR#~1SBi74#UpPN5XL|lm~)+x*%5{NjWo0F&77)cAW zb3w>&LNsa%CwXexmhpEGPkf>J#+0Lho)XN! zcfN@PSY?VqEn;_ryqfIkHWme(MqVQWVjPHwW?v}uJQB=;@BOp=-MLe65PH3>Q>@m( zbp8WLvIx{wa4XTE{+cg%xwF`^Hn*$@^TI-NyOYg(uV&4-!!M41e{Ms6c~#|-p9Thhuub_QKp%Y&S(^r_FBJU=U(8ovB(H~u~jd|o~$%YYz`jY-Tq<6 zX~ZpGxUH5cg=6u{Ibq{gZzQf{2UXjUkm6*ld>~?z_qovdd@kDWj2D>*LHa7@=)#0b zJbTI2_ha*^SldJ8zd@F53b4s*8f$M5u;pTc!i=-Wbs&l#P5nCH+jmv>v%sRrsPgP^ z$S%zPO751&TqPoU_fPUx(GL;V19ko~9<8x=&bPB58cCh@PjxC3Rt*WBj(cc6hrP~x zIjIM|zh zSv*M2clPCcZ4P#ijk1?@-pUE?hB72+vH1NU9d{d=W~pK-r0Lrk8YkB8N*pmvDus}g zn7Cz3$FW}mM4kpqBw<&kHZfXQH|1fyprDHw|NRCoa?0E5)cAb--r+R{E{TkC&(@FO zU}}2&6ke-O3N&ndA7}(E`)fEKCrP;?QP@nV-!6!t=e}P*O_9K7`%rN#fO=S}!ZEie zn9jX6MaRkUve#DH8`bP*nJi6?=w=K&BxhK^*;m`#w48;LFvfG@`NA+78Jr!|5Ut0p zbo1Hbn@5QPHocF(Z%PNljNZN244p@oN;r=la@ikYyjlP;m6e`t9@2&d8Lp0+WYu_I zYMQ!ZEUI6yXxYs;8_<$kv7v}^)RbcRCq2MSv0M-fogZq)ob$4%64?;e_)&FZZZ@C{ zbK&oD%Y*y}K@ma2>a*zIpgBa4X@}{U>@UGb}v~f%ZL>I!#!`Bfpg2bfyUW#iy6n+@1ZzcbSz+yy}rYVRvAk(mH`+ zLoZ0G+)a()Z!qBUk}9>K5x9Sn2fR)rCiOc2S2+cIxGM4oTBSkS_y<(X+QZa@Ch^UF zK&=yZse`Tk_6|KseSFOaK zRF0#atU5Urg#7827J6jIL*&x*{oFZcjr5-%@HNn+R<mK@1j)kPMXts6)aTK;oSo9u{$Gv-^?<(W%J3^swE&6A;|#eU8;A*=$d4JKH1 zn+43Wi1En`-!#xTd#uOYQslD931B+#-G|J~yyEq3Z9dy+qq!d^5gGrX-R9iItT*#7 z-+xrTvGy8Yo3uAPM~S&RXeUc~8kDleS~W1L8p1z%uySG;+003uzh6L8*?Jdbu%G#9 zIP2lSY|9@3yZ7dndZPQXFV8CIQe1`!G1T@eeD-5j6sA8!`Xp$jX+=8(nWjWbmYY8c zMx{KC5BATRI3yzFHzRMjt=vDnZ1=heJ>c-=Cy@V#%c@<^A;8vWw@Zv|;4J;6B^HTF zw$J1xoHPRS-w&q*k+>oI-}5IXq+o?JcBB0sTe`)ED>W2coV7O`B6FlT>M$rQAxV7t zqXvy*y2<`y+pj#xc001SZzY0_4Vp>zQ2HC5%VH{}5_2tPiyLcshZ5_kI17QD?4ZDD zMGYu-AYV`HI5M$+gVVO@A@}u+elu@=98*|Dk;}2wgPQMoG%cL%zUTiO4r>yTvQ^Q= zR~n(ui9PSc6lmlvDp}E*LH?y^irPBSGNvH<3wHw*p`-fsLEw!1t!f90Sc8i$2P%BL z!#ZEzZ4)@etuIOdZ)Z1mcu^0V5(&)|-=DAkHfHpc^fFyI8YP1n6sXlvKl@>vNP!@w zc%W34rpO`oQT+0eGu6hXa7A(Yl-%$={4TUYu0Il~yd*ioDPR_72I(}5<1}5o_@trV zBu1!a)|KT&UlEzVwG7;R>%L?{8Gspa96n* zNFS%Az88}Tw@?|0Git)~FFb#BA5c&5fnw51(N9=hhPs;}$s0}mA5qKb)s@090e8s! z{3&YziCUuAMlUyBs)j`kylyf-gZaa?QLsdZD5Ku9f$3X=!}IxSOGmq4?1*P|T0^M0 zyAQR8zf_HNQmPt2_)itk$HlW11W!Uzc%PGYGH7?d=M=D4oQ)=Ym&Qr|bb@yQP|;c? zTpAlforS1Zf?}BA&S>h@#Iq`DNmB7&=8W~xHb3jkZ_PZ-c1lx2;{pCba@un$-^V##lg`tJi{cNl)m;a%t%}&p=&IHiRU|}? zQ<`0Eo-w&LuMeEs6eQlZh5q~TIZR}%Xi%qm|94p#*?S45GX=O3b42Z()q)l#0!O@& z^R!oN`b0E#xUamhd?VF+DNC|35Scu`^k|}uA!xes+|?Y#E*g)M^D7DCVNRej$|bK{ zqddB!kKebj2c5hvIRuhf_QILWVWN5qmw(4cRpKJ-?8z&h6IEjVmX^zeW`T0;8p3|d zI?Y8;)m)XUUTsVgp57s&-hZP7;h0PrL{E-Jf&f(M9M&+5qIn*ax{SZS!NbftD7#r7 z=A+YSFuZ#Y87^ePNuB$!g2`u+2Q*iWnYXx%RE@6p?KyxE#ghz3wtmhKZIN5Z5!s2| zHEqE`eaxKM!eP8gPXF**dSSAw$A`P7PwjGLVCAr{0T zS=y*?&+_PTzcyadI<7ujX+9BLl)Hvo|8g#Vn7OXk=xpK0>fl_Z;DUa?HMFJpD%s;Q z^C8`q_M%5|=|<}PqE(8(w6(4T&HQoA{4m+9X%CfI=z6!u>JxE|u#A*8$>->zjyqR? zU|lKmn(S)Uu`dFnn;y}q(&=w+Yb>vZh00EW+dO}-!>P;*?bI%a2r zNvCH~>nHC=rNr~ZBr*uyQFh8EyrS@U!VH*+#Qh?b0%_tO+(m2@Cr2D8xR%-l|BaMx zunLx1)w%+0U^DLNi;blKj4>Zs^1Y|vx3vR?pJkwY?y}jw2Ku$(s zg)eF`alDKA+(w7*nZGZ%wGr*bh$X$_{~Y!g*beanBe{GQGu)|F)Dz;L8I=Z3B^QBS zt>QZjL~MoGT98Roc+JghS+gVezsh;MKf%y#`FLgosZ~2AEgh z;!<|Z4H$pG?2g17=?jAG(s^^+++nZuRJZJVi-3?7)1CyjV&8w%B#XH4&&A4gO@ z$?a1Jb=`mK-1UowMDS8(j1zet_~MA|>>aF@GgRt=Er{BV0Dj*(=An3Ln6p4Yf&}A8 zF~$}hB*1ZuKw!-b*SF7icJAyJsy(K$Qo`g~@5Fju zZ$;T|BV{4LZa~kPB+aQ;8p}jw`2RfZnFCBjJt+j+W&*bY)jKciZhdL@g}Cp%6&zzP zPL4Z=`OJ>0{c6&e^Tw+1cs1yKYT4H($6;Ch7o#nEz582me`#) zn_`V(D7d2s0Oy`R>4Ew6YgenxO9iRO5+wY#>PXN0Znb4+AfwQ17!SfQ6nH0_C5>%K33-uHQf#&d)UgD?a8e#?tv^^bAx}`Bo4Da!E z!z!(TT4Lcg3&|2Lp8K)$lYppfc9P4GrdFh7jVmP68v19CvqG=gDLg-EKC_eDCqnT7 zlF`=)A)N{#*p&>te(C?@j#LR&o32Zc$X^*B)G}Ryt-IHWk!tLPsO$q67w~z1;3@4% zZSO~B3M6Qt^JI>HM*3?c3gMm`hX2Xxb94u?Opb}m81@jO(Iezs%nH<|dMF$hTWJQm zs)~CaXIPiiQ_q>0Y;pNgs!GMKdveSek`Ec!2;22=FyYo-8zU60!$G4U884q4&-T+s zCg(a0ggQ;V1fo)Q-XxPGD83;+sVUp`?TMW-KK%GaefL-p(NBT=HdhNK&!QnLS$3gf znJIkj?3A*3^7(Ih;ngcx$Vj^n$=j_x62EKo$cDtlay+yT9G5>>;FR%{#(JimAh{2( zO|NX`kzA1TYOm-C!d|bGrf>Ip&xC6?cQ(kcYIiqmCb;rg7Eo3EUWN~aMV&0st7K>z zpkk0Ah*w`6#W9=WEKs=kyC8D-m`sIk6|r!wFMrXvjD5MvVlQG<^*A>u1g|%18#K(e zkQ`*KmED(JjIc&{sn^fHgYbEW7CO;2843J5WRm`kgdQZ?Prjx+j~4zXpqAVGa>i${ zMg?14u*W2>Xdf||B+Tg#G3@7^j?gW++CNKjT(Nj^b<-+10Sw5lx9FO;scZRe>FNOe3v) zLcF$vvEr2qt5Lh+Hk&)@6@S zX2{3D7nH`o{EaKR2Q@hbQEB$ZBcrK)JeP`T#>=}1@^_|e`>Zx{5zHZV0&9o~o9>6+ zY4FS(S(%yUBt-#?$N4hP83EsM2nLLujk(KQLYEGd(W2hnQ ze2naS(A~kWq&rsY@Me%hD_J*`yTW76T2yGNvn3? z-3A&XoDa%gM|HT^!Nn#SY^8xo02qUXDJ+H>*3w#!zSQPx136f8) zPO8g`-u{${MWEVXX7m?1$XqzyyN0ut-Rw5@F*>#Iq{z}-y26ld*^O)~Mo^>u)~gpQ zLT{q>#XfYjqCfNH5Fkdgghk zrFCYuu^CsaSV}H(T;x`i53B`WAfcURJjp*X9=42Ly6E|pX8GnoQq53 zZqOZt)$6%Kzjzl&xt(5oDSe_L^f==_#lv&!LlU-4D$<(vBC4%p8m)Io=Yk6#q^g76 zJF`^k8v|coAP;g@L1LPK!4M=#8R2{m79VRdRDjycy=XUI?A1-9_&0eSb9B1FdX6gz zZDrdS0r#I+cgnHPzP|bA1Fb`Ws{$OaKCC+nt1l{>DosQ)?ol>X3x5;onCNwCywoP9 zgb)e~?~0BBl8@2De9|=5=f?OXa<`W?T0`6Tcs0&-aRSf&7{Q$4Han^#1ZH`i5Dojv z{O$C%kDYpw^%QRwGZ-�hCN)??pw5Q7TpEwrkL+zI0Ri{mMStpZ~`uXZ#{+q26Cx zdc9?xH$!;s=mg!=T0J9H6*3MblQI?#Z%URFwqp5&Qxw7)_C#K}M2r5elkhfVMqW0` zH;MGK`JSbxz7FZ6@bhk0WM!AkOwKxCUm$T$wQ(s9n~f^W5fSG(+upyS)vKHFwp3mn z-dJsCjV3Up((s>fU7T^Q+DDQ07yCHI^?~(w+lpTW^roQle6R)4;#iF3?7VU419iMs zGJR{PUD(o}aHF?|86xTM*N9UNi!6<5@HCJZ1=#%k&@f~6;%IeZSlQDR{8QAsMW#vz zrSAp9?%RJ`(x2&_L)%))&CV7(0>f*5t3+88Mh!3PaV-_LvMv4+Vm>Dw2*8U>Vq4p@ z2yNqlvuHA98)PqWetocec;Ka`INBmx8``c%yk2=_cM4 zb<~C}JmE;wn)Hc>SHa9W37Lmr5)}G+hlu~{C1a~{&Vfc2nbA6Jb~75X{v>#Gve5E1or1w zjSHcT1D`A|o%LybmZ#MylkbC5T3Wk^M@?&tq3dt_-K&H3aXX~R(`>9R5nSyjfUm~R z(XU$JrYPTZdNFJ^U43aBcFy&~A1(IlBy|>5Y?ZrBc>S092p9Qe(O2}vByP?0Ql@)| z$*hx6oarhh*#zr8crun+G00tA8NC8A9m@!%tZHyWXqpN*Hcj!n27Q&)C0zBE^)Jqd zlhpYK$ySazdgTPI4Pe+kf@vvb;bagphrjVoa>DGNXN4OtoWs8iJqxa^S5u<%HtfbF zyeSXPGmn8J^*mLj_5x_zw*qe#{@601%*&sw@MUO+KZvGL*u=#;xNI<6d2{4iM?%M- zqfn_gJyB<+w6vPBZ(1FzTD_}STvj6W7jFc!?1Gkd=B{suHGe0@*Gc%f2h62zNu=>-hZQ0%Evq!5?8&NPg8i){H%pVt&! zx-PM=7$}!6BEqLd#PH~Uuv3|IE_N0oMUHf)kVG>|RUC5?qvLL`NTiQ09DNSO^nFlt zHuUFX;DX1*+WR(&+H1nl-X`L*OIw=9Z4y5n##a;Kh~s{U)Fu^XMkbP2CS^oSUaU}?K4ANcG3$ycL#qy_omFpZpel5XO$1^F?H`*DFUySGgw$ZF$r z#k)I3E{Si;#f@4>CUA&3za;~lT}FrhZ3Hl;9o4=}mz_UP^HZj!{=4}SW+6^Mt*INz zm$a(>#&GIz)P)Mc|1?Q9oeInc#NQh+vse~4#aBC)p4H1!WV5c)RVwE;#h0GAYYdt< zCdtRJR9KXX%f;chB1#OfQi$Q6u@Z<@$6Ro2$R`{(s?TlE)-C)Q!p*kQS6kSuFgpJf zjTtf_T8Bum11d8z_}x;o*f^exN$P@{&^Y%pHK|EcYE)1m66-apNaIp#V|Thmb;c0D zrwm!N6zS4hMAs=+Z$@&i(y*yQN$X$tJRd?|6Z%)(%FE1DH1&yJ4A&AT^%jr|4X}LA zHr`ZYr}9=vL6k bool { + crate::preflight::managed_install_ready().await +} + +fn should_emit_repair_failed(msg: &str) -> bool { + !msg.contains("NEEDS_ELEVATION") +} + +#[tauri::command] +pub async fn desktop_preflight() -> crate::preflight::DesktopPreflightResult { + crate::preflight::desktop_preflight_result().await +} + +/// Check if unsloth is installed AND functional. +/// Runs `unsloth -h` to verify the import chain works — a partial install +/// (binary exists but deps missing) will fail on import and return false, +/// which sends the user to the install screen for a clean re-install. +#[tauri::command] +pub async fn check_install_status() -> bool { + let Some(bin) = process::find_unsloth_binary() else { + return false; + }; + + let mut cmd = tokio::process::Command::new(&bin); + cmd.arg("-h") + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()); + + #[cfg(windows)] + { + cmd.creation_flags(crate::process::CREATE_NO_WINDOW); + } + + // Match the same AppImage env clearing used in process.rs and install.rs, + // otherwise the probe can fail due to bundled libs even when the install is fine. + #[cfg(target_os = "linux")] + if std::env::var_os("APPIMAGE").is_some() { + cmd.env_remove("LD_LIBRARY_PATH"); + cmd.env_remove("PYTHONHOME"); + cmd.env_remove("PYTHONPATH"); + } + + let mut child = match cmd.spawn() { + Ok(c) => c, + Err(e) => { + warn!("Install check: failed to spawn {:?}: {}", bin, e); + return false; + } + }; + + match tokio::time::timeout(std::time::Duration::from_secs(10), child.wait()).await { + Ok(Ok(status)) => { + let ok = status.success(); + if !ok { + warn!("Install check: `unsloth -h` exited with {}", status); + } + ok + } + Ok(Err(e)) => { + warn!("Install check: wait failed: {}", e); + false + } + Err(_) => { + warn!("Install check: `unsloth -h` timed out after 10s"); + let _ = child.kill().await; + false + } + } +} + +/// Start the backend server on the given port. +/// Also spawns a health watchdog that monitors the backend and emits +/// `server-crashed` if it becomes unresponsive (deadlock, OOM, etc.). +#[tauri::command] +pub async fn start_server( + app: AppHandle, + state: tauri::State<'_, BackendState>, + shutdown: tauri::State<'_, ShutdownFlag>, + port: u16, +) -> Result<(), String> { + info!("start_server command called with port {}", port); + + process::start_backend(&app, &state, port, &shutdown)?; + + // Spawn health watchdog for the owned backend — detects + // deadlocks and hangs that stdout-based crash detection misses. + let watchdog_state = state.inner().clone(); + let watchdog_shutdown = shutdown.inner().clone(); + let watchdog_app = app.clone(); + tokio::spawn(async move { + health_watchdog(watchdog_app, watchdog_state, watchdog_shutdown).await; + }); + + Ok(()) +} + +/// Start the managed backend without reusing an existing backend. +#[tauri::command] +pub async fn start_managed_server( + app: AppHandle, + state: tauri::State<'_, BackendState>, + shutdown: tauri::State<'_, ShutdownFlag>, + port: u16, +) -> Result<(), String> { + info!("start_managed_server command called with port {}", port); + process::start_backend(&app, &state, port, &shutdown)?; + + let watchdog_state = state.inner().clone(); + let watchdog_shutdown = shutdown.inner().clone(); + let watchdog_app = app.clone(); + tokio::spawn(async move { + health_watchdog(watchdog_app, watchdog_state, watchdog_shutdown).await; + }); + + Ok(()) +} + +/// Stop the backend server. +/// Sends SIGTERM to the process group, which triggers uvicorn's graceful +/// shutdown (same codepath as /api/shutdown). Falls back to SIGKILL after 5s. +#[tauri::command] +pub fn stop_server( + state: tauri::State<'_, BackendState>, + shutdown: tauri::State<'_, ShutdownFlag>, +) -> Result<(), String> { + info!("stop_server command called"); + process::stop_backend(&state, &shutdown) +} + +/// Check if a healthy Unsloth backend is running on the given port. +/// Expects JSON response with status=="healthy" AND service=="Unsloth UI Backend". +#[tauri::command] +pub async fn check_health(port: u16) -> Result { + match check_health_inner(port).await { + Ok(healthy) => Ok(healthy), + Err(e) => { + // Network errors are not command errors — just means not healthy + info!("Health check on port {} failed: {}", port, e); + Ok(false) + } + } +} + +async fn check_health_inner(port: u16) -> Result { + let url = format!("http://127.0.0.1:{}/api/health", port); + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(2)) + .build()?; + let resp = client.get(&url).send().await?; + let json: serde_json::Value = resp.json().await?; + + let healthy = json + .get("status") + .and_then(|v| v.as_str()) + .map(|s| s == "healthy") + .unwrap_or(false); + let correct_service = json + .get("service") + .and_then(|v| v.as_str()) + .map(|s| s == "Unsloth UI Backend") + .unwrap_or(false); + + Ok(healthy && correct_service) +} + +/// Return buffered server logs. +#[tauri::command] +pub fn get_server_logs(state: tauri::State<'_, BackendState>) -> Vec { + match state.lock() { + Ok(proc) => proc.logs.iter().cloned().collect(), + Err(e) => { + error!("Failed to lock state for logs: {}", e); + vec![] + } + } +} + +/// Open the Unsloth Studio directory in the system file manager. +#[tauri::command] +pub fn open_logs_dir() -> Result<(), String> { + let home = dirs::home_dir().ok_or("Could not determine home directory")?; + let dir = home.join(".unsloth").join("studio"); + + if !dir.exists() { + return Err(format!("Directory does not exist: {}", dir.display())); + } + + open::that(&dir).map_err(|e| format!("Failed to open directory: {}", e)) +} + +/// Start the first-launch installation process. +/// Runs the platform installer script with --tauri flag and streams progress events. +/// Returns "NEEDS_ELEVATION" if system packages need elevated install (Linux only). +#[tauri::command] +pub async fn start_install( + app: AppHandle, + state: tauri::State<'_, install::InstallState>, +) -> Result<(), String> { + let state = state.inner().clone(); + tokio::task::spawn_blocking(move || install::run_install(app, state)) + .await + .map_err(|e| format!("Install task panicked: {e}"))? +} + +/// Install system packages with elevated permissions (Linux only). +/// Called by frontend after user approves the elevation dialog. +/// Only allows packages that the install script reported as needed. +#[cfg(target_os = "linux")] +#[tauri::command] +pub fn install_system_packages( + packages: Vec, + state: tauri::State<'_, install::InstallState>, +) -> Result<(), String> { + // Cross-check against the packages the install script actually reported + let allowed = state + .lock() + .map(|s| s.needed_packages.clone()) + .unwrap_or_default(); + for pkg in &packages { + if !allowed.contains(pkg) { + return Err(format!( + "Package '{}' was not requested by the install script", + pkg + )); + } + } + install::install_system_packages(&packages) +} + +/// Stub for non-Linux platforms — elevation is handled by the scripts themselves. +#[cfg(not(target_os = "linux"))] +#[tauri::command] +pub fn install_system_packages( + _packages: Vec, + _state: tauri::State<'_, install::InstallState>, +) -> Result<(), String> { + Err("Elevated package install is only supported on Linux".to_string()) +} + +/// Run backend update: stop server, run `unsloth studio update`, emit progress. +/// Does NOT restart the backend — the frontend handles shell update + relaunch after. +#[tauri::command] +pub async fn start_backend_update( + app: AppHandle, + backend_state: tauri::State<'_, BackendState>, + shutdown: tauri::State<'_, ShutdownFlag>, + update_state: tauri::State<'_, update::UpdateState>, + install_state: tauri::State<'_, install::InstallState>, +) -> Result<(), String> { + info!("start_backend_update command called"); + + // Signal the health watchdog to exit immediately, before any guards. + // This closes the race window where the watchdog could emit server-crashed + // between our command being called and stop_backend completing. + shutdown.store(true, std::sync::atomic::Ordering::SeqCst); + + // Guard: reject if install is running + if install_state + .lock() + .map(|s| s.child.is_some()) + .unwrap_or(false) + { + return Err("Cannot update while installation is in progress.".to_string()); + } + + // Guard: reject if update is already running + if update_state + .lock() + .map(|s| s.child.is_some()) + .unwrap_or(false) + { + return Err("Update is already running.".to_string()); + } + + // Stop backend if running + if backend_state + .lock() + .map(|s| s.child.is_some()) + .unwrap_or(false) + { + info!("Stopping backend before update..."); + process::stop_backend(&backend_state, &shutdown)?; + } + + // Run update in a blocking thread + let state = update_state.inner().clone(); + tokio::task::spawn_blocking(move || update::run_backend_update(app, state)) + .await + .map_err(|e| format!("Update task panicked: {e}"))? +} + +/// Repair a stale managed Studio install. +#[tauri::command] +pub async fn start_managed_repair( + app: AppHandle, + backend_state: tauri::State<'_, BackendState>, + shutdown: tauri::State<'_, ShutdownFlag>, + update_state: tauri::State<'_, update::UpdateState>, + install_state: tauri::State<'_, install::InstallState>, +) -> Result<(), String> { + info!("start_managed_repair command called"); + + if install_state + .lock() + .map(|s| s.child.is_some()) + .unwrap_or(false) + { + return Err("Cannot repair while installation is in progress.".to_string()); + } + + if update_state + .lock() + .map(|s| s.child.is_some()) + .unwrap_or(false) + { + return Err("Repair is already running.".to_string()); + } + + shutdown.store(true, std::sync::atomic::Ordering::SeqCst); + + if backend_state + .lock() + .map(|s| s.child.is_some()) + .unwrap_or(false) + { + info!("Stopping backend before repair..."); + process::stop_backend(&backend_state, &shutdown)?; + } + + let _ = app.emit("repair-progress", "Updating existing Studio install..."); + let update_app = app.clone(); + let update_state = update_state.inner().clone(); + let update_result = tokio::task::spawn_blocking(move || { + update::run_backend_update_for_repair(update_app, update_state) + }) + .await + .map_err(|e| format!("Repair update task panicked: {e}"))?; + + match update_result { + Ok(()) if managed_install_ready_after_repair().await => { + info!("Managed repair complete after update"); + let _ = app.emit("repair-complete", ()); + return Ok(()); + } + Ok(()) => { + warn!("Managed repair update finished, but preflight is still not ready; falling back to installer"); + let _ = app.emit( + "repair-progress", + "Update finished, but Studio is still not ready. Running bundled installer...", + ); + } + Err(msg) => { + if msg.to_ascii_lowercase().contains("already running") { + error!("Managed repair update conflict: {}", msg); + let _ = app.emit("repair-failed", &msg); + return Err(msg); + } + + warn!( + "Managed repair update failed, falling back to bundled installer: {}", + msg + ); + let _ = app.emit( + "repair-progress", + "Update failed. Running bundled installer...", + ); + } + } + + let install_app = app.clone(); + let install_state = install_state.inner().clone(); + let install_result = tokio::task::spawn_blocking(move || { + install::run_install_for_repair(install_app, install_state) + }) + .await + .map_err(|e| format!("Repair install task panicked: {e}"))?; + + if let Err(msg) = install_result { + if should_emit_repair_failed(&msg) { + error!("Managed repair installer failed: {}", msg); + let _ = app.emit("repair-failed", &msg); + } + return Err(msg); + } + + if managed_install_ready_after_repair().await { + info!("Managed repair complete after installer"); + let _ = app.emit("repair-complete", ()); + return Ok(()); + } + + let msg = "Repair finished, but Studio install is still not desktop-ready.".to_string(); + error!("{}", msg); + let _ = app.emit("repair-failed", &msg); + Err(msg) +} + +#[cfg(test)] +mod tests { + #[test] + fn repair_elevation_is_not_a_terminal_repair_failure() { + assert!(!super::should_emit_repair_failed("NEEDS_ELEVATION")); + assert!(super::should_emit_repair_failed( + "Installer exited with code 1" + )); + } +} + +/// Periodic health check that detects deadlocked or hung backends. +/// Starts 30s after the backend is launched (to allow initial startup), +/// then pings /api/health every 15s. After 3 consecutive failures (45s) +/// with the process still alive, emits `server-crashed` so the frontend +/// can offer a restart. +async fn health_watchdog(app: AppHandle, state: BackendState, shutdown: ShutdownFlag) { + use std::sync::atomic::Ordering; + + // Give the backend time to start up + tokio::time::sleep(std::time::Duration::from_secs(30)).await; + + let mut consecutive_failures: u32 = 0; + + loop { + tokio::time::sleep(std::time::Duration::from_secs(15)).await; + + if shutdown.load(Ordering::SeqCst) { + info!("Health watchdog: shutdown flag set, exiting"); + break; + } + + let (port, has_child) = { + let proc = match state.lock() { + Ok(p) => p, + Err(_) => break, + }; + (proc.port, proc.child.is_some()) + }; + + // Stop watching if the backend is gone + if !has_child { + info!("Health watchdog: backend stopped, exiting"); + break; + } + + let Some(port) = port else { + continue; // Port not yet known + }; + + match check_health_inner(port).await { + Ok(true) => { + consecutive_failures = 0; + } + _ => { + consecutive_failures += 1; + warn!( + "Health watchdog: failure {}/3 on port {}", + consecutive_failures, port + ); + if consecutive_failures >= 3 { + error!( + "Health watchdog: backend unresponsive for 45s, killing and declaring dead" + ); + // Kill the zombie process so retry can start fresh + let _ = process::stop_backend(&state, &shutdown); + let _ = app.emit("server-crashed", ()); + break; + } + } + } + } +} diff --git a/studio/src-tauri/src/desktop_auth.rs b/studio/src-tauri/src/desktop_auth.rs new file mode 100644 index 0000000000..df09df7a14 --- /dev/null +++ b/studio/src-tauri/src/desktop_auth.rs @@ -0,0 +1,388 @@ +use crate::preflight::{DesktopPreflightDisposition, DesktopPreflightResult}; +use crate::process::BackendState; +use log::info; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use std::path::{Path, PathBuf}; + +static DESKTOP_AUTH_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(()); + +#[derive(Debug, Serialize)] +pub struct DesktopAuthResponse { + pub access_token: String, + pub refresh_token: String, +} + +#[derive(Debug, Serialize, Deserialize)] +struct DesktopAuthRequest { + secret: String, +} + +#[derive(Debug, Deserialize)] +struct TokenResponse { + access_token: String, + refresh_token: String, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum PortSource { + Cached, + Discovered, +} + +#[derive(Clone, Copy, Debug)] +struct BackendPort { + port: u16, + source: PortSource, +} + +#[derive(Debug)] +enum AuthError { + Connectivity(String), + Failed(String), +} + +impl AuthError { + fn message(self) -> String { + match self { + Self::Connectivity(message) | Self::Failed(message) => message, + } + } +} + +fn auth_secret_path(home: &Path, filename: &str) -> PathBuf { + home.join(".unsloth") + .join("studio") + .join("auth") + .join(filename) +} + +fn auth_url(port: u16, route: &str) -> String { + format!("http://127.0.0.1:{port}/api/auth/{route}") +} + +fn home_dir() -> Result { + dirs::home_dir().ok_or_else(|| "Could not determine home directory".to_string()) +} + +fn desktop_secret_path() -> Result { + Ok(auth_secret_path(&home_dir()?, ".desktop_secret")) +} + +fn read_secret_if_exists(path: &Path) -> Result, String> { + match std::fs::read_to_string(path) { + Ok(s) => Ok(Some(s.trim().to_string())), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(e) => Err(format!( + "Failed to read auth secret at {}: {}", + path.display(), + e + )), + } +} + +async fn current_backend_port( + state: &tauri::State<'_, BackendState>, +) -> Result { + if let Some(port) = state.lock().map_err(|e| e.to_string())?.port { + return Ok(BackendPort { + port, + source: PortSource::Cached, + }); + } + + let port = discover_compatible_backend_port() + .await + .ok_or_else(|| "Backend is not ready".to_string())?; + + { + let mut proc = state.lock().map_err(|e| e.to_string())?; + if proc.port.is_none() { + proc.port = Some(port); + } + } + + Ok(BackendPort { + port, + source: PortSource::Discovered, + }) +} + +fn attached_ready_port(preflight: DesktopPreflightResult) -> Option { + if preflight.disposition == DesktopPreflightDisposition::AttachedReady { + preflight.port + } else { + None + } +} + +async fn discover_compatible_backend_port() -> Option { + attached_ready_port(crate::preflight::desktop_preflight_result().await) +} + +fn update_backend_port(state: &tauri::State<'_, BackendState>, port: u16) -> Result<(), String> { + let mut proc = state.lock().map_err(|e| e.to_string())?; + proc.port = Some(port); + Ok(()) +} + +fn classify_auth_send_error(error: reqwest::Error) -> AuthError { + let message = format!("Desktop auth failed: {}", error); + if error.is_connect() || error.is_timeout() { + AuthError::Connectivity(message) + } else { + AuthError::Failed(message) + } +} + +fn should_retry_with_discovered_port(source: PortSource, error: &AuthError) -> bool { + matches!( + (source, error), + (PortSource::Cached, AuthError::Connectivity(_)) + ) +} + +async fn exchange_desktop_secret( + client: &Client, + port: u16, + secret: &str, +) -> Result, AuthError> { + let response = client + .post(auth_url(port, "desktop-login")) + .json(&DesktopAuthRequest { + secret: secret.to_string(), + }) + .send() + .await + .map_err(classify_auth_send_error)?; + + if response.status() == reqwest::StatusCode::NOT_FOUND { + return Err(AuthError::Failed( + "Running Studio backend is too old for this desktop app. Update that backend and restart." + .to_string(), + )); + } + if response.status() == reqwest::StatusCode::UNAUTHORIZED { + return Ok(None); + } + if !response.status().is_success() { + return Err(AuthError::Failed("Desktop auth failed".to_string())); + } + + response + .json::() + .await + .map(|tokens| { + Some(DesktopAuthResponse { + access_token: tokens.access_token, + refresh_token: tokens.refresh_token, + }) + }) + .map_err(|e| AuthError::Failed(format!("Desktop auth failed: {}", e))) +} + +async fn provision_desktop_auth() -> Result<(), String> { + let bin = crate::process::resolve_backend_binary()?; + let mut cmd = tokio::process::Command::new(&bin); + cmd.args(["studio", "provision-desktop-auth"]) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::piped()); + #[cfg(target_os = "linux")] + if std::env::var_os("APPIMAGE").is_some() { + cmd.env_remove("LD_LIBRARY_PATH"); + cmd.env_remove("PYTHONHOME"); + cmd.env_remove("PYTHONPATH"); + } + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + cmd.creation_flags(crate::process::CREATE_NO_WINDOW); + } + + let output = tokio::time::timeout(std::time::Duration::from_secs(30), cmd.output()) + .await + .map_err(|_| "Desktop auth provisioning timed out after 30s".to_string())? + .map_err(|e| format!("Desktop auth provisioning failed: {}", e))?; + if output.status.success() { + return Ok(()); + } + let stderr = String::from_utf8_lossy(&output.stderr); + Err(format!( + "Desktop auth provisioning failed: {}", + stderr.trim() + )) +} + +async fn authenticate_with_stale_port_retry( + client: &Client, + state: &tauri::State<'_, BackendState>, + backend: BackendPort, + secret: &str, +) -> Result<(Option, BackendPort), String> { + match exchange_desktop_secret(client, backend.port, secret).await { + Ok(tokens) => Ok((tokens, backend)), + Err(error) if should_retry_with_discovered_port(backend.source, &error) => { + let Some(port) = discover_compatible_backend_port().await else { + return Err(error.message()); + }; + update_backend_port(state, port)?; + let backend = BackendPort { + port, + source: PortSource::Discovered, + }; + exchange_desktop_secret(client, port, secret) + .await + .map(|tokens| (tokens, backend)) + .map_err(AuthError::message) + } + Err(error) => Err(error.message()), + } +} + +#[tauri::command] +pub async fn desktop_auth( + state: tauri::State<'_, BackendState>, +) -> Result { + let _auth_guard = DESKTOP_AUTH_LOCK.lock().await; + let mut backend = current_backend_port(&state).await?; + let client = Client::builder() + .timeout(std::time::Duration::from_secs(5)) + .build() + .map_err(|e| format!("Desktop auth failed: {}", e))?; + + for attempt in 0..2 { + if attempt == 1 { + info!("Desktop auth: provisioning local desktop secret"); + provision_desktop_auth().await?; + } + + let path = desktop_secret_path()?; + let Some(secret) = read_secret_if_exists(&path)? else { + continue; + }; + + info!("Desktop auth: exchanging desktop secret"); + let (tokens, resolved_backend) = + authenticate_with_stale_port_retry(&client, &state, backend, &secret).await?; + backend = resolved_backend; + if let Some(tokens) = tokens { + return Ok(tokens); + } + } + + Err( + "Desktop auth failed. Update or repair the managed Studio install, then restart Studio." + .to_string(), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + use tokio::net::TcpListener; + + async fn login_server(status: &str) -> u16 { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let port = listener.local_addr().unwrap().port(); + let status = status.to_string(); + + tokio::spawn(async move { + let (mut stream, _) = listener.accept().await.unwrap(); + let mut buffer = [0; 1024]; + let _ = stream.read(&mut buffer).await.unwrap(); + let response = + format!("HTTP/1.1 {status}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"); + stream.write_all(response.as_bytes()).await.unwrap(); + }); + + port + } + + #[test] + fn auth_secret_path_joins_expected_location() { + let home = PathBuf::from("/home/alex"); + assert_eq!( + auth_secret_path(&home, ".desktop_secret"), + PathBuf::from("/home/alex/.unsloth/studio/auth/.desktop_secret") + ); + } + + #[test] + fn auth_url_builds_local_endpoint() { + assert_eq!( + auth_url(8890, "desktop-login"), + "http://127.0.0.1:8890/api/auth/desktop-login" + ); + } + + #[test] + fn retry_discovery_only_for_cached_connectivity_errors() { + assert!(should_retry_with_discovered_port( + PortSource::Cached, + &AuthError::Connectivity("connection refused".to_string()) + )); + assert!(!should_retry_with_discovered_port( + PortSource::Discovered, + &AuthError::Connectivity("connection refused".to_string()) + )); + assert!(!should_retry_with_discovered_port( + PortSource::Cached, + &AuthError::Failed("Desktop auth failed".to_string()) + )); + } + + #[test] + fn attached_ready_port_requires_attached_ready_with_port() { + let compatible = DesktopPreflightResult { + disposition: DesktopPreflightDisposition::AttachedReady, + reason: None, + port: Some(8890), + can_auto_repair: false, + managed_bin: None, + }; + assert_eq!(attached_ready_port(compatible), Some(8890)); + + let missing_port = DesktopPreflightResult { + disposition: DesktopPreflightDisposition::AttachedReady, + reason: None, + port: None, + can_auto_repair: false, + managed_bin: None, + }; + assert_eq!(attached_ready_port(missing_port), None); + + let managed_ready = DesktopPreflightResult { + disposition: DesktopPreflightDisposition::ManagedReady, + reason: None, + port: Some(8890), + can_auto_repair: false, + managed_bin: None, + }; + assert_eq!(attached_ready_port(managed_ready), None); + } + + #[tokio::test] + async fn exchange_desktop_secret_returns_none_for_unauthorized() { + let port = login_server("401 Unauthorized").await; + let tokens = exchange_desktop_secret(&Client::new(), port, "desktop-stale") + .await + .unwrap(); + + assert!(tokens.is_none()); + } + + #[tokio::test] + async fn exchange_desktop_secret_reports_unsupported_backend_on_not_found() { + let port = login_server("404 Not Found").await; + let error = exchange_desktop_secret(&Client::new(), port, "desktop-secret") + .await + .unwrap_err() + .message(); + + assert_eq!( + error, + "Running Studio backend is too old for this desktop app. Update that backend and restart." + ); + } +} diff --git a/studio/src-tauri/src/install.rs b/studio/src-tauri/src/install.rs new file mode 100644 index 0000000000..5760989353 --- /dev/null +++ b/studio/src-tauri/src/install.rs @@ -0,0 +1,555 @@ +use log::{error, info, warn}; +use process_wrap::std::*; +use std::io::BufRead; +use std::path::{Path, PathBuf}; +use std::process::{Command, ExitStatus, Stdio}; +use std::sync::{Arc, Mutex}; +use tauri::{AppHandle, Emitter, Manager}; + +// ── Types ── + +pub struct InstallProcess { + /// Process group handle — killing this kills the entire subprocess tree. + pub child: Option>, + pub intentional_stop: bool, + /// Packages needing elevated install, parsed from [TAURI:NEED_SUDO] output. + pub needed_packages: Vec, +} + +impl Default for InstallProcess { + fn default() -> Self { + Self { + child: None, + intentional_stop: false, + needed_packages: Vec::new(), + } + } +} + +pub type InstallState = Arc>; + +pub fn new_install_state() -> InstallState { + Arc::new(Mutex::new(InstallProcess::default())) +} + +use crate::process::trim_line_endings; + +// ── Script Resolution ── + +/// Returns (script_path, args) depending on dev vs production mode. +/// Dev mode: repo root script + --tauri --local +/// Production: bundled resource + --tauri +fn resolve_install_script(app: &AppHandle) -> Result<(PathBuf, Vec), String> { + let mut args = vec!["--tauri".to_string()]; + + if cfg!(debug_assertions) { + let repo_root = Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() // studio/ + .and_then(|p| p.parent()) // repo root + .ok_or("Cannot resolve repo root from CARGO_MANIFEST_DIR")?; + + let script = if cfg!(unix) { + repo_root.join("install.sh") + } else { + repo_root.join("install.ps1") + }; + + if !script.exists() { + return Err(format!("Install script not found: {}", script.display())); + } + + args.push("--local".to_string()); + info!("Dev mode: using repo script at {}", script.display()); + Ok((script, args)) + } else { + let name = if cfg!(unix) { + "install.sh" + } else { + "install.ps1" + }; + let script = app + .path() + .resolve(name, tauri::path::BaseDirectory::Resource) + .map_err(|e| format!("Failed to resolve bundled {}: {}", name, e))?; + info!("Production: using bundled script at {}", script.display()); + Ok((script, args)) + } +} + +// ── Emit Helpers ── + +#[derive(Clone, Copy)] +enum InstallEventMode { + Full, + Repair, +} + +impl InstallEventMode { + fn progress_event(self) -> &'static str { + match self { + Self::Full => "install-progress", + Self::Repair => "repair-progress", + } + } + + fn emit_install_structured_events(self) -> bool { + matches!(self, Self::Full) + } + + fn emit_terminal_events(self) -> bool { + matches!(self, Self::Full) + } + + fn needs_elevation_event(self) -> &'static str { + match self { + Self::Full => "install-needs-elevation", + Self::Repair => "repair-needs-elevation", + } + } +} + +fn emit_mode_progress(app: &AppHandle, mode: InstallEventMode, message: &str) { + info!("[install] {}", message); + let _ = app.emit(mode.progress_event(), message); +} + +fn emit_failed(app: &AppHandle, message: &str) { + error!("[install] FAILED: {}", message); + let _ = app.emit("install-failed", message); +} + +fn emit_complete(app: &AppHandle) { + info!("[install] Installation complete"); + let _ = app.emit("install-complete", ()); +} + +// ── Spawn ── + +/// Spawns the install script in a process group. +/// Returns (stdout, stderr) handles for streaming. +/// The GroupChild is stored in state so stop_install() can kill the entire tree. +fn spawn_script( + script: &Path, + args: &[String], + state: &InstallState, +) -> Result< + ( + Option, + Option, + ), + String, +> { + let mut install = state.lock().map_err(|e| e.to_string())?; + if install.child.is_some() { + return Err("Installation is already running.".to_string()); + } + install.intentional_stop = false; + install.needed_packages.clear(); + + // Scripts create ~/.unsloth/studio/ themselves, but need a writable cwd. + let home = dirs::home_dir().ok_or("Could not determine home directory")?; + let work_dir = home.join(".unsloth"); + if !work_dir.exists() { + std::fs::create_dir_all(&work_dir) + .map_err(|e| format!("Failed to create {}: {}", work_dir.display(), e))?; + } + + #[cfg(unix)] + let mut cmd = Command::new("bash"); + #[cfg(unix)] + cmd.arg(script) + .args(args) + .current_dir(&work_dir) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + #[cfg(windows)] + let mut cmd = Command::new("powershell.exe"); + #[cfg(windows)] + cmd.args([ + "-NoLogo", + "-NoProfile", + "-NonInteractive", + "-WindowStyle", + "Hidden", + "-ExecutionPolicy", + "Bypass", + "-File", + ]) + .arg(script) + .args(args) + .current_dir(&work_dir) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + // AppImage sets LD_LIBRARY_PATH to its bundled libs, which breaks Python + // spawned by the install script. Only clear inside AppImage — native installs + // may need these for custom CUDA or conda paths. + #[cfg(target_os = "linux")] + if std::env::var_os("APPIMAGE").is_some() { + cmd.env_remove("LD_LIBRARY_PATH"); + cmd.env_remove("PYTHONHOME"); + cmd.env_remove("PYTHONPATH"); + } + + // On Windows, launch the installer directly with CREATE_NO_WINDOW. + // The app process is assigned to a KILL_ON_JOB_CLOSE job in main.rs, so + // child cleanup on crash comes from inherited job membership instead. + #[cfg(windows)] + let mut child: Box = { + use std::os::windows::process::CommandExt; + cmd.creation_flags(crate::process::CREATE_NO_WINDOW); + let child = cmd + .spawn() + .map_err(|e| format!("Failed to spawn install script: {}", e))?; + Box::new(child) + }; + + #[cfg(unix)] + let mut child: Box = { + // Keep the whole installer tree in a process group on Unix. + let mut wrap = CommandWrap::from(cmd); + wrap.wrap(ProcessGroup::leader()); + wrap.spawn() + .map_err(|e| format!("Failed to spawn install script: {}", e))? + }; + + let stdout = child.stdout().take(); + let stderr = child.stderr().take(); + install.child = Some(child); + Ok((stdout, stderr)) +} + +// ── Stream ── + +/// Spawns reader threads for stdout/stderr. +/// Parses [TAURI:*] lines from stdout for structured events. +fn stream_output( + app: &AppHandle, + state: &InstallState, + event_mode: InstallEventMode, + stdout: Option, + stderr: Option, +) -> Vec> { + let mut threads = Vec::new(); + + if let Some(out) = stdout { + let app_clone = app.clone(); + let state_clone = Arc::clone(state); + threads.push(std::thread::spawn(move || { + let mut reader = std::io::BufReader::new(out); + let mut buf = Vec::new(); + loop { + buf.clear(); + match reader.read_until(b'\n', &mut buf) { + Ok(0) => break, + Ok(_) => { + let text = String::from_utf8_lossy(trim_line_endings(&buf)).into_owned(); + // Parse structured Tauri protocol lines + if let Some(packages) = text.strip_prefix("[TAURI:NEED_SUDO] ") { + let pkgs: Vec = + packages.split_whitespace().map(String::from).collect(); + if let Ok(mut install) = state_clone.lock() { + install.needed_packages = pkgs; + } + } else if let Some(step) = text.strip_prefix("[TAURI:STEP] ") { + if !event_mode.emit_install_structured_events() { + info!("[install][stdout] {}", text); + let _ = app_clone.emit(event_mode.progress_event(), &text); + continue; + } + let _ = app_clone.emit("install-step", step); + } else if let Some(detail) = text.strip_prefix("[TAURI:PROGRESS] ") { + if !event_mode.emit_install_structured_events() { + info!("[install][stdout] {}", text); + let _ = app_clone.emit(event_mode.progress_event(), detail); + continue; + } + let _ = app_clone.emit("install-progress-detail", detail); + } + // Always forward the raw line + info!("[install][stdout] {}", text); + let _ = app_clone.emit(event_mode.progress_event(), &text); + } + Err(e) => { + warn!("[install] Error reading stdout: {}", e); + break; + } + } + } + })); + } + + if let Some(err) = stderr { + let app_clone = app.clone(); + threads.push(std::thread::spawn(move || { + let mut reader = std::io::BufReader::new(err); + let mut buf = Vec::new(); + loop { + buf.clear(); + match reader.read_until(b'\n', &mut buf) { + Ok(0) => break, + Ok(_) => { + let text = String::from_utf8_lossy(trim_line_endings(&buf)).into_owned(); + warn!("[install][stderr] {}", text); + let _ = app_clone.emit(event_mode.progress_event(), &text); + } + Err(e) => { + warn!("[install] Error reading stderr: {}", e); + break; + } + } + } + })); + } + + threads +} + +// ── Wait & Finalize ── + +/// Waits for the install process to exit. Returns (exit_status, was_intentional_stop). +/// Times out after 2 hours to prevent infinite loops if the child hangs. +fn wait_for_exit(state: &InstallState) -> Result<(ExitStatus, bool), String> { + const MAX_WAIT_ITERATIONS: u32 = 72_000; // 2h at 100ms intervals + for _ in 0..MAX_WAIT_ITERATIONS { + let mut install = state.lock().map_err(|e| e.to_string())?; + let intentional = install.intentional_stop; + + match install.child.as_mut() { + Some(child) => match child.try_wait() { + Ok(Some(status)) => { + install.child = None; + return Ok((status, intentional)); + } + Ok(None) => {} + Err(e) => { + install.child = None; + return Err(format!("Error waiting for installer: {}", e)); + } + }, + None if intentional => return Err("Installation stopped.".to_string()), + None => return Err("Installer process disappeared unexpectedly.".to_string()), + } + + drop(install); + std::thread::sleep(std::time::Duration::from_millis(100)); + } + // Timed out — kill and report + let _ = stop_install(state); + Err("Installation timed out after 2 hours".to_string()) +} + +// ── Public API ── + +/// Run the install script. Returns Ok(()) on success. +/// Returns Err("NEEDS_ELEVATION") if system packages need elevated install (Linux only). +/// Returns Err(message) on other failures. +pub fn run_install(app: AppHandle, state: InstallState) -> Result<(), String> { + run_install_with_event_mode(app, state, InstallEventMode::Full) +} + +pub(crate) fn run_install_for_repair(app: AppHandle, state: InstallState) -> Result<(), String> { + run_install_with_event_mode(app, state, InstallEventMode::Repair) +} + +fn run_install_with_event_mode( + app: AppHandle, + state: InstallState, + event_mode: InstallEventMode, +) -> Result<(), String> { + emit_mode_progress(&app, event_mode, "Starting installation..."); + + let (script, args) = resolve_install_script(&app)?; + emit_mode_progress( + &app, + event_mode, + &format!("Using script: {}", script.display()), + ); + + let (stdout, stderr) = spawn_script(&script, &args, &state)?; + let threads = stream_output(&app, &state, event_mode, stdout, stderr); + + // Wait for exit, join reader threads + let result = wait_for_exit(&state); + for handle in threads { + let _ = handle.join(); + } + + match result { + Ok((status, _)) if status.success() => { + if event_mode.emit_terminal_events() { + emit_complete(&app); + } + Ok(()) + } + Ok((status, _)) => { + let code = status.code().unwrap_or(-1); + if code == 2 { + // Script needs elevated package install — report to frontend + let packages = state + .lock() + .map(|i| i.needed_packages.clone()) + .unwrap_or_default(); + info!("[install] Needs elevation for packages: {:?}", packages); + let _ = app.emit(event_mode.needs_elevation_event(), &packages); + Err("NEEDS_ELEVATION".to_string()) + } else { + let msg = format!("Installer exited with code {}", code); + if event_mode.emit_terminal_events() { + emit_failed(&app, &msg); + } + Err(msg) + } + } + Err(msg) if msg == "Installation stopped." => { + info!("[install] Installation stopped intentionally"); + Err(msg) + } + Err(msg) => { + if event_mode.emit_terminal_events() { + emit_failed(&app, &msg); + } + Err(msg) + } + } +} + +/// Stop a running install process gracefully. +/// Unix: SIGTERM to process group -> wait up to 5s -> SIGKILL +/// Windows: hidden taskkill /T /F to terminate the installer tree +pub fn stop_install(state: &InstallState) -> Result<(), String> { + let mut child = { + let mut install = match state.lock() { + Ok(guard) => guard, + Err(poisoned) => { + warn!("Install state mutex poisoned, recovering for cleanup"); + poisoned.into_inner() + } + }; + install.intentional_stop = true; + install.child.take() + }; + + let Some(ref mut child) = child else { + return Ok(()); + }; + + let pid = child.id(); + info!("Stopping installer process group (pid {})", pid); + + // Try graceful SIGTERM first so pip/cmake can clean up temp files + #[cfg(unix)] + { + if pid > i32::MAX as u32 { + // PID too large for i32 negation, fall back to direct kill + warn!("PID {} exceeds i32 range, using direct kill", pid); + let _ = child.kill(); + let _ = child.wait(); + return Ok(()); + } + unsafe { + libc::kill(-(pid as i32), libc::SIGTERM); + } + // Wait up to 5s for graceful exit + for _ in 0..50 { + match child.try_wait() { + Ok(Some(status)) => { + info!("Installer exited gracefully with status: {:?}", status); + return Ok(()); + } + Ok(None) => std::thread::sleep(std::time::Duration::from_millis(100)), + Err(_) => break, + } + } + warn!("Installer did not exit gracefully, force killing"); + } + + #[cfg(windows)] + { + crate::process::force_kill_process_tree(pid, child, "Installer"); + return Ok(()); + } + + #[cfg(unix)] + { + // Force kill (SIGKILL on Unix) + let _ = child.kill(); + let _ = child.wait(); + info!("Installer process group force stopped"); + Ok(()) + } +} + +/// Install system packages with elevated permissions (Linux only). +/// Uses `elevated-command` crate for native auth dialog. +#[cfg(target_os = "linux")] +pub fn install_system_packages(packages: &[String]) -> Result<(), String> { + use regex::Regex; + use std::path::Path; + use std::process::Command as StdCommand; + + // Validate package names to prevent injection via elevated command. + let valid_pkg = Regex::new(r"^[a-zA-Z0-9][a-zA-Z0-9.+\-]*$").unwrap(); + for pkg in packages { + if !valid_pkg.is_match(pkg) { + return Err(format!("Invalid package name: {}", pkg)); + } + } + + // AppImage bundles run on non-Debian distros too. Pick the first package + // manager we find. Names in `packages` are Debian-style; callers that want + // cross-distro support should translate before invoking. + let (program, base_args): (&str, &[&str]) = if Path::new("/usr/bin/apt-get").exists() { + ("apt-get", &["install", "-y"]) + } else if Path::new("/usr/bin/dnf").exists() { + ("dnf", &["install", "-y"]) + } else if Path::new("/usr/bin/zypper").exists() { + ("zypper", &["install", "-y"]) + } else if Path::new("/usr/bin/pacman").exists() { + ("pacman", &["-S", "--noconfirm"]) + } else { + return Err( + "No supported system package manager found (apt-get, dnf, zypper, pacman)".to_string(), + ); + }; + + info!( + "[install] Elevated install of packages via {}: {}", + program, + packages.join(", ") + ); + + let mut cmd = StdCommand::new(program); + cmd.args(base_args).args(packages); + + let elevated = elevated_command::Command::new(cmd) + .output() + .map_err(|e| format!("Elevated install failed: {}", e))?; + + if !elevated.status.success() { + let stderr = String::from_utf8_lossy(&elevated.stderr); + return Err(format!("Package installation failed: {}", stderr)); + } + + info!("[install] Elevated package install succeeded"); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn repair_install_mode_uses_repair_elevation_event() { + assert_eq!( + InstallEventMode::Full.needs_elevation_event(), + "install-needs-elevation" + ); + assert_eq!( + InstallEventMode::Repair.needs_elevation_event(), + "repair-needs-elevation" + ); + assert!(!InstallEventMode::Repair.emit_terminal_events()); + } +} diff --git a/studio/src-tauri/src/main.rs b/studio/src-tauri/src/main.rs new file mode 100644 index 0000000000..f7d49cce60 --- /dev/null +++ b/studio/src-tauri/src/main.rs @@ -0,0 +1,189 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +mod commands; +mod desktop_auth; +mod install; +mod preflight; +mod process; +mod update; +mod windows_job; + +use log::info; +use process::new_backend_state; +use simplelog::{ + CombinedLogger, Config, LevelFilter, SharedLogger, TermLogger, TerminalMode, WriteLogger, +}; +use std::fs; +use tauri::menu::{MenuBuilder, MenuItemBuilder}; +use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}; +use tauri::{Emitter, Manager}; + +fn setup_logging() { + let mut loggers: Vec> = vec![]; + + // Always log to stderr for development + loggers.push(TermLogger::new( + LevelFilter::Info, + Config::default(), + TerminalMode::Stderr, + simplelog::ColorChoice::Auto, + )); + + // Try to set up file logging to ~/.unsloth/studio/tauri.log + if let Some(home) = dirs::home_dir() { + let log_dir = home.join(".unsloth").join("studio"); + if fs::create_dir_all(&log_dir).is_ok() { + let log_path = log_dir.join("tauri.log"); + let rotated_path = log_dir.join("tauri.log.1"); + let max_log_bytes = 5 * 1024 * 1024; + if fs::metadata(&log_path) + .map(|meta| meta.len() >= max_log_bytes) + .unwrap_or(false) + { + let _ = fs::remove_file(&rotated_path); + let _ = fs::rename(&log_path, &rotated_path); + } + if let Ok(file) = fs::OpenOptions::new() + .create(true) + .append(true) + .open(&log_path) + { + loggers.push(WriteLogger::new(LevelFilter::Info, Config::default(), file)); + } + } + } + + if !loggers.is_empty() { + let _ = CombinedLogger::init(loggers); + } +} + +fn setup_tray(app: &tauri::App) -> Result<(), Box> { + let open = MenuItemBuilder::with_id("open", "Open Studio").build(app)?; + let toggle = MenuItemBuilder::with_id("toggle", "Start/Stop Server").build(app)?; + let quit = MenuItemBuilder::with_id("quit", "Quit").build(app)?; + let menu = MenuBuilder::new(app) + .items(&[&open, &toggle, &quit]) + .build()?; + + TrayIconBuilder::new() + .menu(&menu) + .tooltip("Unsloth Studio (Desktop)") + .icon(app.default_window_icon().unwrap().clone()) + .on_menu_event(move |app, event| match event.id().as_ref() { + "open" => { + if let Some(window) = app.get_webview_window("main") { + let _ = window.show(); + let _ = window.set_focus(); + } + } + "toggle" => { + let _ = app.emit("tray-toggle-server", ()); + } + "quit" => { + let install_state = app.state::(); + let _ = crate::install::stop_install(&install_state); + let update_state = app.state::(); + let _ = crate::update::stop_update(&update_state); + // Detach the 5s graceful-wait so the tray click does not + // block the Tauri main loop. Exit runs the RunEvent::Exit + // safety net which also calls stop_backend synchronously. + let shutdown = app.state::().inner().clone(); + let backend_state = app.state::().inner().clone(); + crate::process::stop_backend_detached(backend_state, shutdown); + app.exit(0); + } + _ => {} + }) + .on_tray_icon_event(|tray, event| { + if let TrayIconEvent::Click { + button: MouseButton::Left, + button_state: MouseButtonState::Up, + .. + } = event + { + if let Some(window) = tray.app_handle().get_webview_window("main") { + let _ = window.show(); + let _ = window.set_focus(); + } + } + }) + .build(app)?; + + Ok(()) +} + +fn main() { + // Fix PATH for GUI apps (macOS .app bundles, Linux AppImage, Windows) + // GUI apps don't inherit shell dotfile PATH — this spawns the user's + // login shell to source .zshrc/.bashrc/.profile and sets PATH properly. + let _ = fix_path_env::fix(); + + setup_logging(); + info!("Unsloth Studio desktop app starting"); + windows_job::initialize(); + + tauri::Builder::default() + .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| { + if let Some(window) = app.get_webview_window("main") { + let _ = window.show(); + let _ = window.set_focus(); + } + })) + .plugin(tauri_plugin_process::init()) + .plugin(tauri_plugin_opener::init()) + .plugin(tauri_plugin_updater::Builder::new().build()) + .manage(install::new_install_state()) + .manage(new_backend_state()) + .manage(process::new_shutdown_flag()) + .manage(update::new_update_state()) + .invoke_handler(tauri::generate_handler![ + commands::check_install_status, + commands::desktop_preflight, + commands::start_install, + commands::start_server, + commands::start_managed_server, + commands::stop_server, + commands::check_health, + commands::get_server_logs, + commands::open_logs_dir, + commands::start_backend_update, + commands::start_managed_repair, + commands::install_system_packages, + desktop_auth::desktop_auth, + ]) + .setup(|app| { + setup_tray(app)?; + Ok(()) + }) + .on_window_event(|window, event| { + if let tauri::WindowEvent::CloseRequested { api, .. } = event { + // Hide window instead of closing — this is a tray app. + // Processes keep running so the backend stays available. + // Full cleanup happens via: + // - Tray "Quit" menu item (explicit user action) + // - RunEvent::Exit handler (OS shutdown, SIGTERM, etc.) + let _ = window.hide(); + api.prevent_close(); + } + }) + .build(tauri::generate_context!()) + .expect("error while building tauri application") + .run(|app, event| { + if let tauri::RunEvent::Exit = event { + // Cleanup on ALL exit paths — safety net for non-tray exits + if let Some(install_state) = app.try_state::() { + let _ = install::stop_install(&install_state); + } + if let Some(update_state) = app.try_state::() { + let _ = update::stop_update(&update_state); + } + if let Some(backend_state) = app.try_state::() { + let shutdown = app + .try_state::() + .expect("ShutdownFlag must be managed"); + let _ = process::stop_backend(&backend_state, &shutdown); + } + } + }); +} diff --git a/studio/src-tauri/src/preflight.rs b/studio/src-tauri/src/preflight.rs new file mode 100644 index 0000000000..d3df06d057 --- /dev/null +++ b/studio/src-tauri/src/preflight.rs @@ -0,0 +1,831 @@ +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use std::process::Stdio; +use std::time::Duration; +use tokio::io::AsyncReadExt; +use tokio::process::Command; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum DesktopPreflightDisposition { + NotInstalled, + ManagedReady, + ManagedStale, + AttachedReady, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct DesktopPreflightResult { + pub disposition: DesktopPreflightDisposition, + pub reason: Option, + pub port: Option, + pub can_auto_repair: bool, + pub managed_bin: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum ManagedProbe { + Missing, + Ready { bin: PathBuf }, + Stale { bin: PathBuf, reason: String }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum BackendProbe { + Missing, + Ready { port: u16 }, + Old { port: u16, reason: String }, +} + +#[derive(Debug, Deserialize)] +struct DesktopCapability { + desktop_protocol_version: Option, + supports_api_only: Option, + supports_provision_desktop_auth: Option, + desktop_auth_stale_reason: Option, +} + +#[derive(Debug)] +struct BackendHealth { + desktop_protocol_version: Option, + supports_desktop_auth: Option, + stale_reason: Option, +} + +fn release_auto_repair() -> bool { + !cfg!(debug_assertions) +} + +fn choose_preflight(managed: ManagedProbe, backend: BackendProbe) -> DesktopPreflightResult { + match (backend, managed) { + (BackendProbe::Ready { port }, ManagedProbe::Ready { bin }) => DesktopPreflightResult { + disposition: DesktopPreflightDisposition::AttachedReady, + reason: None, + port: Some(port), + can_auto_repair: false, + managed_bin: Some(bin), + }, + (_, managed) => match managed { + ManagedProbe::Ready { bin } => DesktopPreflightResult { + disposition: DesktopPreflightDisposition::ManagedReady, + reason: None, + port: None, + can_auto_repair: false, + managed_bin: Some(bin), + }, + ManagedProbe::Stale { bin, reason } => DesktopPreflightResult { + disposition: DesktopPreflightDisposition::ManagedStale, + reason: Some(reason), + port: None, + can_auto_repair: release_auto_repair(), + managed_bin: Some(bin), + }, + ManagedProbe::Missing => DesktopPreflightResult { + disposition: DesktopPreflightDisposition::NotInstalled, + reason: None, + port: None, + can_auto_repair: false, + managed_bin: None, + }, + }, + } +} + +async fn run_cli_probe(bin: &std::path::Path, args: &[&str]) -> bool { + let mut cmd = Command::new(bin); + cmd.args(args).stdout(Stdio::null()).stderr(Stdio::null()); + + #[cfg(target_os = "linux")] + if std::env::var_os("APPIMAGE").is_some() { + cmd.env_remove("LD_LIBRARY_PATH"); + cmd.env_remove("PYTHONHOME"); + cmd.env_remove("PYTHONPATH"); + } + + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + cmd.creation_flags(crate::process::CREATE_NO_WINDOW); + } + + let Ok(mut child) = cmd.spawn() else { + return false; + }; + + match tokio::time::timeout(Duration::from_secs(10), child.wait()).await { + Ok(Ok(status)) => status.success(), + _ => { + let _ = child.kill().await; + let _ = child.wait().await; + false + } + } +} + +async fn probe_cli_capability(bin: &std::path::Path) -> Option { + let mut cmd = Command::new(bin); + cmd.args(["studio", "desktop-capabilities", "--json"]) + .stdout(Stdio::piped()) + .stderr(Stdio::null()); + + #[cfg(target_os = "linux")] + if std::env::var_os("APPIMAGE").is_some() { + cmd.env_remove("LD_LIBRARY_PATH"); + cmd.env_remove("PYTHONHOME"); + cmd.env_remove("PYTHONPATH"); + } + + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + cmd.creation_flags(crate::process::CREATE_NO_WINDOW); + } + + let Ok(mut child) = cmd.spawn() else { + return None; + }; + let Some(mut stdout) = child.stdout.take() else { + return None; + }; + + match tokio::time::timeout(Duration::from_secs(10), child.wait()).await { + Ok(Ok(status)) if status.success() => {} + Err(_) => { + let _ = child.kill().await; + let _ = child.wait().await; + return None; + } + _ => return None, + } + + let mut output = Vec::new(); + if stdout.read_to_end(&mut output).await.is_err() { + return None; + } + + serde_json::from_slice::(&output).ok() +} + +fn desktop_capability_ready(capability: &DesktopCapability) -> bool { + capability.desktop_protocol_version == Some(1) + && capability.supports_api_only == Some(true) + && capability.supports_provision_desktop_auth == Some(true) +} + +fn desktop_capability_stale_reason(capability: &DesktopCapability) -> String { + capability + .desktop_auth_stale_reason + .clone() + .unwrap_or_else(|| "desktop_capability_incompatible".to_string()) +} + +async fn probe_managed_bin(bin: PathBuf) -> ManagedProbe { + if !run_cli_probe(&bin, &["-h"]).await { + return ManagedProbe::Stale { + bin, + reason: "cli_unusable".to_string(), + }; + } + + let capability = probe_cli_capability(&bin).await; + if let Some(capability) = capability { + if desktop_capability_ready(&capability) { + return ManagedProbe::Ready { bin }; + } + return ManagedProbe::Stale { + bin, + reason: desktop_capability_stale_reason(&capability), + }; + } + + ManagedProbe::Stale { + bin, + reason: "desktop_capability_probe_failed".to_string(), + } +} + +async fn probe_managed_install() -> ManagedProbe { + match crate::process::find_unsloth_binary() { + Some(bin) => probe_managed_bin(bin).await, + None => ManagedProbe::Missing, + } +} + +pub async fn managed_install_ready() -> bool { + matches!(probe_managed_install().await, ManagedProbe::Ready { .. }) +} + +async fn backend_health(client: &reqwest::Client, port: u16) -> Option { + let url = format!("http://127.0.0.1:{port}/api/health"); + let response = client.get(url).send().await.ok()?; + if !response.status().is_success() { + return None; + } + let json = response.json::().await.ok()?; + let healthy = json + .get("status") + .and_then(|v| v.as_str()) + .map(|s| s == "healthy") + .unwrap_or(false); + let service = json + .get("service") + .and_then(|v| v.as_str()) + .map(|s| s == "Unsloth UI Backend") + .unwrap_or(false); + if !healthy || !service { + return None; + } + + let desktop_protocol_version = json + .get("desktop_protocol_version") + .and_then(|v| v.as_u64()) + .and_then(|v| u16::try_from(v).ok()); + let supports_desktop_auth = json.get("supports_desktop_auth").and_then(|v| v.as_bool()); + if desktop_protocol_version.is_none() && supports_desktop_auth.is_none() { + return None; + } + let stale_reason = match supports_desktop_auth { + Some(false) => json + .get("desktop_auth_stale_reason") + .and_then(|v| v.as_str()) + .map(ToOwned::to_owned), + _ => None, + }; + Some(BackendHealth { + desktop_protocol_version, + supports_desktop_auth, + stale_reason, + }) +} + +fn backend_capability_stale_reason(health: &BackendHealth) -> Option { + if health.desktop_protocol_version != Some(1) { + return health + .stale_reason + .clone() + .or_else(|| Some("desktop_protocol_incompatible".to_string())); + } + if health.supports_desktop_auth != Some(true) { + return health + .stale_reason + .clone() + .or_else(|| Some("desktop_auth_unsupported".to_string())); + } + None +} + +#[derive(Serialize)] +struct DesktopLoginProbe<'a> { + secret: &'a str, +} + +async fn backend_desktop_auth_status( + client: &reqwest::Client, + port: u16, + health: &BackendHealth, +) -> BackendProbe { + if let Some(reason) = backend_capability_stale_reason(health) { + return BackendProbe::Old { port, reason }; + } + + let url = format!("http://127.0.0.1:{port}/api/auth/desktop-login"); + let response = client + .post(url) + .json(&DesktopLoginProbe { + secret: "desktop-preflight-invalid-secret", + }) + .send() + .await; + + let Ok(response) = response else { + return BackendProbe::Old { + port, + reason: backend_capability_stale_reason(health) + .unwrap_or_else(|| "desktop_login_probe_failed".to_string()), + }; + }; + + match response.status() { + reqwest::StatusCode::UNAUTHORIZED => BackendProbe::Ready { port }, + reqwest::StatusCode::NOT_FOUND => BackendProbe::Old { + port, + reason: "desktop_login_not_found".to_string(), + }, + _ => BackendProbe::Old { + port, + reason: backend_capability_stale_reason(health) + .unwrap_or_else(|| "desktop_login_probe_failed".to_string()), + }, + } +} + +async fn probe_existing_backends() -> BackendProbe { + let client = match reqwest::Client::builder() + .timeout(Duration::from_secs(2)) + .build() + { + Ok(client) => client, + Err(_) => return BackendProbe::Missing, + }; + + // Fan out health probes concurrently. The desktop-auth probe is still + // sequential per candidate because it has auth-log side effects. + let ports: Vec = (8888u16..=8908).collect(); + let mut health_futs = Vec::with_capacity(ports.len()); + for port in ports { + // why: reqwest::Client is internally Arc-wrapped; clone is a refcount bump + // (documented cheap). tokio::spawn needs 'static, so each task owns its own clone. + let c = client.clone(); + health_futs.push(tokio::spawn(async move { + backend_health(&c, port).await.map(|h| (port, h)) + })); + } + + let mut candidates: Vec<(u16, BackendHealth)> = Vec::new(); + for fut in health_futs { + if let Ok(Some(pair)) = fut.await { + candidates.push(pair); + } + } + + let mut first_old = None; + for (port, health) in candidates { + match backend_desktop_auth_status(&client, port, &health).await { + ready @ BackendProbe::Ready { .. } => return ready, + old @ BackendProbe::Old { .. } if first_old.is_none() => first_old = Some(old), + _ => {} + } + } + + first_old.unwrap_or(BackendProbe::Missing) +} + +pub async fn desktop_preflight_result() -> DesktopPreflightResult { + let (managed, backend) = tokio::join!(probe_managed_install(), probe_existing_backends()); + choose_preflight(managed, backend) +} + +#[cfg(test)] +mod tests { + use super::*; + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + use tokio::net::TcpListener; + + #[test] + fn compatible_backend_does_not_win_over_stale_managed_install() { + let result = choose_preflight( + ManagedProbe::Stale { + bin: PathBuf::from("/managed/unsloth"), + reason: "old cli".to_string(), + }, + BackendProbe::Ready { port: 8000 }, + ); + + assert_eq!( + result.disposition, + DesktopPreflightDisposition::ManagedStale + ); + assert_eq!(result.port, None); + assert_eq!(result.reason, Some("old cli".to_string())); + assert_eq!(result.can_auto_repair, release_auto_repair()); + assert_eq!(result.managed_bin, Some(PathBuf::from("/managed/unsloth"))); + } + + #[test] + fn compatible_backend_wins_over_ready_managed_install() { + let result = choose_preflight( + ManagedProbe::Ready { + bin: PathBuf::from("/managed/unsloth"), + }, + BackendProbe::Ready { port: 8000 }, + ); + + assert_eq!( + result.disposition, + DesktopPreflightDisposition::AttachedReady + ); + assert_eq!(result.port, Some(8000)); + assert_eq!(result.managed_bin, Some(PathBuf::from("/managed/unsloth"))); + assert!(!result.can_auto_repair); + } + + #[test] + fn compatible_backend_does_not_win_over_missing_managed_install() { + let result = choose_preflight(ManagedProbe::Missing, BackendProbe::Ready { port: 8000 }); + + assert_eq!( + result.disposition, + DesktopPreflightDisposition::NotInstalled + ); + assert_eq!(result.port, None); + assert_eq!(result.managed_bin, None); + assert!(!result.can_auto_repair); + } + + #[test] + fn old_backend_falls_back_to_ready_managed_install() { + let result = choose_preflight( + ManagedProbe::Ready { + bin: PathBuf::from("/managed/unsloth"), + }, + BackendProbe::Old { + port: 8001, + reason: "missing endpoint".to_string(), + }, + ); + + assert_eq!( + result.disposition, + DesktopPreflightDisposition::ManagedReady + ); + assert_eq!(result.reason, None); + assert_eq!(result.port, None); + assert_eq!(result.managed_bin, Some(PathBuf::from("/managed/unsloth"))); + assert!(!result.can_auto_repair); + } + + #[test] + fn old_backend_falls_back_to_stale_managed_install() { + let result = choose_preflight( + ManagedProbe::Stale { + bin: PathBuf::from("/managed/unsloth"), + reason: "old cli".to_string(), + }, + BackendProbe::Old { + port: 8001, + reason: "missing endpoint".to_string(), + }, + ); + + assert_eq!( + result.disposition, + DesktopPreflightDisposition::ManagedStale + ); + assert_eq!(result.reason, Some("old cli".to_string())); + assert_eq!(result.port, None); + assert_eq!(result.can_auto_repair, release_auto_repair()); + assert_eq!(result.managed_bin, Some(PathBuf::from("/managed/unsloth"))); + } + + #[test] + fn managed_ready_when_no_backend() { + let result = choose_preflight( + ManagedProbe::Ready { + bin: PathBuf::from("/managed/unsloth"), + }, + BackendProbe::Missing, + ); + + assert_eq!( + result.disposition, + DesktopPreflightDisposition::ManagedReady + ); + assert_eq!(result.managed_bin, Some(PathBuf::from("/managed/unsloth"))); + assert!(!result.can_auto_repair); + } + + #[test] + fn managed_stale_when_no_backend() { + let result = choose_preflight( + ManagedProbe::Stale { + bin: PathBuf::from("/managed/unsloth"), + reason: "old cli".to_string(), + }, + BackendProbe::Missing, + ); + + assert_eq!( + result.disposition, + DesktopPreflightDisposition::ManagedStale + ); + assert_eq!(result.reason, Some("old cli".to_string())); + assert_eq!(result.can_auto_repair, release_auto_repair()); + } + + #[test] + fn not_installed_when_no_backend_no_managed_binary() { + let result = choose_preflight(ManagedProbe::Missing, BackendProbe::Missing); + + assert_eq!( + result.disposition, + DesktopPreflightDisposition::NotInstalled + ); + assert_eq!(result.managed_bin, None); + assert!(!result.can_auto_repair); + } + + #[test] + fn old_backend_with_no_managed_install_uses_install_flow() { + let result = choose_preflight( + ManagedProbe::Missing, + BackendProbe::Old { + port: 8002, + reason: "old version".to_string(), + }, + ); + + assert_eq!( + result.disposition, + DesktopPreflightDisposition::NotInstalled + ); + assert_eq!(result.reason, None); + assert_eq!(result.port, None); + assert!(!result.can_auto_repair); + } + + #[cfg(unix)] + struct FakeCli { + bin: PathBuf, + dir: PathBuf, + } + + #[cfg(unix)] + impl Drop for FakeCli { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.dir); + } + } + + #[cfg(unix)] + fn fake_cli(test_name: &str, script: &str) -> FakeCli { + use std::fs; + use std::os::unix::fs::PermissionsExt; + use std::time::{SystemTime, UNIX_EPOCH}; + + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let dir = std::env::temp_dir().join(format!( + "unsloth-preflight-{test_name}-{}-{nanos}", + std::process::id() + )); + fs::create_dir_all(&dir).unwrap(); + let bin = dir.join("unsloth"); + fs::write(&bin, script).unwrap(); + let mut perms = fs::metadata(&bin).unwrap().permissions(); + perms.set_mode(0o755); + fs::set_permissions(&bin, perms).unwrap(); + FakeCli { bin, dir } + } + + #[cfg(unix)] + #[tokio::test] + async fn managed_cli_stale_when_desktop_capabilities_missing() { + let fake = fake_cli( + "cap-missing", + r#"#!/bin/sh +if [ "$1" = "-h" ]; then exit 0; fi +if [ "$1" = "studio" ] && [ "$2" = "provision-desktop-auth" ] && [ "$3" = "--help" ]; then exit 0; fi +exit 1 +"#, + ); + let bin = fake.bin.clone(); + + assert!(matches!( + probe_managed_bin(bin.clone()).await, + ManagedProbe::Stale { bin: actual_bin, .. } if actual_bin == bin + )); + } + + #[cfg(unix)] + #[tokio::test] + async fn managed_cli_stale_when_desktop_capabilities_command_missing() { + let fake = fake_cli( + "missing-helper", + r#"#!/bin/sh +if [ "$1" = "-h" ]; then exit 0; fi +exit 1 +"#, + ); + let bin = fake.bin.clone(); + + assert!(matches!( + probe_managed_bin(bin.clone()).await, + ManagedProbe::Stale { bin: actual_bin, .. } if actual_bin == bin + )); + } + + #[cfg(unix)] + #[tokio::test] + async fn managed_cli_stale_when_help_broken() { + let fake = fake_cli( + "broken-help", + r#"#!/bin/sh +exit 1 +"#, + ); + let bin = fake.bin.clone(); + + assert_eq!( + probe_managed_bin(bin.clone()).await, + ManagedProbe::Stale { + bin, + reason: "cli_unusable".to_string() + } + ); + } + + #[cfg(unix)] + #[tokio::test] + async fn managed_cli_ready_when_desktop_capabilities_compatible() { + let fake = fake_cli( + "cap-true-helper-missing", + r#"#!/bin/sh +if [ "$1" = "-h" ]; then exit 0; fi +if [ "$1" = "studio" ] && [ "$2" = "desktop-capabilities" ] && [ "$3" = "--json" ]; then + printf '{"desktop_protocol_version":1,"supports_api_only":true,"supports_provision_desktop_auth":true}' + exit 0 +fi +exit 1 +"#, + ); + let bin = fake.bin.clone(); + + assert_eq!( + probe_managed_bin(bin.clone()).await, + ManagedProbe::Ready { bin } + ); + } + + #[cfg(unix)] + #[tokio::test] + async fn capability_false_reason_used_when_legacy_helper_missing() { + let fake = fake_cli( + "cap-false-helper-missing", + r#"#!/bin/sh +if [ "$1" = "-h" ]; then exit 0; fi +if [ "$1" = "studio" ] && [ "$2" = "desktop-capabilities" ] && [ "$3" = "--json" ]; then + printf '{"desktop_protocol_version":1,"supports_api_only":true,"supports_provision_desktop_auth":false,"desktop_auth_stale_reason":"cap_false"}' + exit 0 +fi +exit 1 +"#, + ); + let bin = fake.bin.clone(); + + assert_eq!( + probe_managed_bin(bin.clone()).await, + ManagedProbe::Stale { + bin, + reason: "cap_false".to_string() + } + ); + } + + #[cfg(unix)] + #[tokio::test] + async fn capability_false_overrides_working_legacy_helper() { + let fake = fake_cli( + "cap-false-helper-ready", + r#"#!/bin/sh +if [ "$1" = "-h" ]; then exit 0; fi +if [ "$1" = "studio" ] && [ "$2" = "desktop-capabilities" ] && [ "$3" = "--json" ]; then + printf '{"desktop_protocol_version":1,"supports_api_only":true,"supports_provision_desktop_auth":false,"desktop_auth_stale_reason":"cap_false"}' + exit 0 +fi +if [ "$1" = "studio" ] && [ "$2" = "provision-desktop-auth" ] && [ "$3" = "--help" ]; then exit 0; fi +exit 1 +"#, + ); + let bin = fake.bin.clone(); + + assert_eq!( + probe_managed_bin(bin.clone()).await, + ManagedProbe::Stale { + bin, + reason: "cap_false".to_string() + } + ); + } + + async fn backend_server(health_body: &'static str, route_status: &'static str) -> u16 { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let port = listener.local_addr().unwrap().port(); + + tokio::spawn(async move { + for _ in 0..2 { + let (mut stream, _) = listener.accept().await.unwrap(); + let mut buffer = [0; 2048]; + let n = stream.read(&mut buffer).await.unwrap(); + let request = String::from_utf8_lossy(&buffer[..n]); + let (status, body) = if request.starts_with("GET /api/health ") { + ("200 OK", health_body) + } else if request.starts_with("POST /api/auth/desktop-login ") { + (route_status, "") + } else { + ("404 Not Found", "") + }; + let response = format!( + "HTTP/1.1 {status}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", + body.len() + ); + stream.write_all(response.as_bytes()).await.unwrap(); + } + }); + + port + } + + async fn probe_test_backend( + health_body: &'static str, + route_status: &'static str, + ) -> BackendProbe { + let port = backend_server(health_body, route_status).await; + let client = reqwest::Client::new(); + let health = backend_health(&client, port).await.unwrap(); + backend_desktop_auth_status(&client, port, &health).await + } + + #[tokio::test] + async fn backend_health_without_desktop_capability_fields_is_not_compatible() { + let port = backend_server( + r#"{"status":"healthy","service":"Unsloth UI Backend"}"#, + "401 Unauthorized", + ) + .await; + let client = reqwest::Client::new(); + + assert!(backend_health(&client, port).await.is_none()); + } + + #[tokio::test] + async fn backend_with_auth_support_but_missing_protocol_is_old() { + let probe = probe_test_backend( + r#"{"status":"healthy","service":"Unsloth UI Backend","supports_desktop_auth":true}"#, + "401 Unauthorized", + ) + .await; + + assert!(matches!(probe, BackendProbe::Old { .. })); + } + + #[tokio::test] + async fn backend_with_auth_support_but_unsupported_protocol_is_old() { + let probe = probe_test_backend( + r#"{"status":"healthy","service":"Unsloth UI Backend","desktop_protocol_version":2,"supports_desktop_auth":true}"#, + "401 Unauthorized", + ) + .await; + + assert!(matches!(probe, BackendProbe::Old { .. })); + } + + #[tokio::test] + async fn backend_health_with_desktop_capability_fields_and_401_is_ready() { + let probe = probe_test_backend( + r#"{"status":"healthy","service":"Unsloth UI Backend","desktop_protocol_version":1,"supports_desktop_auth":true}"#, + "401 Unauthorized", + ) + .await; + + assert!(matches!(probe, BackendProbe::Ready { .. })); + } + + #[tokio::test] + async fn backend_route_404_is_old() { + let probe = probe_test_backend( + r#"{"status":"healthy","service":"Unsloth UI Backend","desktop_protocol_version":1,"supports_desktop_auth":true}"#, + "404 Not Found", + ) + .await; + + assert!(matches!( + probe, + BackendProbe::Old { + reason, + .. + } if reason == "desktop_login_not_found" + )); + } + + #[tokio::test] + async fn backend_route_500_is_old() { + let probe = probe_test_backend( + r#"{"status":"healthy","service":"Unsloth UI Backend","desktop_protocol_version":1,"supports_desktop_auth":true}"#, + "500 Internal Server Error", + ) + .await; + + assert!(matches!(probe, BackendProbe::Old { .. })); + } + + #[tokio::test] + async fn backend_capability_false_is_old_even_when_route_401() { + let port = backend_server( + r#"{"status":"healthy","service":"Unsloth UI Backend","desktop_protocol_version":1,"supports_desktop_auth":false,"desktop_auth_stale_reason":"cap_false"}"#, + "401 Unauthorized", + ) + .await; + let client = reqwest::Client::new(); + let health = backend_health(&client, port).await.unwrap(); + + assert!(matches!( + backend_desktop_auth_status(&client, port, &health).await, + BackendProbe::Old { + reason, + .. + } if reason == "cap_false" + )); + } +} diff --git a/studio/src-tauri/src/process.rs b/studio/src-tauri/src/process.rs new file mode 100644 index 0000000000..beba8c1208 --- /dev/null +++ b/studio/src-tauri/src/process.rs @@ -0,0 +1,537 @@ +use log::{error, info, warn}; +use process_wrap::std::*; +use regex::Regex; +use std::collections::VecDeque; +use std::io::BufRead; +use std::process::{Command, Stdio}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; +use tauri::{AppHandle, Emitter}; + +const MAX_LOG_LINES: usize = 1000; + +pub struct BackendProcess { + pub child: Option>, + pub port: Option, + pub logs: VecDeque, + pub intentional_stop: bool, +} + +impl Default for BackendProcess { + fn default() -> Self { + Self { + child: None, + port: None, + logs: VecDeque::with_capacity(MAX_LOG_LINES), + intentional_stop: false, + } + } +} + +pub type BackendState = Arc>; +pub type ShutdownFlag = Arc; + +pub fn new_backend_state() -> BackendState { + Arc::new(Mutex::new(BackendProcess::default())) +} + +pub fn new_shutdown_flag() -> ShutdownFlag { + Arc::new(AtomicBool::new(false)) +} + +pub(crate) fn trim_line_endings(bytes: &[u8]) -> &[u8] { + let mut end = bytes.len(); + while end > 0 && matches!(bytes[end - 1], b'\n' | b'\r') { + end -= 1; + } + &bytes[..end] +} + +/// Windows `CREATE_NO_WINDOW` flag — suppresses console windows for child processes. +#[cfg(windows)] +pub(crate) const CREATE_NO_WINDOW: u32 = 0x08000000; + +/// Force-kill a Windows process tree via hidden `taskkill /T /F`, falling +/// back to `child.kill()` if taskkill itself fails. Reaps the child afterward. +#[cfg(windows)] +pub(crate) fn force_kill_process_tree( + pid: u32, + child: &mut Box, + label: &str, +) { + use std::os::windows::process::CommandExt; + + let taskkill_status = Command::new("taskkill.exe") + .creation_flags(CREATE_NO_WINDOW) + .args(["/PID", &pid.to_string(), "/T", "/F"]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + + match taskkill_status { + Ok(status) if status.success() => {} + Ok(status) => { + warn!( + "taskkill returned non-zero status for {} pid {}: {}", + label, pid, status + ); + let _ = child.kill(); + } + Err(e) => { + warn!("taskkill failed for {} pid {}: {}", label, pid, e); + let _ = child.kill(); + } + } + + let _ = child.wait(); + info!("{} process tree force stopped", label); +} + +/// Returns the path to the unsloth binary inside the managed venv, if it exists. +/// Checks the new layout (~/.unsloth/studio/unsloth_studio/) first, +/// then falls back to the old layout (~/.unsloth/studio/.venv/) for compat. +fn find_unsloth_binary_in_studio_dir(studio: &std::path::Path) -> Option { + // New layout (upstream scripts >= March 2026) + let new_base = studio.join("unsloth_studio"); + // Old layout (bundled scripts, older upstream) + let old_base = studio.join(".venv"); + + for base in [new_base, old_base] { + #[cfg(unix)] + let bin = base.join("bin").join("unsloth"); + #[cfg(windows)] + let bin = base.join("Scripts").join("unsloth.exe"); + + if bin.exists() { + return Some(bin); + } + } + + None +} + +pub fn find_unsloth_binary() -> Option { + let home = dirs::home_dir()?; + let studio = home.join(".unsloth").join("studio"); + + find_unsloth_binary_in_studio_dir(&studio) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use std::path::PathBuf; + use std::time::{SystemTime, UNIX_EPOCH}; + + fn temp_studio_dir(test_name: &str) -> PathBuf { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let dir = std::env::temp_dir().join(format!( + "unsloth-{test_name}-{}-{nanos}", + std::process::id() + )); + fs::create_dir_all(&dir).unwrap(); + dir + } + + #[test] + fn finds_new_layout_before_legacy_layout() { + let temp = temp_studio_dir("new-before-legacy"); + + #[cfg(unix)] + let new_bin = temp.join("unsloth_studio/bin/unsloth"); + #[cfg(unix)] + let old_bin = temp.join(".venv/bin/unsloth"); + #[cfg(windows)] + let new_bin = temp.join("unsloth_studio/Scripts/unsloth.exe"); + #[cfg(windows)] + let old_bin = temp.join(".venv/Scripts/unsloth.exe"); + + fs::create_dir_all(new_bin.parent().unwrap()).unwrap(); + fs::create_dir_all(old_bin.parent().unwrap()).unwrap(); + fs::write(&new_bin, "").unwrap(); + fs::write(&old_bin, "").unwrap(); + + assert_eq!(find_unsloth_binary_in_studio_dir(&temp), Some(new_bin)); + fs::remove_dir_all(temp).unwrap(); + } + + #[test] + fn finds_legacy_layout_when_new_missing() { + let temp = temp_studio_dir("legacy"); + + #[cfg(unix)] + let old_bin = temp.join(".venv/bin/unsloth"); + #[cfg(windows)] + let old_bin = temp.join(".venv/Scripts/unsloth.exe"); + + fs::create_dir_all(old_bin.parent().unwrap()).unwrap(); + fs::write(&old_bin, "").unwrap(); + + assert_eq!(find_unsloth_binary_in_studio_dir(&temp), Some(old_bin)); + fs::remove_dir_all(temp).unwrap(); + } + + #[test] + fn returns_none_when_no_managed_layout_exists() { + let temp = temp_studio_dir("none"); + + assert_eq!(find_unsloth_binary_in_studio_dir(&temp), None); + fs::remove_dir_all(temp).unwrap(); + } + + #[test] + fn backend_args_always_enable_api_only() { + assert_eq!( + backend_args(8888), + vec!["studio", "--api-only", "-H", "127.0.0.1", "-p", "8888"] + ); + } +} + +/// Find the unsloth binary, preferring the dev repo if available. +/// In dev mode (debug builds), checks for a local .venv in the repo first. +/// Falls back to find_unsloth_binary() which checks ~/.unsloth/studio/unsloth_studio/ +/// (new layout) then ~/.unsloth/studio/.venv/ (old layout). +pub(crate) fn resolve_backend_binary() -> Result { + // In dev mode, check for local repo venv first + #[cfg(debug_assertions)] + { + // CARGO_MANIFEST_DIR is set at compile time to studio/src-tauri/ + // Repo root is 2 levels up: studio/src-tauri -> studio -> repo_root + let manifest_dir = env!("CARGO_MANIFEST_DIR"); + let repo_root = std::path::Path::new(manifest_dir) + .parent() // studio/ + .and_then(|p| p.parent()); // repo_root/ + + if let Some(root) = repo_root { + #[cfg(unix)] + let dev_bin = root.join(".venv/bin/unsloth"); + #[cfg(windows)] + let dev_bin = root.join(".venv/Scripts/unsloth.exe"); + + if dev_bin.exists() { + info!("Dev mode: using local repo backend at {:?}", dev_bin); + return Ok(dev_bin.to_path_buf()); + } + } + info!("Dev mode: no local .venv found, falling back to installed backend"); + } + + find_unsloth_binary() + .ok_or_else(|| "Unsloth binary not found. Please install Unsloth Studio first.".to_string()) +} + +fn backend_args(port: u16) -> Vec { + [ + "studio", + "--api-only", + "-H", + "127.0.0.1", + "-p", + &port.to_string(), + ] + .into_iter() + .map(String::from) + .collect() +} + +/// Spawn the backend process and wire up stdout/stderr reader threads. +pub fn start_backend( + app: &AppHandle, + state: &BackendState, + port: u16, + shutdown: &ShutdownFlag, +) -> Result<(), String> { + let bin = resolve_backend_binary()?; + + shutdown.store(false, Ordering::SeqCst); + + // Reset state + { + let mut proc = state.lock().map_err(|e| e.to_string())?; + if proc.child.is_some() { + return Err("Backend is already running.".to_string()); + } + proc.port = None; + proc.logs.clear(); + proc.intentional_stop = false; + } + + let args = backend_args(port); + info!("Starting backend: {:?} {}", bin, args.join(" ")); + + let mut cmd = Command::new(&bin); + cmd.args(&args) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + // AppImage sets LD_LIBRARY_PATH to its bundled libs, which breaks the spawned + // Python process (wrong libpython/libz → "No module named encodings"). + // Only clear when running inside an AppImage — native .deb/.rpm installs may + // need these env vars for custom CUDA or conda paths. + #[cfg(target_os = "linux")] + if std::env::var_os("APPIMAGE").is_some() { + cmd.env_remove("LD_LIBRARY_PATH"); + cmd.env_remove("PYTHONHOME"); + cmd.env_remove("PYTHONPATH"); + } + + // On Windows, launch the backend directly with hidden-window flags. + // The app process is assigned to a KILL_ON_JOB_CLOSE job in main.rs, so + // children inherit crash-safe cleanup without the buggy per-child JobObject wrapper. + #[cfg(windows)] + let mut child: Box = { + use std::os::windows::process::CommandExt; + + const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200; + cmd.creation_flags(CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW); + let child = cmd + .spawn() + .map_err(|e| format!("Failed to spawn backend: {}", e))?; + Box::new(child) + }; + + #[cfg(unix)] + let mut child: Box = { + // Keep the backend tree in a process group on Unix for cleanup. + let mut wrap = CommandWrap::from(cmd); + wrap.wrap(ProcessGroup::leader()); + wrap.spawn() + .map_err(|e| format!("Failed to spawn backend: {}", e))? + }; + + let stdout = child.stdout().take(); + let stderr = child.stderr().take(); + + // Store child in state + { + let mut proc = state.lock().map_err(|e| e.to_string())?; + proc.child = Some(child); + } + + // Spawn stdout reader thread + if let Some(stdout) = stdout { + let app_handle = app.clone(); + let state_clone = Arc::clone(state); + std::thread::spawn(move || { + read_output_stream(stdout, &app_handle, &state_clone, false); + }); + } + + // Spawn stderr reader thread + if let Some(stderr) = stderr { + let app_handle = app.clone(); + let state_clone = Arc::clone(state); + std::thread::spawn(move || { + read_output_stream(stderr, &app_handle, &state_clone, true); + }); + } + + Ok(()) +} + +/// Read lines from a child process stream (stdout or stderr). +/// For stdout, parse TAURI_PORT=(\d+) to detect the actual port. +/// When stdout closes and the stop was not intentional, emit server-crashed. +fn read_output_stream( + stream: R, + app: &AppHandle, + state: &BackendState, + is_stderr: bool, +) { + let mut reader = std::io::BufReader::new(stream); + let port_re = Regex::new(r"TAURI_PORT=(\d+)").unwrap(); + let mut buf = Vec::new(); + + loop { + buf.clear(); + match reader.read_until(b'\n', &mut buf) { + Ok(0) => break, + Ok(_) => { + let text = String::from_utf8_lossy(trim_line_endings(&buf)).into_owned(); + let log_line = if is_stderr { + format!("[stderr] {}", text) + } else { + text.clone() + }; + + // Check for TAURI_PORT on stdout only + if !is_stderr { + if let Some(caps) = port_re.captures(&text) { + if let Some(port_str) = caps.get(1) { + if let Ok(port) = port_str.as_str().parse::() { + info!("Detected backend port: {}", port); + if let Ok(mut proc) = state.lock() { + proc.port = Some(port); + } + let _ = app.emit("server-port", port); + } + } + } + } + + // Buffer the log line + if let Ok(mut proc) = state.lock() { + if proc.logs.len() >= MAX_LOG_LINES { + proc.logs.pop_front(); + } + proc.logs.push_back(log_line.clone()); + } + + info!("[backend] {}", log_line); + + // Emit to frontend + let _ = app.emit("server-log", &log_line); + } + Err(e) => { + warn!( + "Error reading backend {}: {}", + if is_stderr { "stderr" } else { "stdout" }, + e + ); + break; + } + } + } + + // Stream closed. Only the stdout reader checks for crashes. + if !is_stderr { + if let Ok(mut proc) = state.lock() { + let intentional = proc.intentional_stop; + let exited = if let Some(ref mut child) = proc.child { + match child.try_wait() { + Ok(Some(status)) => { + info!("Backend stdout stream ended with status: {}", status); + true + } + Ok(None) => { + warn!("Backend stdout stream ended, but process is still running"); + false + } + Err(e) => { + warn!("Failed to query backend status after stdout closed: {}", e); + false + } + } + } else { + false + }; + + if exited { + proc.child = None; + if !intentional { + error!("Backend process stdout closed unexpectedly (crash detected)"); + let _ = app.emit("server-crashed", ()); + } + } + } + } +} + +/// Graceful shutdown of the backend process and its entire subprocess tree. +/// Unix: SIGTERM to process group -> wait up to 5s -> SIGKILL to group +/// Windows: CTRL_BREAK_EVENT -> wait up to 5s -> hidden taskkill /T /F +pub fn stop_backend(state: &BackendState, shutdown: &ShutdownFlag) -> Result<(), String> { + shutdown.store(true, Ordering::SeqCst); + + // Extract the child and mark intentional stop. + // We take the child OUT of the mutex so we don't hold the lock during the wait loop. + let mut child = { + let mut proc = match state.lock() { + Ok(guard) => guard, + Err(poisoned) => { + warn!("Backend state mutex poisoned, recovering for cleanup"); + poisoned.into_inner() + } + }; + proc.intentional_stop = true; + proc.child.take() + }; + + let Some(ref mut child) = child else { + return Ok(()); // Nothing running + }; + + let pid = child.id(); + info!("Stopping backend process group (pid {})", pid); + + // Send SIGTERM to the entire process group (negative PID = group signal). + // This gives Python and any workers a chance to shut down gracefully. + #[cfg(unix)] + { + if pid > i32::MAX as u32 { + // PID too large for i32 negation, fall back to direct kill + warn!("PID {} exceeds i32 range, using direct kill", pid); + let _ = child.kill(); + let _ = child.wait(); + return Ok(()); + } + unsafe { + libc::kill(-(pid as i32), libc::SIGTERM); + } + } + #[cfg(windows)] + { + unsafe { + windows_sys::Win32::System::Console::GenerateConsoleCtrlEvent( + windows_sys::Win32::System::Console::CTRL_BREAK_EVENT, + pid, + ); + } + } + + // Poll for up to 5 seconds (50 iterations * 100ms) + for _ in 0..50 { + match child.try_wait() { + Ok(Some(status)) => { + info!("Backend exited with status: {}", status); + return Ok(()); + } + Ok(None) => { + std::thread::sleep(std::time::Duration::from_millis(100)); + } + Err(e) => { + warn!("Error polling backend process: {}", e); + break; + } + } + } + + #[cfg(windows)] + { + warn!( + "Backend did not exit gracefully, force killing process tree (pid {})", + pid + ); + force_kill_process_tree(pid, child, "Backend"); + return Ok(()); + } + + #[cfg(unix)] + { + // Force kill the process group on Unix + warn!( + "Backend did not exit gracefully, force killing group (pid {})", + pid + ); + let _ = child.kill(); + + // Reap the process + let _ = child.wait(); + info!("Backend process group forcefully stopped"); + Ok(()) + } +} + +/// Spawn `stop_backend` on a background thread and return immediately. +/// Used by the tray "quit" path so the 5s graceful-wait does not block +/// the Tauri main event loop before `app.exit(0)` fires. +pub fn stop_backend_detached(state: BackendState, shutdown: ShutdownFlag) { + std::thread::spawn(move || { + let _ = stop_backend(&state, &shutdown); + }); +} diff --git a/studio/src-tauri/src/update.rs b/studio/src-tauri/src/update.rs new file mode 100644 index 0000000000..477f18ce28 --- /dev/null +++ b/studio/src-tauri/src/update.rs @@ -0,0 +1,293 @@ +use log::{error, info, warn}; +use process_wrap::std::*; +use std::io::BufRead; +use std::process::{Command, ExitStatus, Stdio}; +use std::sync::{Arc, Mutex}; +use tauri::{AppHandle, Emitter}; + +// ── Types ── + +pub struct UpdateProcess { + pub child: Option>, + pub intentional_stop: bool, +} + +impl Default for UpdateProcess { + fn default() -> Self { + Self { + child: None, + intentional_stop: false, + } + } +} + +pub type UpdateState = Arc>; + +pub fn new_update_state() -> UpdateState { + Arc::new(Mutex::new(UpdateProcess::default())) +} + +// ── Spawn ── + +fn spawn_update( + bin: &std::path::Path, + state: &UpdateState, +) -> Result< + ( + Option, + Option, + ), + String, +> { + let mut update = state.lock().map_err(|e| e.to_string())?; + if update.child.is_some() { + return Err("Update is already running.".to_string()); + } + update.intentional_stop = false; + + let mut cmd = Command::new(bin); + cmd.args(["studio", "update"]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + // AppImage sets LD_LIBRARY_PATH to its bundled libs, which breaks Python + #[cfg(target_os = "linux")] + if std::env::var_os("APPIMAGE").is_some() { + cmd.env_remove("LD_LIBRARY_PATH"); + cmd.env_remove("PYTHONHOME"); + cmd.env_remove("PYTHONPATH"); + } + + #[cfg(windows)] + let mut child: Box = { + use std::os::windows::process::CommandExt; + + cmd.creation_flags(crate::process::CREATE_NO_WINDOW); + let child = cmd + .spawn() + .map_err(|e| format!("Failed to spawn update: {}", e))?; + Box::new(child) + }; + + #[cfg(unix)] + let mut child: Box = { + let mut wrap = CommandWrap::from(cmd); + wrap.wrap(ProcessGroup::leader()); + wrap.spawn() + .map_err(|e| format!("Failed to spawn update: {}", e))? + }; + + let stdout = child.stdout().take(); + let stderr = child.stderr().take(); + update.child = Some(child); + Ok((stdout, stderr)) +} + +// ── Stream ── + +fn stream_output( + app: &AppHandle, + progress_event: &'static str, + stdout: Option, + stderr: Option, +) -> Vec> { + let mut threads = Vec::new(); + + if let Some(out) = stdout { + let app_clone = app.clone(); + threads.push(std::thread::spawn(move || { + let reader = std::io::BufReader::new(out); + for line in reader.lines() { + match line { + Ok(text) => { + info!("[update][stdout] {}", text); + let _ = app_clone.emit(progress_event, &text); + } + Err(e) => { + warn!("[update] Error reading stdout: {}", e); + break; + } + } + } + })); + } + + if let Some(err) = stderr { + let app_clone = app.clone(); + threads.push(std::thread::spawn(move || { + let reader = std::io::BufReader::new(err); + for line in reader.lines() { + match line { + Ok(text) => { + warn!("[update][stderr] {}", text); + let _ = app_clone.emit(progress_event, &text); + } + Err(e) => { + warn!("[update] Error reading stderr: {}", e); + break; + } + } + } + })); + } + + threads +} + +// ── Wait ── + +fn wait_for_exit(state: &UpdateState) -> Result<(ExitStatus, bool), String> { + const MAX_WAIT_ITERATIONS: u32 = 72_000; // 2h at 100ms intervals + for _ in 0..MAX_WAIT_ITERATIONS { + let mut update = state.lock().map_err(|e| e.to_string())?; + let intentional = update.intentional_stop; + + match update.child.as_mut() { + Some(child) => match child.try_wait() { + Ok(Some(status)) => { + update.child = None; + return Ok((status, intentional)); + } + Ok(None) => {} + Err(e) => { + update.child = None; + return Err(format!("Error waiting for update: {}", e)); + } + }, + None if intentional => return Err("Update stopped.".to_string()), + None => return Err("Update process disappeared unexpectedly.".to_string()), + } + + drop(update); + std::thread::sleep(std::time::Duration::from_millis(100)); + } + let _ = stop_update(state); + Err("Update timed out after 2 hours".to_string()) +} + +// ── Public API ── + +pub fn run_backend_update(app: AppHandle, state: UpdateState) -> Result<(), String> { + run_backend_update_with_terminal_events(app, state, true) +} + +pub(crate) fn run_backend_update_for_repair( + app: AppHandle, + state: UpdateState, +) -> Result<(), String> { + run_backend_update_with_terminal_events(app, state, false) +} + +fn run_backend_update_with_terminal_events( + app: AppHandle, + state: UpdateState, + terminal_events: bool, +) -> Result<(), String> { + let bin = crate::process::find_unsloth_binary() + .ok_or("Unsloth binary not found. Cannot run update.")?; + + info!("[update] Starting backend update via {:?}", bin); + let progress_event = if terminal_events { + "update-progress" + } else { + "repair-progress" + }; + let _ = app.emit(progress_event, "Starting backend update..."); + + let (stdout, stderr) = spawn_update(&bin, &state)?; + let threads = stream_output(&app, progress_event, stdout, stderr); + + let result = wait_for_exit(&state); + for handle in threads { + let _ = handle.join(); + } + + match result { + Ok((status, _)) if status.success() => { + info!("[update] Backend update complete"); + if terminal_events { + let _ = app.emit("update-complete", ()); + } + Ok(()) + } + Ok((_status, intentional)) if intentional => { + info!("[update] Update stopped intentionally"); + Err("Update stopped.".to_string()) + } + Ok((status, _)) => { + let code = status.code().unwrap_or(-1); + let msg = format!("Update exited with code {}", code); + error!("[update] {}", msg); + if terminal_events { + let _ = app.emit("update-failed", &msg); + } + Err(msg) + } + Err(msg) => { + error!("[update] {}", msg); + if terminal_events { + let _ = app.emit("update-failed", &msg); + } + Err(msg) + } + } +} + +pub fn stop_update(state: &UpdateState) -> Result<(), String> { + let mut child = { + let mut update = match state.lock() { + Ok(guard) => guard, + Err(poisoned) => { + warn!("Update state mutex poisoned, recovering for cleanup"); + poisoned.into_inner() + } + }; + update.intentional_stop = true; + update.child.take() + }; + + let Some(ref mut child) = child else { + return Ok(()); + }; + + let pid = child.id(); + info!("Stopping update process group (pid {})", pid); + + #[cfg(unix)] + { + if pid > i32::MAX as u32 { + warn!("PID {} exceeds i32 range, using direct kill", pid); + let _ = child.kill(); + let _ = child.wait(); + return Ok(()); + } + unsafe { + libc::kill(-(pid as i32), libc::SIGTERM); + } + for _ in 0..50 { + match child.try_wait() { + Ok(Some(status)) => { + info!("Update exited gracefully with status: {:?}", status); + return Ok(()); + } + Ok(None) => std::thread::sleep(std::time::Duration::from_millis(100)), + Err(_) => break, + } + } + warn!("Update did not exit gracefully, force killing"); + } + + #[cfg(windows)] + { + crate::process::force_kill_process_tree(pid, child, "Update"); + return Ok(()); + } + + #[cfg(unix)] + { + let _ = child.kill(); + let _ = child.wait(); + info!("Update process group force stopped"); + Ok(()) + } +} diff --git a/studio/src-tauri/src/windows_job.rs b/studio/src-tauri/src/windows_job.rs new file mode 100644 index 0000000000..411449fb83 --- /dev/null +++ b/studio/src-tauri/src/windows_job.rs @@ -0,0 +1,72 @@ +#[cfg(windows)] +use log::{info, warn}; +#[cfg(windows)] +use std::mem::size_of; +#[cfg(windows)] +use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle}; +#[cfg(windows)] +use std::sync::OnceLock; +#[cfg(windows)] +use windows_sys::Win32::System::JobObjects::{ + AssignProcessToJobObject, CreateJobObjectW, JobObjectExtendedLimitInformation, + SetInformationJobObject, JOBOBJECT_EXTENDED_LIMIT_INFORMATION, + JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, +}; +#[cfg(windows)] +use windows_sys::Win32::System::Threading::GetCurrentProcess; + +#[cfg(windows)] +static APP_JOB_INITIALIZED: OnceLock<()> = OnceLock::new(); + +pub fn initialize() { + #[cfg(windows)] + { + if APP_JOB_INITIALIZED.get().is_some() { + return; + } + + match unsafe { create_app_job_object() } { + Ok(()) => { + let _ = APP_JOB_INITIALIZED.set(()); + info!("Windows app job object initialized for crash-safe child cleanup"); + } + Err(err) => { + warn!( + "Failed to initialize Windows app job object; crash cleanup will rely on explicit stop paths: {}", + err + ); + } + } + } +} + +#[cfg(windows)] +unsafe fn create_app_job_object() -> std::io::Result<()> { + let job = CreateJobObjectW(std::ptr::null(), std::ptr::null()); + if job.is_null() { + return Err(std::io::Error::last_os_error()); + } + + let job = OwnedHandle::from_raw_handle(job); + + let mut limits: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = std::mem::zeroed(); + limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + + if SetInformationJobObject( + job.as_raw_handle(), + JobObjectExtendedLimitInformation, + &limits as *const _ as *const _, + size_of::() as u32, + ) == 0 + { + return Err(std::io::Error::last_os_error()); + } + + if AssignProcessToJobObject(job.as_raw_handle(), GetCurrentProcess()) == 0 { + return Err(std::io::Error::last_os_error()); + } + + // Keep the job handle alive for the full app lifetime. + std::mem::forget(job); + Ok(()) +} diff --git a/studio/src-tauri/tauri.conf.json b/studio/src-tauri/tauri.conf.json new file mode 100644 index 0000000000..49819f8a25 --- /dev/null +++ b/studio/src-tauri/tauri.conf.json @@ -0,0 +1,66 @@ +{ + "productName": "Unsloth Studio (Desktop)", + "identifier": "ai.unsloth.studio", + "build": { + "beforeDevCommand": "npm --prefix ../frontend run dev -- --port 5173 --strictPort", + "beforeBuildCommand": "npm --prefix ../frontend run build", + "frontendDist": "../frontend/dist", + "devUrl": "http://localhost:5173" + }, + "app": { + "withGlobalTauri": true, + "security": { + "csp": "default-src 'self'; connect-src 'self' http://localhost:* ws://localhost:* ws://127.0.0.1:* http://127.0.0.1:* https://huggingface.co https://*.huggingface.co https://datasets-server.huggingface.co; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; font-src 'self' data:" + }, + "windows": [ + { + "label": "main", + "title": "Unsloth Studio (Desktop)", + "width": 690, + "height": 480, + "visible": false, + "resizable": false + } + ], + "trayIcon": { + "iconPath": "icons/icon.png", + "id": "main", + "tooltip": "Unsloth Studio (Desktop)" + } + }, + "plugins": { + "updater": { + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDJENjJENDYxMTQ1QTYyOEIKUldTTFlsb1VZZFJpTFY1aHgyRkRqZUdjWGg0Sm1BVEZoMDVWME5PdE42bjZiekFRc1Fsb0JNbmIK", + "endpoints": [ + "https://github.com/danielhanchen/unsloth-staging-2/releases/latest/download/latest.json" + ], + "windows": { + "installMode": "passive" + } + } + }, + "bundle": { + "active": true, + "createUpdaterArtifacts": true, + "targets": ["app", "appimage", "deb", "dmg", "nsis"], + "resources": { + "../../install.sh": "install.sh", + "../../install.ps1": "install.ps1", + "../install_python_stack.py": "install_python_stack.py" + }, + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/icon.ico", + "icons/icon.icns" + ], + "linux": { + "deb": { + "postRemoveScript": "./linux/postremove.sh" + }, + "rpm": { + "postRemoveScript": "./linux/postremove.sh" + } + } + } +} diff --git a/studio/src-tauri/tauri.macos.conf.json b/studio/src-tauri/tauri.macos.conf.json new file mode 100644 index 0000000000..202c9fb505 --- /dev/null +++ b/studio/src-tauri/tauri.macos.conf.json @@ -0,0 +1,11 @@ +{ + "bundle": { + "macOS": { + "entitlements": "./Entitlements.plist", + "dmg": { + "appPosition": { "x": 180, "y": 220 }, + "applicationFolderPosition": { "x": 480, "y": 220 } + } + } + } +} diff --git a/studio/src-tauri/tauri.windows.conf.json b/studio/src-tauri/tauri.windows.conf.json new file mode 100644 index 0000000000..a68cf6351f --- /dev/null +++ b/studio/src-tauri/tauri.windows.conf.json @@ -0,0 +1,16 @@ +{ + "bundle": { + "windows": { + "signCommand": { + "cmd": "trusted-signing-cli", + "args": ["-e", "https://eus.codesigning.azure.net", "-d", "Unsloth Studio (Desktop)", "%1"] + }, + "nsis": { + "installerHooks": "./windows/hooks.nsh", + "installerIcon": "./icons/icon.ico", + "headerImage": "./windows/branding/nsis-header.bmp", + "sidebarImage": "./windows/branding/nsis-sidebar.bmp" + } + } + } +} diff --git a/studio/src-tauri/windows/branding/nsis-header.bmp b/studio/src-tauri/windows/branding/nsis-header.bmp new file mode 100644 index 0000000000000000000000000000000000000000..7eacdce7db5026855113aa5f03c47663d1e17561 GIT binary patch literal 25818 zcmeI2M`%?|6vyAoYkB>pM+JLBK}E5|0vb>h4T7NJ!i|2eT)0utg{~Ag7ElNZE-XP% z(S>}9pkT*}9g&D2*s&q@{{7~jWHPylFYkru{S0%txlTEA=FIt@|CyPX>YDn0e9kk9 z??8TP`KjW^@2lkN^VK!5v^=ytv^@0Q1D1!Dhu%k_-487fEe|aZz4w6S zq2;0XQE2x=%R|dU%R}!y&|V&vrBfxTL}@Bno=K6-_?Bf-Ql?5-Rhx&nOGb?#ji!lcqkA+Bk+m_+K~?jgGeLHC}w2crhdxBsRZpxXMrDW1R8ds zIUSFgl2mC#TdT2v+QM)cC4PUypZ-y3cpLv9ke;T-Mlm{FVj|uo+;X^XQ64CW7e-n& z$uz!YIe!dekx1jj4ga!O9xAa&SR5%;m*;RQGWZ79CY>@>h6ya9i$$OzlZdGc|p@`v9Gjd(F{i>QLhB#Hs$7-C!iusD4FfexvNmHzjaDZ|| z9(;p8Xuy9|AvR^fu(BwfbX-mg%z=&c5)R^NzM898qDJ|A(MXsmAkU*p#B#)t)rmYS znmTprjvYHroH(&(&z{+{XX9yKw`AMUUTwKDWMopH z6HCadWFcd_ai6L8@FyBm@vLn z=#UU;*REakx-cYXo;-Q-$B!Q~X3Ss|Ec*26)77h2d-m*k@#01JHfho%+DDHb4ea;t z--n9VuV06&)z#HUjvN8BMT-{6@=2ud`N@+flBHR)Yq43X`N*P1+}1t};T!fk99H)d zP9#)u3`AmoV}}F^vDCy6NpUR)5!X5-c{LTj^y&1DA3vTnd+pk_UcGt=hG0ee;K74mzI^%k z@nh&G7*3u%nVl94=ggVoOy~o;r%#_EhOo3~YuhaRP7FC)XjsTpl5b_df`nK~*mFj~ zMjXtm?i`M%O$^b%5QO=nL@x*pg00>9EN4Ax^M(l)+Gz*M!Hm3HS&-X2@d9{ z0g=FG!GZ-NMvPzx`=OajT^Pbc7lsojPGr>L#f#B)?%cUPefmf)9XD>ABu~C_T4KM2 z&){ELTFMYC(JO7U@H;WY7U39iFF4R5E_V_aH6L9#kE+Oko-3D91!rLhX_G@0=s{_r zF*v{_1W+qSN?IuMqEl~iOoRbU!7BjLLWr5}(ZNEOc>VI_%gdH6;|l_-SFhf%VFO3X zH*em2`}U0-yKURHv17-QOX(FJN-l+I@R0qG_5%kF?A*EY)vH&ZKYu0=IMdv^bqk!y zu8f81)J_pb=#-pJVE;^Ix4Hj&)$H+vN5afqp} zuV-5W5FU^u@6DPu>)^qIFJ8Q0^JEmMl7xBc)Ttgldca|1c-ZAQqq6UDBm%>;XU{%* z^hl1iGiT1^uM zN#iaLZa5pCA!F`8M1kU-(~AJZmDin1w+lmA0osU8Bpq$sEzzbx0AcQ#43|vEm#O*f z40P2PGLcxWh6kMA@ihJhNR-Q079xRw%Lfb?09&})VPiNPW~buhp&!nZTo?QH?Moq* zN~x%c$A=6V!YG6pC6bIPDJd~^B7>x~fGSxU@Vj;ECM~3MoMu|pW;stDqK}0V`Yy$^ z(5@RDnT9H5d};1HT1>Rj%WDpA6+b^I;)TpZAD40HG2|3R`7Vc6207DMAw+|oV2wFTcpD&F?x7Hu0F zAhbk}{Qrk;?_`DlB@a89kAL@aEQa2tn~uWPVrVgRUlcnkwyd7qJz#lgd1!g)y$380 zEf2kq!rb?b&MIknXnAOP=)DIl4=oS9kHXHn%jLc<%R|dU%R}!yV0mbH=zSFCzHf9^ TNy|gaL(4<&Jz#m*8F=^~mCb?X literal 0 HcmV?d00001 diff --git a/studio/src-tauri/windows/branding/nsis-sidebar.bmp b/studio/src-tauri/windows/branding/nsis-sidebar.bmp new file mode 100644 index 0000000000000000000000000000000000000000..c3bd480fc8993fa88d280c35ad105a7ddf1bc7ff GIT binary patch literal 154542 zcmeI52YeL8+sBb6N=q;FUP3QHs?w!O5k&q178GoN1w;WsdJT{OAtBUMdQa~VAQb5e zQk32ivCss3zq@m{+1y<&mxw@kC!ZzE?al6Oe!qF@JTup_^V+v^=Td$e@UII0g8A3@ zxm>xP%jHHZSFW!O@2f8izdT%v%0nU`5s(N-1SA3y0f~S_Kq4R!kO)WwBmxoviGV~v zA|Mfv2uK7Z0ulj>bCGh6 zg)2A}uHvHX!BW|!NM+|DqFq(IbSXkZ4-45$_(L8>3&CeQYmU&3P^_0I%Dr-B85 z8b}46{M9g`aP4U5W07lw4snA-c%eTUl97yBMjp9?G7_8$m$EAelnfZURdcg*E38n% z4|0uk^De_iFv4%H6(H~=^x^63U&zmBfqHHwx^V|{t{EyOT=Un7{&3f>UAtw=mL(-6 ztyr<5XV0E6!p-gM?0kKFIa9G>Mdg8b$lSR*ckUb-8k(4xxN_x6dcyIGFTUvI<+WM; zS+i!%=+UDqRjNei!i5XZnKNhAs#R60DBF}3D^~35ufL9nh=_}en>A}z?b@}KJF0)? zlh@})*ufJGte~fgExH=qct{($<_|$X2xSG8+(bw&cTi+4J=bV;cuhk<_n~y=yif() z5R0I6%a=zO0&dGk*v>U~Ky`BmetPxl^~WE7{Q2jfJ9qB9eEIUNTetf6@6UNxSJ&gm zkKew1yFmkGUpamt`o@hL4<0-?ckbNZfB*g0Uw`%P-MdhsLVNe_{pX*5xRxIphYlU8 zTeq&gz5S0r{&?@+y+(~1xw*N;#>Vp2)vH&}o;^$Br+qP}ni4!M=hldNn3qM_7~HPcq)HadaiNBh0Nnn36mlIcE}Y< zTd0C8agB=(!iv34^TX}Lpc2TpZ{J=}jBLVDaB#4PhX-$&a9yxq!L3`jqQEIzg@87?_Y}vAcYdGb6wQAL_U%!6g!Ue=xaNVX&8$1EHe&dZd#3w@WrAwFSMrU#6 zNl~nQZkD*Vdm+ExbP6XKbq%MuZTv-yUNkc_AzX@Y3U>DF*)wd|Fn+_- zgb5R7&YX!0ee>qc`}gmo+ZQfe*tl_HbU5z{xfVJc$wjWQ*PLIwc5Pr_;IF^_diU;K z1`NL0;lqa+4`_9+MRlO(@!tLX{Ai;eh71|PgYm=Q)E?|fHH=)RlL*CLtAVY(h_8*_ z6oMn3T>Fh}Rb-J| z!Ysa@@WXi$F@DwQCr6u0lnlsJCp{Qo3|$bOL?&!3yJ(W0!I5;CkD(ZRgLQ7jAr)E?w{dxhLKl zHy=85sBp{>YSlSO8;9BxKF$Vc0IWed8zlm?Im3A+jCHlc2ogLa(YRRTOyC#55jXni z<5~14bclE9K&lNw6$7fX*9zCFKZXFJv>EO2OJ{}_c^eT~GZOIRxm;*qej~XYXoHGQ z2Cflj@#I{~9mu)i;hWh*vC3BNQxIPF3vC}0Zp%P%%Ah|oZ%S&ghT%@ zCMQi^BK62J%<3=Eo>shz0CG>mNlRRdx3~j6P41?AoSK1WbO9IRL3=`aLM|s6gqQ~F zPa-~oJ`6cwAO!f2^Ecy=F=-Jf4Y6mckuld+{!H-ExYn@JbFDaLMnz=3!Q}+?r~(ub z8sPC9uIUE5PR9UW8+`dsA=kC2q3>jB(Qb>(C(RKWj zUJ6 z&*~0uzUMxAaXIgp%5xK|%uT2|H^F0WLbZ7b)#oK@j6-*=#nD~Xt(p(lQ5(V{*N3iO zv3Sn3(Qm*0s%NDVlJTc^w5xWxZe@p_{ai;aEIlixyiY;}pSX%Xah2x8SDq7JWp2EY z@xW8i$hG<7`ToD(sI%e}q7+(__Fmuw^eY&=;U$MCAwn-U(YP7oWzDn4& z>)*pjeyO%Y|F2vpuP-qxuGGw!(z9YY z%FK>c828SE@fyAfwFB3Gi1v=!xI8&_FGJo2buLgE&)3A0KSIPL}X53(%?ce^)W%9aWGh>T;MRSx8 zhf24eaRj{V>^OsrTQuSUo4TzGnY(FCP<-Ut9b01d?M~UhJ0*2%Tw?Twi1ndsLl=fF zp0mJb;>geYcWz$4R%sW>_+w&R)!R)PI1HKQIz6hGS4{C43eno3XB+`9HA~Uz$hZ}i zUUgo4%_XZoS{=L~d~HZV^!lAC@rU-M{%~OT-kn=h5+h^7S8rIkbj32?zy&jXXMQ() z@Y|hUsaI1n{$EFPu69-3>|Sl}IC7DjS4^=P(b{2&@pLNPsu549;v43WP!{|V?V(7 z{@vTQB}HihzRZ8-!nu>Zr;PaG!``npu34j`lVto+GVZE)VmkKI*<(BtBVJ)nRILTeK88f1t<7tL6QXd{;;`Dk zoH%mf+^MstPW<%4{_l5gAt)p3vVO&q<;#4REl^at->h*n#t-}S-Pc=rdQ>Ye8P9sQ zO|{Xl_j8=I9-?u)IgDJ3h6Tp;0&Yd6>*F%L5ijo(QE$QGA%P2Ktqk%*enlXLM4$ZW z2k5?Z;S5^+@PXZ`&!z@tVGF~S%w6g?BVf*?`7_5(ANS=a?{sTfzp7+B3j$7f?ZWEPmg>iEuPI9%p&t^~mMV|-y&s-5Wf8(lUcxBrXBZ<55 z)6Sjw`O2m97tftO@zdc0-)~2yGZ@eSsQu8zJ{a);pGkyf)5d=BQNOM&>Q(V5<{-6t zrleCz(yFz?m{4bko)%>|{srS%ip$FRY--@Y@bhJUUdxxxLsv6DvL$Zwo*hXzX{Uca z2HjULLpZX&fA20s7_J!cVE>Rs-okD3_4;n=cb^Y)H?ZbJ7zn z7@r>XZ)98_ml;E|bVl5mT#YO1v$?+CqCrdN&p<`54O>VKjZ7Nm`{3SGIELx#S1&<2 z)H49~>`2ClGg*cnz-fug3`;>vo5*Pg#*z!KkSWKq!4Oo7EktRdD$ zmX99ZcjlMlS1(`q_1YCg9Ov!u{@ui7h=25^6{OlQ_j<-Bj~?=V?~YAtl&|V$FAHD{ zVy^mQy$3o>33r+r>CB?^@KW3S~V!qd*&)0<{M6TVg3iy#jdkMfVUp#x``c-5cUmdsY(B9NtDe>tT zSM4~S+dQu^-wprh&CbmmR4GZy_y27bD@50?WA_l zID|4gHaoWbg5;M%cXnBt(qc|rB?IFIx=V?l-#Qo<0UA61NnWF*^p=tM~h47zka#<_aA@Wx%~6h z_>KDqc*mAkReBlaH*y_UanC9LfByLgv4uE1?U04DnfY3`d=cq2EO<&n1TOmFeLGJa zJ%n?9^~%NHjEtk?6|KHK0XrVEc@@688lBBSMflC0Flo$}ANTLov4v;73dR3lF^*8T zXkq`|a!q^dS=P5T0XH?`m~XPC#wkwxG56_F97gKXF|K%PYxnm1^X?T$)c^c_|JL`X z0{jwds{*dj9k#Q}?R&q{=i$SL;o+MHzTa=s=%IlNyuy~uLBKJ}R3|1ztw*JQpPI}B z89Myj>7QX-m2vF&>7S4P$a%&Cqk^#!zBU-g9&@nN&&zx2=#fJ|`k-I;4$bS={eQx^ zt6j~qc8c$9s=f6rYi+%Nr!lV1i5OgN9plABne&TX;K=Mn9&lX*3=g)J*`J|*IH#fIjxpI{#=JxJeJr?*(rcMdUs7OR$QR&Qw z?nsW_yK{?R{OswIS1zAd?D+L7m+;|E9zAmCdwh6`oyfjMZV213atZ00fVopAjv4ml zp#J^)cI(jGvu^ofib{WWwK_uGp^L-B4Ng-HE}2HFm4=>iB`8y;LJV9hv_`p6ltMKr z#VWMb36T;rq7pgw!sILZp@oE3}`*!c}O8sY+aW$a+e3p$ut!H1*?KCAqvGOyb z4B1hbr2-r_f4&-b{>JX37uU!9@D&RI=%AJ=6=wjPxxP(sI%UcfBZ0Y`oSf#)o%{FS ze~Y(HU*DQ|bj-c`fAW$cQLS1vttXA=o_p@iH+!X~BvG7(K3~3cE?E?exWf3Rl}wWn ziKDm4y&U~v9|8FJv%lcHUB7yn-?RzP5PyaQ-KdAzyf$Qs|LnfGLBHc*3)6~COwInw8^euo%!t*U2;LPXGm%%TxBAr>5;A+K|5ZBV!@a8 z`}DOfOHy8eZiIT}?w*3dt5>hquU}u!V8Ma~XU?2?|Neb(;r$19MPuE%b%arxNpB2HT<)$zx?>4{%>|{(V%u&_h%&if>2km&yrB9M7!~O z>9&?fF08wG>nz0me*66K%h6#wyRu^I{Ea;t#Sb1l(ApPn>{*=hGJ|B-&-28|&6_vB z@WKlw3_|zRsZ;OVxuac(0v|YVph-9FrHiX;`SRrp7b;Y}YNdX?yH6YcO;EtB)xmy5 z;)G@_F^CIat=1$*kytyhXZw$bzCUsFFf3!kvE%4ln8rtM3|u^a#^kZ% zM-TsQayyMu%wJynXKE zmAHrhC^i>gYxeBfZ@&4akB`rve=2&IpVQYT zcdkm6DjK;~8o6`lc5`uR-n8MT1NzOHGMY3go*UKXIB!&;Gbu*!wIewO-L8;+a1REZ zx));e^Jjh`UZahe?@8UVaZT9#IbPEyeK%?B$O&V<9y8*LK?C}9GRSz&^xUX9DP{N3 z_$*jzRj6GtxTmgd5k%a&b?c{}e)|3Q-^EMu8y-iE8ih%Pmppm$3?Dw+D5nq601qE3 zDw?axlqq9{xP}cIE?Kf<-@bicfBiK#vC`B0t$g|Nxx2YEYEbvRzHbCCo`(@fu`^}1 zc}-x{hUG+FgzDRqW03THySL%RVbclL@YPTM{1e8VbX-zgWZ;qo-d)VrJc5h4Cw`=>My}OSb+Q-b<@uLTi{IGjlO2qON z^L!VK^j#VLf~|c9XqvEf@LL~ffHxnn|j>WKBJgI8|vw=}s^ zKtl5ciH&eR7i?(|u(iR0%|Q^|hVyeuei7-!mR@z!SKGl33^ z{PN2$3og)^>({RrJR-kuz4ewkM@5SkWo+ELckkz)e}47VSIs;8cV{pnckkXEKYslG z{`bG5M-6VqPnR~c@Lwz)WksQG)v#fmIPdx;*Y!`X=b!9JxB<%?3sYViHMd5svW_`O zjiZHHwYHzIE(`e+h=%U)gM)NGzsn^J_f_`_PyU$?tWu#yoYZ>jd`Yv#5KCv2m-D->Z^=52-woVdvm>(FSCX; zhlnqLDDY`fu6s`}67oA^#tc0>*k8^gwu1%@(x1<28#k;_ zp+c^%u4T)Xg(D9SkII!RV>l`xB16^2#ev zuU&3Sqjv4u#fujgJwN;GGr=`E9})sA9ffg=+X5*2ek3aUeyBUW7#I)O>N&!@TFo*! z!y8vT@Fx121S@i_=xq+7EslUKjsCcE5uk3|xY3|N16x40(uX*@Zr!@Wh7F5|h&X%p zEGAV@km>I~{`dp$Zu<1;O`A3?P@sU7kI=s*VnEKoyNDW?OJ%x^1*8z2@wNbIJH4ok z*Yn>}f5w{HO*~m9oCDN2581W1C^WLdGV_~C;ei@wIg#%o+6qbSEYh1NPMj!NvZNl# zEVb?I>umvf}1t`-dUNP1?3?iduR++x7L5If82z;=MOKa|I63>OtyTNtYqr8}@%7G9lZxZPOex+d-x$8opPrURX?H zb|u?9r(JmS=FJ5Q7F4WQ(e@`nnTYF|VZmM_+UmX^>9+nz+ZModZy&`!fB%beX+h0X z9Dh+M`h~YGTpJ3Cgn+|!><_~QL`+6xNt6T&sC~rn_U+qgJUzM={y$E>rl_s+j_tT| zMX8h^kGCYHIi*l6L(sPVu(&ONy3>my;}~(jB$CdfgKtb=LtE-p0wIY#->ackjDls$a-;fJ;)RB`p=b1h20fJi^@Y(lH~O9CiAKR?@D zFTo;dE-lYyf!7R9k+%K#SCpGF%`o;PqDFP zD84_!F;k(|b4`sw^x-eB{dr6)9w0=u0;Bd8xoF}k>og&}#Ln~P&HMJ-Z^@ZYoH)^B z{;N`@N}-{l`l;}YZitgcevBBO858XpGHQ^%6DIr8%>%v(HPvMZVgaefxY({ux3>@5 z0*DRz5~|LL^XUIY>8D?=Kby%Co=kMO@Vzn9gw<=l?{nwxn|g%NlNn*Cowv8QU%q^~ z{wnh}ej2(|%W`Pf!tanFL--WIJ26^VSeW_c;tZMj)YMe&)4X}}!i5WKH?iKJtVuOs(ZB?*nR^08Q?ZimRpm-jEF|Pivjb_JMWlu zMuFfdCEqy6tbh{r20!%1}~8-VD`E`MO%lt}PW;8Be!J+^Evy%FPJR z0Xlrj=FGWP9d%_*?!qmN|G1;n7ct+&^kSw+Ld1?8J1o*CmN17crvF-PpzShmmV#!Gck`MZ=tNWm^ER zs2t#%O^r~>#%u|@=*D#I@3$3WN>+*)A}i2|w@6f)XkZhH^)K}jjfj2s-FNyVCz(u& zSoHk~y~)h*L9ooKA3V1jHELw~NEvm|{JqP+ANEbG?iEwQWdAN5<9d~DSTH^_ItRpD z(;{7!!h^`n8Aq#XkqDnpezS*=$v4T=Cj(}T{ac2w}W&8k6l2g;488J=(g<7Uy`18*{fA78b zSeMbbabsqnAOemkIK*=%lZK@hv8|p*@?+-3OO&YT`!HZkxaM6HJ0pru%;>2yIXx=*=P!!}mv-%9JTD1}}`<35+U}h5$ z7K=$UV%$WfPu-|4)qlEkq?6aNyF0c?8OCaCrP8g|ew!I_W!3GTQws#(*wd`yX{A4W z5j*RtB>hEV1ldPF@GilZ@RPMZjEB~(TZ?<5WcKghj}pFk@gg-UOkJ6L7BvYiTC^bN zW^!J8NfJQJ$XxtwA7w}y*D#&|-Ra!62`h6zK79Q0k~3pV;dUA=l#X!=M%*##(B}f} zWy_Yajz+`E_689*w%GhyrXaPIdg3F2hY<6kyc*-!8 z8C&v2{FyMWs&s=9SF$KNm5wdDrmoZ@lW9wct$`^T5|>McMHvr#^~F(uhzl5!TMiuX9}TJnMJ>)E6=bd z<1xiP9_RY>bN%6{Qmntj!6w&dWSogS!8n>Kol3`EGZI)O0W8QAHqIP0>t_go#q1$> zz=^^gf(}vii%AvrAtr3v@{$!CqJ)^4Zsu57=*Jm-z<%h^AuGui)V`Se;Ira>90AKS zlqmqXwu$cPk;VEB$^msLz27h46I+F;SW#0a%5MxR-Dt!$l`dvLwCW>fnXNNx{w{=? zOl~qkDMBX_BQRnMvsU_;_-|TImiCxGjCO{>COpHxAKYdE2zty&w4gf^#;0y}f32s} z(=XQ@JG;*cXPJYCsEejtR#es*ReCxju8O(q`n~Tyc&O}dL7JP)7s`|S%0w>E-kCH7PpHzD0po(Un`ez2P)Ay)qT{IfRVjR>gp5*H7*`j$pwfjAFQ*!DWv@szE+hS0 zk`7{X3>+To6PkKFt+a8aaHlgZIAr-JH4MahdP8iTXy(nCXw&D_%^!nL=bqv|?5Jj# zz}0bWlyOa;r&H;0J$R~1jWYI6zg$->X8-ZXivC;bu~La@XV#^P^=m?LfhkFvBJxGXYnrX8EXx!#987YxuIB` zOHsxr}TWQj9;QBOHIl7^R?7ugH^^+ z>B5L3;Auu2=MAn|`t%KO>y+u=)lX-T?#`%sflIhQmIar<3f{3pm0)BgS>m6zA%t*>*k$ zaIJb2_5;4CvN)yLqODC9ZfT@2o($tcrCTuKs;^#=ed@%TTr_Z|rKD)=@NmQ00drc- z&oFO=&Sx^3NoqV~bS!O+IE2w;E}J_L6bu+JKueEnovk;-wg6my;kUimETd5CYjwn| zpP*aS>ggHp`L1IQAzD#8sPT4fOZvq$Te9utMO&LLG%zkydYTbu7ZKJt7`7dVS9Tsa zbk0!nO@Wa002aXU!6*&YMAe*M{TZgJ1xL(QFmJ__7FJcy8e)zVYnO;gF?|PH+Kj*> zM~;Y_Bpewnl(v~@3*PmN8#YeoJ5a{1bZOc+XDD!C6d0CuDmqPA)L_|;7VI9W3OI}_ zDm`F}X2cP2LtKVd=OFT0zU%eB?kdS`M4LLyOyR|2@7c3wN=k|qYb%h~Poi-ZHHXyp z>+es&1NRY~sc*^BB3$i0nowW=^Ei6Ul2GdfoOU|K;d;6VQ+!MJiH)YyL}Pyl}NlnJ6)!$oev2BFsTP7$^9sAW!k zp;eCXz)j@3dP#@jQ|bq&zRH58W!qaL;7hj&l`f1p0*>33X2it~5=i&fW0M~~RQ8o6 zs`@u+G?E@zXKGQ#jvdS77ZrmVHIKYO0hKWKtU%m+;A3NurDrpfHjCii9p+fIcuvyZ z8sms|moDWNC$xC?^)dwQftOl~ajUxK>4bykE1h-JKP>Q#%AJ z0**@0#E7fWStE5DLuF==5R8@bHfAXw6_1(ThiOxu%9wk9$a+kw2eZL7LjB=5$LghW zSg55LYpsFR?$)j1(xi@|yI$i99nv!{jJT%KMI^4oWoovq5wd^$vSEi|vYO;TfHzYL z9#03Vh`qeLwCQhB-%R0UM|(#(ja}u`vQ^F&YC$)mU89WSi0O@%@9Gk|^R>{O2zaO9 zj8wW3i5ucFkzb|r*b*dWx9`tWj=)xBQ|T&qIkX-h*W z9hdL!9=1y`-bJYNOpJI6xfjKbBlRr)5{pCt90!=dmaL|z{I^{^v)YrLlvubB_xM^IbX4@d0 z^4FYaKr5hvhwB)Mlwl!b8_2uO|yD`&E?m|cz zzghZ0PJ{A;f4{8_GoDb)#2E=3ljUGHZ=FdxI+h7{6BjERFW(vFSS{xV)YeBPn!ItH zV$%a#t=RLrO1GZzbShoAZRSQ?IO^opqYn?g@Z0yA1&49!7T8UNB2vCAl>~V9Bpn}s z)jcNl#(&CJkLtHTh|%wJ!FPs-Kx)^nKq{$y4$uYGjDyn6?=qq zo5;9r9DQPlJgMwkaW!C<%2?O(+?9M@;#Ns=0*NlNo zWuFOgcE=KVl6&|6;;WOf9cq=J`dy#O!o%zm9qq(ke@z-bn`YXaYb+GXyldAA0nr_d z%G`u*O{I%S+*)##Nij0*@kd6H(72_3ak-v!+GIR%o>?-_PB;`ZP;tuqCMholfcXk~ z&21xu7ljAZ$q@8SiC{BEWs|OKq(HTSfkclEc52nyfwGfl)l6HUn;IK;`~UT;x;VbW z^4;Aq+(vz_SLqt#N~+By5?5zPwdATYA&$JS+xu?nNpJQOXPd$maf49P^eV1q1A2TOfoI$kt z?#RCVJr*al58KrxY*$wu-Fn8;8F4GgRS||m`jV6u44%kCL-@{I(&= zt0p;2s7F^*xkRBUDH^uAqFRZ`Xf6MF`20#%4XYAufk91Xtv4Wg>`JH3JscR}Ijfc| zBl@y>-az;JRTss#3)}S?u{LhGo^i!XPalcvjJU<*s;Gic>t57aWeOVc<#6d&?C!~G z&<*?FU%#(EOMuVc*rmnkCe-U+vek*|k11b0W1USa?;42i=|IT&WQkU+=My>@8-niM z{VMxKwhrCZiMW-lS~gU=d2$s2S7uL?;#Iz|QWUnOm8LUix@u2vDgY@XxqR~wU+jp~ zvo4t0C%(9W?ez$vF$Y|(eucWS_jUH@R%~>LQ-`i;68)sme&V02(yiF;E?vv{tZN>! zld1+7M{j49ar2^=v>8%mF+#fKCrYidvSe8OUOdb(^DnFS^yVviBMuGb8#>6*u{?nt zzYSY|s9Pc^E3mT2I1|&~EOKnsMzztZKla1{fAS9i4z6~uwk|a_uyJr|JKQj`rsgVL zmwU0zh?`cfrdxiZTTv)h8>vf%W5tr;vNrGSlYtRecgz3W+p#HGP|32n;c&HUQqTSC z8FiO#Z5^E2US(WS>6VN*F1qm2ZAh*vrRkO{SJf(I7Kxi|3n12zn(ytyj#bK@CnkIQ zm|JS}_M}ZtAB=LWUqgwbpS&oUL;Vz-AZxNRpv*&5|sL?%6RsTxNViIY0FPC z*cL#1EsN#7eawwGdV5T$Q;&BXYLt}{o!K~dwPUioLx)l<5h7$(8OMlswra%Fsq~DJ ztLpL-3ze&^A+>Q^0QEaNlubXhy?s<8E*5SmbCgrUo%p(w=FJVBn3QN+5J12^tGf*R zvg*9>W~>~+cOtU3(TMBfvUJH+lgd?5`I89KXo=N`_rDmW-C z%fRa26Rq9B)s7e(#=G?>J92hC|L7J(;>J|lzfZ1eahcc%LcfMoG2+U$0EyM+C3?(B zs4{hJg~5|bweREt%L>JkWsRdp+iI2 z%TLl(uCipd>W-irBNm>3^rrCn_&S;#V)!8P<-M&F} z*RI{l4f(dt^x#((#kUVleT_O*N+5L8;@a#(cKGtoF^+hBL!j1w?lY zN+IZ0N+1;8)hTkOO_jW>x)>o{b$Y5|#DjKF*w%V!N~?f`SLSSLGI44B&&OB)-+Sd- zH7{1DvJ>MAQ6xVNCYGT5&?;TL_i`S5s(N-1SA3y0f~S_Kq4R!kO)WwBmxoviGV~v zA|Mfv2uK7Z0ulj start && /exit 0/ { print NR; exit }' "$INSTALL_SH") +tauri_done_line=$(awk -v start="$tauri_marker_line" 'NR > start && /tauri_log "DONE" ""/ { print NR; exit }' "$INSTALL_SH") +first_setup_check_line=$(grep -n 'if \[ "$_SETUP_EXIT" -ne 0 \]; then' "$INSTALL_SH" | head -n1 | cut -d: -f1) +setup_exit_line=$(awk -v start="$first_setup_check_line" 'NR > start && /exit "\$_SETUP_EXIT"/ { print NR; exit }' "$INSTALL_SH") +shortcut_line=$(grep -n 'create_studio_shortcuts "$VENV_ABS_BIN/unsloth" "$OS"' "$INSTALL_SH" | head -n1 | cut -d: -f1) +shortcut_guard_line=$( + awk -v stop="$first_setup_check_line" ' + NR >= stop { exit } + /\[ "\$TAURI_MODE" != true \]/ { + print NR + exit + } + ' "$INSTALL_SH" +) +shortcut_guard_end_line=$( + awk -v start="$shortcut_guard_line" -v shortcut="$shortcut_line" ' + NR <= start { next } + NR <= shortcut { next } + /^[[:space:]]*fi[[:space:]]*$/ { + print NR + exit + } + ' "$INSTALL_SH" +) +early_tauri_exit_line=$( + awk -v setup="$first_setup_check_line" ' + NR >= setup { exit } + /\[ "\$TAURI_MODE" = true \]/ { + in_tauri = 1 + depth = 1 + next + } + in_tauri && /^[[:space:]]*if[[:space:]].*;[[:space:]]*then[[:space:]]*$/ { + depth++ + } + in_tauri && /exit[[:space:]]+0/ { + print NR + exit + } + in_tauri && /^[[:space:]]*fi[[:space:]]*$/ { + depth-- + if (depth == 0) { + in_tauri = 0 + } + } + ' "$INSTALL_SH" +) + +if [ -z "$tauri_marker_line" ] || [ -z "$tauri_exit_line" ] || [ -z "$tauri_done_line" ] || [ -z "$first_setup_check_line" ] || [ -z "$setup_exit_line" ] || [ -z "$shortcut_line" ] || [ -z "$shortcut_guard_line" ] || [ -z "$shortcut_guard_end_line" ]; then + echo " FAIL: required install.sh markers not found" + exit 1 +fi + +if [ -n "$early_tauri_exit_line" ]; then + echo " FAIL: Tauri success exit before setup failure check at line $early_tauri_exit_line" + exit 1 +fi + +if [ "$shortcut_guard_line" -ge "$shortcut_line" ] || [ "$shortcut_line" -ge "$shortcut_guard_end_line" ]; then + echo " FAIL: shortcuts are not guarded by non-Tauri check" + exit 1 +fi + +if [ "$shortcut_line" -ge "$first_setup_check_line" ]; then + echo " FAIL: shortcut line $shortcut_line is after setup failure check line $first_setup_check_line" + exit 1 +fi + +if [ "$first_setup_check_line" -ge "$setup_exit_line" ]; then + echo " FAIL: setup failure check line $first_setup_check_line is after setup exit line $setup_exit_line" + exit 1 +fi + +if [ "$setup_exit_line" -ge "$tauri_done_line" ]; then + echo " FAIL: setup failure exit line $setup_exit_line is after Tauri DONE line $tauri_done_line" + exit 1 +fi + +if [ "$setup_exit_line" -ge "$tauri_exit_line" ]; then + echo " FAIL: setup failure check line $first_setup_check_line is after Tauri exit line $tauri_exit_line" + exit 1 +fi + +echo " PASS: non-Tauri shortcuts run before setup failure exit" +echo " PASS: setup failure exits before Tauri success" +echo " PASS: Tauri skips shortcuts" diff --git a/unsloth_cli/commands/studio.py b/unsloth_cli/commands/studio.py index a3c0840be1..b74f42674d 100644 --- a/unsloth_cli/commands/studio.py +++ b/unsloth_cli/commands/studio.py @@ -1,11 +1,19 @@ # SPDX-License-Identifier: AGPL-3.0-only # Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 +import importlib.util +import hashlib +import json import os import platform +import secrets +import sqlite3 import subprocess import sys +import tempfile import time +import types +from datetime import datetime, timezone from pathlib import Path from typing import Optional import typer @@ -13,12 +21,52 @@ import typer studio_app = typer.Typer(help = "Unsloth Studio commands.") STUDIO_HOME = Path.home() / ".unsloth" / "studio" +BOOTSTRAP_PASSWORD_FILE = ".bootstrap_password" +DESKTOP_SECRET_FILE = ".desktop_secret" +DEFAULT_ADMIN_USERNAME = "unsloth" +DESKTOP_SECRET_PREFIX = "desktop-" +API_KEY_PBKDF2_SALT_KEY = "api_key_pbkdf2_salt" +DESKTOP_SECRET_HASH_KEY = "desktop_secret_hash" +DESKTOP_SECRET_CREATED_AT_KEY = "desktop_secret_created_at" +PBKDF2_ITERATIONS = 100_000 # __file__ is unsloth_cli/commands/studio.py -- two parents up is the package root # (either site-packages or the repo root for editable installs). _PACKAGE_ROOT = Path(__file__).resolve().parent.parent.parent +def _should_hide_windows_subprocesses() -> bool: + """Hide child console windows only for non-interactive Windows launches.""" + if platform.system() != "Windows": + return False + try: + return not sys.stdout.isatty() + except (AttributeError, OSError, ValueError): + return True + + +def _windows_hidden_subprocess_kwargs() -> dict[str, object]: + """Return Windows-only Popen kwargs that suppress transient console windows.""" + if not _should_hide_windows_subprocesses(): + return {} + + kwargs: dict[str, object] = {} + create_no_window = getattr(subprocess, "CREATE_NO_WINDOW", 0) + if create_no_window: + kwargs["creationflags"] = create_no_window + + startupinfo_factory = getattr(subprocess, "STARTUPINFO", None) + startf_use_showwindow = getattr(subprocess, "STARTF_USESHOWWINDOW", 0) + sw_hide = getattr(subprocess, "SW_HIDE", 0) + if startupinfo_factory is not None and startf_use_showwindow: + startupinfo = startupinfo_factory() + startupinfo.dwFlags |= startf_use_showwindow + startupinfo.wShowWindow = sw_hide + kwargs["startupinfo"] = startupinfo + + return kwargs + + def _studio_venv_python() -> Optional[Path]: """Return the studio venv Python binary, or None if not set up.""" if platform.system() == "Windows": @@ -97,12 +145,228 @@ def _create_api_key_inprocess(name: str) -> str: ``POST /api/auth/api-keys`` on fresh installs. Safe because the CLI already has filesystem access to ``~/.unsloth/studio``. """ - from auth.storage import create_api_key, DEFAULT_ADMIN_USERNAME + storage = _load_backend_auth_storage() - raw_key, _row = create_api_key(username = DEFAULT_ADMIN_USERNAME, name = name) + raw_key, _row = storage.create_api_key( + username = storage.DEFAULT_ADMIN_USERNAME, + name = name, + ) return raw_key +def _load_backend_auth_storage(): + run_py = _find_run_py() + backend_dir = ( + run_py.parent if run_py is not None else _PACKAGE_ROOT / "studio" / "backend" + ) + if backend_dir.is_dir() and str(backend_dir) not in sys.path: + sys.path.insert(0, str(backend_dir)) + + auth_dir = backend_dir / "auth" + storage_py = auth_dir / "storage.py" + loaded = sys.modules.get("auth.storage") + loaded_path = Path(getattr(loaded, "__file__", "")).resolve() + if loaded is not None and loaded_path == storage_py: + return loaded + + package = sys.modules.get("auth") + package_paths = [Path(path).resolve() for path in getattr(package, "__path__", [])] + if package is None or auth_dir.resolve() not in package_paths: + package = types.ModuleType("auth") + package.__path__ = [str(auth_dir)] + package.__package__ = "auth" + package.__file__ = str(auth_dir / "__init__.py") + sys.modules["auth"] = package + + spec = importlib.util.spec_from_file_location("auth.storage", storage_py) + if spec is None or spec.loader is None: + raise ImportError(f"Could not load backend auth storage from {storage_py}") + storage = importlib.util.module_from_spec(spec) + sys.modules["auth.storage"] = storage + spec.loader.exec_module(storage) + + return storage + + +def _write_auth_secret(path: Path, secret: str) -> None: + path.parent.mkdir(parents = True, exist_ok = True) + fd, tmp_name = tempfile.mkstemp(prefix = f".{path.name}.", dir = path.parent) + tmp_path = Path(tmp_name) + try: + try: + os.chmod(tmp_path, 0o600) + except OSError: + pass + with os.fdopen(fd, "w") as f: + fd = -1 + f.write(secret) + os.replace(tmp_path, path) + except Exception: + if fd >= 0: + os.close(fd) + tmp_path.unlink(missing_ok = True) + raise + try: + os.chmod(path, 0o600) + except OSError: + pass + + +def _connect_auth_db() -> sqlite3.Connection: + auth_dir = STUDIO_HOME / "auth" + auth_dir.mkdir(parents = True, exist_ok = True) + conn = sqlite3.connect(auth_dir / "auth.db") + conn.execute( + """ + CREATE TABLE IF NOT EXISTS auth_user ( + id INTEGER PRIMARY KEY, + username TEXT UNIQUE NOT NULL, + password_salt TEXT NOT NULL, + password_hash TEXT NOT NULL, + jwt_secret TEXT NOT NULL, + must_change_password INTEGER NOT NULL DEFAULT 0 + ); + """ + ) + conn.execute( + """ + CREATE TABLE IF NOT EXISTS refresh_tokens ( + id INTEGER PRIMARY KEY, + token_hash TEXT NOT NULL, + username TEXT NOT NULL, + expires_at TEXT NOT NULL, + is_desktop INTEGER NOT NULL DEFAULT 0 + ); + """ + ) + conn.execute( + """ + CREATE TABLE IF NOT EXISTS api_keys ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL, + key_prefix TEXT NOT NULL, + key_hash TEXT NOT NULL UNIQUE, + name TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL, + last_used_at TEXT, + expires_at TEXT, + is_active INTEGER NOT NULL DEFAULT 1 + ); + """ + ) + conn.execute( + """ + CREATE TABLE IF NOT EXISTS app_secrets ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ); + """ + ) + auth_columns = {row[1] for row in conn.execute("PRAGMA table_info(auth_user)")} + if "must_change_password" not in auth_columns: + conn.execute( + "ALTER TABLE auth_user ADD COLUMN must_change_password INTEGER NOT NULL DEFAULT 0" + ) + refresh_columns = { + row[1] for row in conn.execute("PRAGMA table_info(refresh_tokens)") + } + if "is_desktop" not in refresh_columns: + conn.execute( + "ALTER TABLE refresh_tokens ADD COLUMN is_desktop INTEGER NOT NULL DEFAULT 0" + ) + conn.commit() + return conn + + +def _pbkdf2_hex(value: str, salt: bytes) -> str: + return hashlib.pbkdf2_hmac( + "sha256", + value.encode("utf-8"), + salt, + PBKDF2_ITERATIONS, + ).hex() + + +def _hash_password(password: str) -> tuple[str, str]: + salt = secrets.token_hex(16) + pwd_hash = _pbkdf2_hex(password, salt.encode("utf-8")) + return salt, pwd_hash + + +def _get_or_create_api_key_pbkdf2_salt(conn: sqlite3.Connection) -> bytes: + row = conn.execute( + "SELECT value FROM app_secrets WHERE key = ?", + (API_KEY_PBKDF2_SALT_KEY,), + ).fetchone() + if row is None: + salt_hex = secrets.token_hex(32) + conn.execute( + "INSERT OR IGNORE INTO app_secrets (key, value) VALUES (?, ?)", + (API_KEY_PBKDF2_SALT_KEY, salt_hex), + ) + row = conn.execute( + "SELECT value FROM app_secrets WHERE key = ?", + (API_KEY_PBKDF2_SALT_KEY,), + ).fetchone() + return bytes.fromhex(row[0]) + + +def _ensure_cli_default_admin(conn: sqlite3.Connection) -> None: + row = conn.execute( + "SELECT 1 FROM auth_user WHERE username = ?", + (DEFAULT_ADMIN_USERNAME,), + ).fetchone() + if row is not None: + return + + bootstrap_password = secrets.token_urlsafe(32) + password_salt, password_hash = _hash_password(bootstrap_password) + conn.execute( + """ + INSERT INTO auth_user ( + username, + password_salt, + password_hash, + jwt_secret, + must_change_password + ) + VALUES (?, ?, ?, ?, ?) + """, + ( + DEFAULT_ADMIN_USERNAME, + password_salt, + password_hash, + secrets.token_urlsafe(64), + 1, + ), + ) + _write_auth_secret( + STUDIO_HOME / "auth" / BOOTSTRAP_PASSWORD_FILE, + bootstrap_password, + ) + + +def _create_desktop_secret_in_cli() -> str: + raw_secret = DESKTOP_SECRET_PREFIX + secrets.token_urlsafe(48) + now = datetime.now(timezone.utc).isoformat() + conn = _connect_auth_db() + try: + _ensure_cli_default_admin(conn) + secret_hash = _pbkdf2_hex(raw_secret, _get_or_create_api_key_pbkdf2_salt(conn)) + conn.execute( + "INSERT OR REPLACE INTO app_secrets (key, value) VALUES (?, ?)", + (DESKTOP_SECRET_HASH_KEY, secret_hash), + ) + conn.execute( + "INSERT OR REPLACE INTO app_secrets (key, value) VALUES (?, ?)", + (DESKTOP_SECRET_CREATED_AT_KEY, now), + ) + conn.commit() + return raw_secret + finally: + conn.close() + + def _load_model_via_http( port: int, api_key: str, @@ -153,6 +417,11 @@ def studio_default( host: str = typer.Option("0.0.0.0", "--host", "-H"), frontend: Optional[Path] = typer.Option(None, "--frontend", "-f"), silent: bool = typer.Option(False, "--silent", "-q"), + api_only: bool = typer.Option( + False, + "--api-only", + help = "Run API server only, no frontend serving (for Tauri desktop app)", + ), ): """Launch the Unsloth Studio server.""" if ctx.invoked_subcommand is not None: @@ -180,13 +449,15 @@ def studio_default( args.extend(["--frontend", str(frontend)]) if silent: args.append("--silent") + if api_only: + args.append("--api-only") # On Windows, os.execvp() spawns a child but the parent lingers, # so Ctrl+C only kills the parent leaving the child orphaned. # Use subprocess.run() on Windows so the parent waits for the child. if sys.platform == "win32": import subprocess as _sp - proc = _sp.Popen(args) + proc = _sp.Popen(args, **_windows_hidden_subprocess_kwargs()) try: rc = proc.wait() except KeyboardInterrupt: @@ -217,7 +488,7 @@ def studio_default( display_host = _resolve_external_ip() if host == "0.0.0.0" else host typer.echo(f"Starting Unsloth Studio on http://{display_host}:{port}") - run_kwargs = dict(host = host, port = port, silent = silent) + run_kwargs = dict(host = host, port = port, silent = silent, api_only = api_only) if frontend is not None: run_kwargs["frontend_path"] = frontend run_server(**run_kwargs) @@ -489,9 +760,16 @@ def _run_setup_script(*, verbose: bool = False) -> None: env = {**os.environ, "UNSLOTH_VERBOSE": "1"} if verbose else None if platform.system() == "Windows": + powershell_args = ["powershell.exe"] + if _should_hide_windows_subprocesses(): + powershell_args.extend( + ["-NoLogo", "-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden"] + ) + powershell_args.extend(["-ExecutionPolicy", "Bypass", "-File", str(script)]) result = subprocess.run( - ["powershell", "-ExecutionPolicy", "Bypass", "-File", str(script)], + powershell_args, env = env, + **_windows_hidden_subprocess_kwargs(), ) else: result = subprocess.run(["bash", str(script)], env = env) @@ -547,6 +825,44 @@ def update( # ── unsloth studio reset-password ──────────────────────────────────── +@studio_app.command("desktop-capabilities", hidden = True) +def desktop_capabilities( + json_output: bool = typer.Option( + False, + "--json", + help = "Emit machine-readable JSON.", + ), +): + payload = { + "desktop_protocol_version": 1, + "supports_provision_desktop_auth": True, + "supports_api_only": True, + "version": "unknown", + } + try: + from importlib.metadata import version as package_version + + payload["version"] = package_version("unsloth") + except Exception: + pass + + if json_output: + typer.echo(json.dumps(payload, sort_keys = True)) + return + + for key, value in payload.items(): + typer.echo(f"{key}: {value}") + + +@studio_app.command("provision-desktop-auth", hidden = True) +def provision_desktop_auth(): + """Create/repair desktop auth state for the local machine.""" + auth_dir = STUDIO_HOME / "auth" + secret = _create_desktop_secret_in_cli() + _write_auth_secret(auth_dir / DESKTOP_SECRET_FILE, secret) + typer.echo("Desktop auth ready.") + + @studio_app.command("reset-password") def reset_password(): """Reset the Studio admin password. @@ -557,13 +873,18 @@ def reset_password(): """ auth_dir = STUDIO_HOME / "auth" db_file = auth_dir / "auth.db" - pw_file = auth_dir / ".bootstrap_password" + stale_files = [ + auth_dir / BOOTSTRAP_PASSWORD_FILE, + auth_dir / DESKTOP_SECRET_FILE, + ] + had_db = db_file.exists() - if not db_file.exists(): + db_file.unlink(missing_ok = True) + for path in stale_files: + path.unlink(missing_ok = True) + + if not had_db: typer.echo("No auth database found -- nothing to reset.") raise typer.Exit(0) - db_file.unlink(missing_ok = True) - pw_file.unlink(missing_ok = True) - typer.echo("Auth database deleted. Restart Unsloth Studio to get a new password.")