diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a318cb24..142d03f9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,6 +15,9 @@ concurrency: group: release-${{ github.ref }} cancel-in-progress: false +permissions: + contents: read + jobs: verify: runs-on: ubuntu-latest @@ -47,8 +50,110 @@ jobs: grep -q '^package/dist/' files.txt || { echo "::error::tarball has no dist/"; exit 1; } if grep -Ei '\.env|secret' files.txt; then echo "::error::tarball contains secrets"; exit 1; fi - publish: + standalone: + name: Standalone (${{ matrix.platform }}) needs: verify + if: ${{ !contains(inputs.tag || github.ref_name, '-') }} + strategy: + fail-fast: false + matrix: + include: + - platform: darwin-arm64 + runner: macos-14 + - platform: darwin-x64 + runner: macos-15-intel + - platform: linux-x64 + runner: ubuntu-latest + - platform: linux-arm64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@v5 + with: + ref: ${{ inputs.tag || github.ref }} + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.14 + + - run: bun install --frozen-lockfile + + - name: Build standalone runtime + env: + PUBLIC_TLDRAW_LICENSE_KEY: ${{ vars.PUBLIC_TLDRAW_LICENSE_KEY }} + run: | + [ -n "$PUBLIC_TLDRAW_LICENSE_KEY" ] || { echo "::error::PUBLIC_TLDRAW_LICENSE_KEY variable is not set"; exit 1; } + bun scripts/build-standalone.ts --outdir dist-standalone + + - uses: actions/upload-artifact@v4 + with: + name: moi-standalone-${{ matrix.platform }} + path: dist-standalone/moi-standalone-* + if-no-files-found: error + + desktop: + name: Desktop (${{ matrix.platform }}) + needs: standalone + if: ${{ !contains(inputs.tag || github.ref_name, '-') }} + strategy: + fail-fast: false + matrix: + include: + - platform: darwin-arm64 + runner: macos-14 + - platform: linux-x64 + runner: ubuntu-latest + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@v5 + with: + ref: ${{ inputs.tag || github.ref }} + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.14 + + - uses: dtolnay/rust-toolchain@stable + + - name: Install webview dependencies + if: startsWith(matrix.platform, 'linux') + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf + + - run: bun install --frozen-lockfile + + - uses: actions/download-artifact@v4 + with: + name: moi-standalone-${{ matrix.platform }} + path: dist-standalone + + - name: Build desktop app + run: | + tarball=$(find dist-standalone -name "moi-standalone-*-${{ matrix.platform }}.tar.gz" -type f -print -quit) + test -n "$tarball" + bun scripts/build-desktop.ts --runtime "$tarball" + + - uses: actions/upload-artifact@v4 + with: + name: moi-desktop-${{ matrix.platform }} + path: | + desktop/src-tauri/target/release/bundle/dmg/*.dmg + desktop/src-tauri/target/release/bundle/appimage/*.AppImage + desktop/src-tauri/target/release/bundle/deb/*.deb + if-no-files-found: error + + publish: + needs: [verify, standalone, desktop] + if: >- + ${{ + always() && + needs.verify.result == 'success' && + ( + contains(inputs.tag || github.ref_name, '-') || + (needs.standalone.result == 'success' && needs.desktop.result == 'success') + ) + }} runs-on: ubuntu-latest # Gates the job on the `release` environment's reviewers, and is also the # environment claim npm's trusted publisher config matches against. @@ -96,3 +201,48 @@ jobs: run: | [ -n "$PUBLIC_TLDRAW_LICENSE_KEY" ] || { echo "::error::PUBLIC_TLDRAW_LICENSE_KEY variable is not set"; exit 1; } npm publish --tag ${{ steps.meta.outputs.dist_tag }} + + github-release: + name: Publish GitHub release + needs: [publish, standalone, desktop] + if: ${{ !contains(inputs.tag || github.ref_name, '-') }} + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v5 + with: + ref: ${{ inputs.tag || github.ref }} + + - uses: actions/download-artifact@v4 + with: + pattern: moi-* + path: release-assets + merge-multiple: true + + - name: Verify complete release payload + run: | + version=$(jq -r .version package.json) + for platform in darwin-arm64 darwin-x64 linux-x64 linux-arm64; do + asset="moi-standalone-$version-$platform.tar.gz" + test -f "release-assets/$asset" + test -f "release-assets/$asset.sha256" + (cd release-assets && sha256sum -c "$asset.sha256") + done + test "$(find release-assets -type f -name '*.dmg' | wc -l)" -ge 1 + test "$(find release-assets -type f -name '*.AppImage' | wc -l)" -ge 1 + test "$(find release-assets -type f -name '*.deb' | wc -l)" -ge 1 + + - name: Stage draft release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ inputs.tag || github.ref_name }} + draft: true + fail_on_unmatched_files: true + files: release-assets/**/* + + - name: Publish complete release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_TAG: ${{ inputs.tag || github.ref_name }} + run: gh release edit "$RELEASE_TAG" --draft=false --latest diff --git a/.gitignore b/.gitignore index c0a61819..3cf02386 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,14 @@ server/test/__fixtures__/**/.build/ # prebuilt client (generated by prepack) dist/ + +# standalone distribution artifacts (scripts/build-standalone.ts) +dist-standalone/ + +# desktop shell build artifacts (scripts/build-desktop.ts) +desktop/src-tauri/target/ +desktop/src-tauri/resources/moi-runtime.tar.gz +desktop/src-tauri/gen/ +!desktop/dist/ +desktop/dist/* +!desktop/dist/.gitkeep diff --git a/AGENTS.md b/AGENTS.md index cb2f05fb..f37be218 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -59,6 +59,10 @@ Only one server runs at a time — it binds port 13337 (HTTP) and 13059 (control All three overwrite `~/.bun/bin/moi` — last action wins; `readlink ~/.bun/bin/moi` shows which. Kill the running server before switching (single port 13337). +### Standalone distribution (no Bun required) + +`docs/standalone.md` covers the fourth install form: a self-contained runtime tree under `~/.moi` (pinned bun + package + production node_modules), installed by `packaging/install.sh`, updated by `moi update` (`server/standalone.ts`), built per platform by `scripts/build-standalone.ts`, and wrapped by the Tauri desktop shell in `desktop/` (`scripts/build-desktop.ts`). Its `moi` shim lives at `~/.moi/bin/moi`, separate from `~/.bun/bin/moi`. + ## Browser testing in cloud sandboxes To drive the app in a browser inside Claude Code on the web, use the vendored **agent-browser skill** (`.agents/skills/agent-browser/`). Cloud-specific setup — server startup, `AGENT_BROWSER_EXECUTABLE_PATH`, Playwright alternative, egress-relay caveats — is in `docs/browser-testing-cloud.md`. diff --git a/README.md b/README.md index 8b86ac37..a17f48a4 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,17 @@ bun i -g moi-computer moi start # http://localhost:13337 ``` +The repository also contains a no-Bun standalone CLI and desktop app for macOS +and Linux. Use them once the +[latest release](https://github.com/molefrog/moi/releases/latest) lists +`moi-standalone-*` assets; see [docs/standalone.md](docs/standalone.md). The +installer fails without changing the machine when a platform build or checksum +is absent: + +```sh +curl -fsSL https://raw.githubusercontent.com/molefrog/moi/main/packaging/install.sh | sh +``` + Then bring in your project, either way works: - open [http://localhost:13337](http://localhost:13337) and create a workspace @@ -110,11 +121,13 @@ moi update # update to the latest release moi update --check # only check; exit 0 up to date, 1 update available, 2 check failed ``` -`moi update` checks npm for the latest release and updates through whichever -package manager owns the install (bun, npm, pnpm, or yarn). A service-managed -server is restarted onto the new version; a foreground `moi start` only gets -a warning — restart it yourself. Prerelease installs (`…-next.N`) are left -alone. `--check` changes nothing and is made for scripts and agents. +For package-manager installs, `moi update` checks npm for the latest release and +updates through whichever package manager owns the install (bun, npm, pnpm, or +yarn). Standalone installs use the matching GitHub release asset and checksum. +A service-managed server is restarted onto the new version; a foreground +`moi start` only gets a warning — restart it yourself. Prerelease installs +(`…-next.N`) are left alone. `--check` changes nothing and is made for scripts +and agents. `moi status` shows when the running server and CLI versions differ, however the update happened. diff --git a/desktop/README.md b/desktop/README.md new file mode 100644 index 00000000..e59c7536 --- /dev/null +++ b/desktop/README.md @@ -0,0 +1,55 @@ +# moi desktop + +A thin Tauri v2 shell around the standalone moi runtime. It contains no +product code: on launch it provisions the same `~/.moi` runtime tree the CLI +installer uses (extracting the bundled `moi-runtime.tar.gz` on first launch, +which also puts the `moi` command in `~/.moi/bin`), starts the `moi service` if +none is running, and opens a webview at `http://localhost:13337`. + +All product iteration happens in `client/` and `server/` — a feature shipped +there appears in this app with no changes here. If a moi server is already +running (for example started with `moi start`), the app attaches to it. The +service stays available after the window closes; when service installation is +unavailable, the app falls back to an app-owned server that stops on quit. + +`moi update` updates the shared runtime for both the CLI and this app. + +## Visual ownership + +The normal desktop window is the existing React client served by moi; the shell +does not maintain a separate application UI. `desktop/ui/index.html` is only a +temporary boot and error surface shown while the local server starts. Its build +imports the main client's Tailwind theme directly. + +Desktop branding comes from `client/assets/favicon.png`. The boot page bundles +that canonical asset, and the committed platform icon set under +`src-tauri/icons/` is generated from it with +`bun scripts/build-desktop-icons.ts`. Do not add a desktop-only logo, loading +animation, palette, or type system. Product loading states after the server +starts remain owned by the client (`HomeLogo` and `LedLogo`). + +## Build + +Requires Rust (`rustup` or `brew install rust`) plus the platform WebView +(preinstalled on macOS; `webkit2gtk` on Linux). + +```sh +bun scripts/build-desktop.ts +``` + +This builds the standalone runtime tarball for the current platform (or +reuses `dist-standalone/` / a `--runtime ` argument), stages it as the +Tauri resource, and runs `tauri build`. Bundles land in +`desktop/src-tauri/target/release/bundle/`. + +## macOS Gatekeeper + +Release builds are ad-hoc signed, not notarized (no Apple Developer account). +A `.dmg` downloaded with a browser is quarantined; first launch needs either +right-click → Open, or: + +```sh +xattr -d com.apple.quarantine /Applications/moi.app +``` + +Installs via the `curl` installer are not quarantined and launch without this. diff --git a/desktop/dist/.gitkeep b/desktop/dist/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock new file mode 100644 index 00000000..4789dec2 --- /dev/null +++ b/desktop/src-tauri/Cargo.lock @@ -0,0 +1,4383 @@ +# 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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +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.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[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.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +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 = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6aedf8ae72766347502cf3cb4f41cf5e9cc37d28bee90f1fdaaae15f9cf9424" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +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.13.1", + "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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2d30e4173c4026932d51d31d6b0613b1fd3014bf3f9f8943d4ba139c437ba0" +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.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" +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 = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +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 = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.1", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.1", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[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.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "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.119", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[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.119", +] + +[[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.119", +] + +[[package]] +name = "dbus" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[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.119", +] + +[[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.13.1", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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.119", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[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 = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.3+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 = "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 = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[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 = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[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.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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 = "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-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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 = "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.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[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.13.1", + "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.119", +] + +[[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.119", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[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 = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +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.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[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", + "tokio", + "tower-service", + "tracing", +] + +[[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 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +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.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.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 = "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.119", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "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.13.1", + "serde", + "unicode-segmentation", +] + +[[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.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[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.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +dependencies = [ + "libc", +] + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[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.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[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 = "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.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moi-desktop" +version = "0.1.0" +dependencies = [ + "libc", + "serde_json", + "tauri", + "tauri-build", +] + +[[package]] +name = "muda" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.1", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[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 = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[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.119", +] + +[[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.13.1", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.1", + "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.13.1", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[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.13.1", + "block2", + "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.13.1", + "objc2", + "objc2-core-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.13.1", + "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.13.1", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "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.13.1", + "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 = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[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_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", + "smallvec", + "windows-link 0.2.1", +] + +[[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.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plist" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.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 = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.13+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-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +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 = "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.13.1", +] + +[[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.119", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.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.119", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.13.1", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +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.119", +] + +[[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.119", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +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.119", +] + +[[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.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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.119", +] + +[[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", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +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", + "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.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[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", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +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.119", +] + +[[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.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.13.1", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "667b20e2726d572dea2de7370da16e188eb06008faf9a92fab7cdc46791190b5" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.119", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f" +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", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 1.1.3+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.3+spec-1.1.0", +] + +[[package]] +name = "tendril" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" +dependencies = [ + "new_debug_unreachable", +] + +[[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.119", +] + +[[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.119", +] + +[[package]] +name = "time" +version = "0.3.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[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.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[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.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +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.14.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.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[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.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[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-core", +] + +[[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.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.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.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[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.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[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 = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[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 = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[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 = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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.119", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.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.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", + "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-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_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_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_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[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_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_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_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 = "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" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +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.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +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", + "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 = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml new file mode 100644 index 00000000..e2ccc6b4 --- /dev/null +++ b/desktop/src-tauri/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "moi-desktop" +version = "0.1.0" +description = "moi desktop shell" +edition = "2021" +license = "Elastic-2.0" + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = [] } +serde_json = "1" +libc = "0.2" + +[profile.release] +strip = true +lto = true diff --git a/desktop/src-tauri/build.rs b/desktop/src-tauri/build.rs new file mode 100644 index 00000000..d860e1e6 --- /dev/null +++ b/desktop/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/desktop/src-tauri/icons/128x128.png b/desktop/src-tauri/icons/128x128.png new file mode 100644 index 00000000..f2763117 Binary files /dev/null and b/desktop/src-tauri/icons/128x128.png differ diff --git a/desktop/src-tauri/icons/128x128@2x.png b/desktop/src-tauri/icons/128x128@2x.png new file mode 100644 index 00000000..0bf70696 Binary files /dev/null and b/desktop/src-tauri/icons/128x128@2x.png differ diff --git a/desktop/src-tauri/icons/32x32.png b/desktop/src-tauri/icons/32x32.png new file mode 100644 index 00000000..ebaa02d1 Binary files /dev/null and b/desktop/src-tauri/icons/32x32.png differ diff --git a/desktop/src-tauri/icons/64x64.png b/desktop/src-tauri/icons/64x64.png new file mode 100644 index 00000000..f39e3a1f Binary files /dev/null and b/desktop/src-tauri/icons/64x64.png differ diff --git a/desktop/src-tauri/icons/Square107x107Logo.png b/desktop/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 00000000..ff52df63 Binary files /dev/null and b/desktop/src-tauri/icons/Square107x107Logo.png differ diff --git a/desktop/src-tauri/icons/Square142x142Logo.png b/desktop/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 00000000..b3b13aed Binary files /dev/null and b/desktop/src-tauri/icons/Square142x142Logo.png differ diff --git a/desktop/src-tauri/icons/Square150x150Logo.png b/desktop/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 00000000..884958b1 Binary files /dev/null and b/desktop/src-tauri/icons/Square150x150Logo.png differ diff --git a/desktop/src-tauri/icons/Square284x284Logo.png b/desktop/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 00000000..1d5467e0 Binary files /dev/null and b/desktop/src-tauri/icons/Square284x284Logo.png differ diff --git a/desktop/src-tauri/icons/Square30x30Logo.png b/desktop/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 00000000..903fb797 Binary files /dev/null and b/desktop/src-tauri/icons/Square30x30Logo.png differ diff --git a/desktop/src-tauri/icons/Square310x310Logo.png b/desktop/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 00000000..fbe6eb9c Binary files /dev/null and b/desktop/src-tauri/icons/Square310x310Logo.png differ diff --git a/desktop/src-tauri/icons/Square44x44Logo.png b/desktop/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 00000000..52d1610b Binary files /dev/null and b/desktop/src-tauri/icons/Square44x44Logo.png differ diff --git a/desktop/src-tauri/icons/Square71x71Logo.png b/desktop/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 00000000..700f9c26 Binary files /dev/null and b/desktop/src-tauri/icons/Square71x71Logo.png differ diff --git a/desktop/src-tauri/icons/Square89x89Logo.png b/desktop/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 00000000..fb30b283 Binary files /dev/null and b/desktop/src-tauri/icons/Square89x89Logo.png differ diff --git a/desktop/src-tauri/icons/StoreLogo.png b/desktop/src-tauri/icons/StoreLogo.png new file mode 100644 index 00000000..342a0689 Binary files /dev/null and b/desktop/src-tauri/icons/StoreLogo.png differ diff --git a/desktop/src-tauri/icons/icon.icns b/desktop/src-tauri/icons/icon.icns new file mode 100644 index 00000000..8ad25ecb Binary files /dev/null and b/desktop/src-tauri/icons/icon.icns differ diff --git a/desktop/src-tauri/icons/icon.ico b/desktop/src-tauri/icons/icon.ico new file mode 100644 index 00000000..c56701ac Binary files /dev/null and b/desktop/src-tauri/icons/icon.ico differ diff --git a/desktop/src-tauri/icons/icon.png b/desktop/src-tauri/icons/icon.png new file mode 100644 index 00000000..facb5025 Binary files /dev/null and b/desktop/src-tauri/icons/icon.png differ diff --git a/desktop/src-tauri/src/main.rs b/desktop/src-tauri/src/main.rs new file mode 100644 index 00000000..2313b5e3 --- /dev/null +++ b/desktop/src-tauri/src/main.rs @@ -0,0 +1,651 @@ +// moi desktop shell — a thin Tauri v2 wrapper around the standalone runtime. +// +// All product code lives in the main codebase (server/ + client/); this shell +// only (1) provisions the same ~/.moi runtime tree the CLI installer uses, +// extracting the bundled moi-runtime.tar.gz on first launch, (2) starts the +// server if none is running, and (3) points a webview at it. `moi update` +// updates the runtime for both the CLI and this app; the shell itself should +// almost never need changes. +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use std::fs; +use std::io::{Read, Write}; +use std::net::TcpStream; +use std::os::unix::fs::PermissionsExt; +use std::path::{Path, PathBuf}; +use std::process::{Child, Command}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Mutex; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +use tauri::{Manager, RunEvent, WindowEvent}; + +const BOOT_TIMEOUT: Duration = Duration::from_secs(60); +const LOCK_TIMEOUT: Duration = Duration::from_secs(60); + +// Honor the server's PORT env seam (server/constants.ts) so an isolated +// instance — e.g. first-launch testing beside a dev server — works end to +// end: the same env var steers this shell's probe/URL and, inherited by the +// spawned child, the server itself. +fn port() -> u16 { + std::env::var("PORT") + .ok() + .and_then(|p| p.parse().ok()) + .unwrap_or(13337) +} + +// The server child process, present only when this app spawned it (an already +// running server — e.g. started from the CLI — is left untouched on quit). +struct SpawnedServer(Mutex>); + +// The retry button can be invoked from JavaScript, so keep repeated requests +// from racing runtime extraction or starting more than one server. +struct Booting(AtomicBool); + +fn normalize_moi_home(path: &Path, user_home: &Path) -> Result { + if !path.is_absolute() { + return Err("MOI_HOME must be an absolute path".into()); + } + + let mut normalized = PathBuf::from("/"); + for component in path.components() { + match component { + std::path::Component::RootDir | std::path::Component::CurDir => {} + std::path::Component::ParentDir => { + normalized.pop(); + } + std::path::Component::Normal(part) => normalized.push(part), + std::path::Component::Prefix(_) => return Err("MOI_HOME must be a Unix path".into()), + } + } + if normalized == Path::new("/") || normalized == user_home { + return Err(format!( + "Refusing to use unsafe MOI_HOME: {}", + normalized.display() + )); + } + Ok(normalized) +} + +fn moi_home() -> Result { + let user_home = PathBuf::from( + std::env::var("HOME").map_err(|_| "HOME is not set for the desktop app".to_string())?, + ); + let requested = std::env::var("MOI_HOME") + .map(PathBuf::from) + .unwrap_or_else(|_| user_home.join(".moi")); + normalize_moi_home(&requested, &user_home) +} + +fn port_open() -> bool { + port_open_at(port()) +} + +fn port_open_at(target_port: u16) -> bool { + TcpStream::connect_timeout( + &([127, 0, 0, 1], target_port).into(), + Duration::from_millis(300), + ) + .is_ok() +} + +// Do not treat an arbitrary process on PORT as moi. Besides opening the wrong +// page in the webview, that would skip first-launch provisioning entirely. +fn moi_server_alive() -> bool { + moi_server_alive_at(port()) +} + +fn moi_server_alive_at(target_port: u16) -> bool { + let Ok(mut stream) = TcpStream::connect_timeout( + &([127, 0, 0, 1], target_port).into(), + Duration::from_millis(300), + ) else { + return false; + }; + let timeout = Some(Duration::from_millis(500)); + if stream.set_read_timeout(timeout).is_err() || stream.set_write_timeout(timeout).is_err() { + return false; + } + let request = b"GET /status HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n"; + if stream.write_all(request).is_err() { + return false; + } + let mut response = Vec::new(); + let mut chunk = [0_u8; 1024]; + while response.len() < 4096 { + match stream.read(&mut chunk) { + Ok(0) => break, + Ok(read) => { + response.extend_from_slice(&chunk[..read]); + if response + .windows(b"moi server status\n".len()) + .any(|window| window == b"moi server status\n") + { + break; + } + } + Err(error) + if matches!( + error.kind(), + std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut + ) => + { + break + } + Err(_) => return false, + } + } + let response = String::from_utf8_lossy(&response); + response.starts_with("HTTP/1.1 200") && response.contains("moi server status\n") +} + +// Version of the runtime a `current` symlink points at, read from its +// app/package.json. None when nothing is provisioned (or the tree is torn). +fn installed_version(current: &Path) -> Option { + let pkg = fs::read_to_string(current.join("app/package.json")).ok()?; + serde_json::from_str::(&pkg) + .ok()? + .get("version")? + .as_str() + .map(String::from) +} + +fn stable_version(version: &str) -> Option<[u64; 3]> { + let parts = version.split('.').collect::>(); + if parts.len() != 3 + || parts.iter().any(|part| { + part.is_empty() + || (part.len() > 1 && part.starts_with('0')) + || !part.bytes().all(|byte| byte.is_ascii_digit()) + }) + { + return None; + } + Some([ + parts[0].parse().ok()?, + parts[1].parse().ok()?, + parts[2].parse().ok()?, + ]) +} + +fn process_is_alive(pid: u32) -> bool { + let result = unsafe { libc::kill(pid as i32, 0) }; + result == 0 || std::io::Error::last_os_error().raw_os_error() == Some(libc::EPERM) +} + +fn lock_is_stale(lock: &Path) -> bool { + let Ok(owner) = fs::read_link(lock) else { + return true; + }; + let owner = owner.to_string_lossy(); + let Some(pid) = owner.split('-').next().and_then(|value| value.parse().ok()) else { + return true; + }; + !process_is_alive(pid) +} + +struct RuntimeLock { + path: PathBuf, + owner: String, +} + +impl Drop for RuntimeLock { + fn drop(&mut self) { + if fs::read_link(&self.path).ok().as_deref() == Some(Path::new(&self.owner)) { + let _ = fs::remove_file(&self.path); + } + } +} + +fn acquire_runtime_lock(runtime: &Path) -> Result { + fs::create_dir_all(runtime).map_err(|e| e.to_string())?; + let lock = runtime.join(".install-lock"); + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_nanos(); + let owner = format!("{}-{nonce}", std::process::id()); + let deadline = Instant::now() + LOCK_TIMEOUT; + + loop { + match std::os::unix::fs::symlink(&owner, &lock) { + Ok(()) => return Ok(RuntimeLock { path: lock, owner }), + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {} + Err(error) => return Err(format!("could not acquire the runtime lock: {error}")), + } + + if lock_is_stale(&lock) { + let stale = runtime.join(format!( + ".stale-lock-{}-{}", + std::process::id(), + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_nanos() + )); + if fs::rename(&lock, &stale).is_ok() { + let _ = remove_path(&stale); + continue; + } + } + + if Instant::now() >= deadline { + return Err("another moi install or update is still running".into()); + } + std::thread::sleep(Duration::from_millis(250)); + } +} + +fn remove_path(path: &Path) -> Result<(), String> { + let Ok(metadata) = fs::symlink_metadata(path) else { + return Ok(()); + }; + if metadata.file_type().is_dir() { + fs::remove_dir_all(path).map_err(|e| e.to_string()) + } else { + fs::remove_file(path).map_err(|e| e.to_string()) + } +} + +struct CleanupPath(PathBuf); + +impl Drop for CleanupPath { + fn drop(&mut self) { + let _ = remove_path(&self.0); + } +} + +fn prune_versions(runtime: &Path, keep: &[&str]) -> Result<(), String> { + for entry in fs::read_dir(runtime).map_err(|e| e.to_string())? { + let entry = entry.map_err(|e| e.to_string())?; + let name = entry.file_name().to_string_lossy().to_string(); + if name == "current" || name.starts_with('.') || keep.contains(&name.as_str()) { + continue; + } + remove_path(&entry.path())?; + } + Ok(()) +} + +fn shell_quote(value: &str) -> String { + format!("'{}'", value.replace('\'', "'\\''")) +} + +fn write_shim(home: &Path) -> Result<(), String> { + let bin = home.join("bin"); + fs::create_dir_all(&bin).map_err(|e| e.to_string())?; + let shim = bin.join("moi"); + let default_home = shell_quote(&home.to_string_lossy()); + fs::write( + &shim, + format!( + "#!/bin/sh\nDEFAULT_MOI_HOME={default_home}\nMOI_HOME=\"${{MOI_HOME:-$DEFAULT_MOI_HOME}}\"\nexport MOI_HOME\nexport MOI_STANDALONE_HOME=\"$MOI_HOME\"\nexec \"$MOI_HOME/runtime/current/bun\" \"$MOI_HOME/runtime/current/app/server/cli.ts\" \"$@\"\n" + ), + ) + .map_err(|e| e.to_string())?; + let mut permissions = fs::metadata(&shim) + .map_err(|e| e.to_string())? + .permissions(); + permissions.set_mode(0o755); + fs::set_permissions(&shim, permissions).map_err(|e| e.to_string()) +} + +// Provision ~/.moi/runtime/current from the bundled tarball when nothing is +// installed, or when this app bundles a newer runtime than `current` (a newer +// .app must not keep silently running the old runtime). Never downgrades a +// runtime that `moi update` moved past the bundle. Mirrors +// packaging/install.sh: stage, version dir, atomic `current` flip, and the +// CLI shim (so the `moi` command works after installing the app). +fn ensure_runtime(resource_dir: &Path, home: &Path, bundled: &str) -> Result { + let runtime = home.join("runtime"); + let current = runtime.join("current"); + let tarball = resource_dir.join("resources/moi-runtime.tar.gz"); + if !tarball.exists() { + return Err(format!("bundled runtime missing at {}", tarball.display())); + } + let bundled_version = stable_version(bundled) + .ok_or_else(|| format!("bundled runtime version is not stable semver: {bundled}"))?; + + let _lock = acquire_runtime_lock(&runtime)?; + let installed_before = installed_version(¤t); + if let Some(installed) = &installed_before { + if stable_version(installed).is_some_and(|version| bundled_version <= version) { + write_shim(home)?; + return Ok(current); + } + } + + // Pid-scoped so two racing launches can't wipe each other's extraction. + let stage = runtime.join(format!(".stage-{}", std::process::id())); + let _ = fs::remove_dir_all(&stage); + fs::create_dir_all(&stage).map_err(|e| e.to_string())?; + let _stage_cleanup = CleanupPath(stage.clone()); + + let status = Command::new("tar") + .args(["-xzf"]) + .arg(&tarball) + .arg("-C") + .arg(&stage) + .status() + .map_err(|e| format!("tar failed to start: {e}"))?; + if !status.success() { + return Err("could not extract the bundled runtime".into()); + } + + let extracted = stage.join("moi-runtime"); + let pkg = fs::read_to_string(extracted.join("app/package.json")).map_err(|e| e.to_string())?; + let version = serde_json::from_str::(&pkg) + .ok() + .and_then(|v| v["version"].as_str().map(String::from)) + .ok_or("could not read the runtime version")?; + if version != bundled { + return Err(format!( + "bundled payload version {version} does not match desktop version {bundled}" + )); + } + let bun = extracted.join("bun"); + let bun_metadata = fs::metadata(&bun).map_err(|e| e.to_string())?; + if !bun_metadata.is_file() || bun_metadata.permissions().mode() & 0o111 == 0 { + return Err("bundled payload has no executable Bun runtime".into()); + } + if !extracted.join("app/server/cli.ts").is_file() { + return Err("bundled payload has no moi CLI".into()); + } + + let dest = runtime.join(&version); + remove_path(&dest)?; + fs::rename(&extracted, &dest).map_err(|e| e.to_string())?; + let _ = fs::remove_dir_all(&stage); + + let next = runtime.join(format!(".current-next-{}", std::process::id())); + let _ = fs::remove_file(&next); + let _next_cleanup = CleanupPath(next.clone()); + std::os::unix::fs::symlink(&version, &next).map_err(|e| e.to_string())?; + fs::rename(&next, ¤t).map_err(|e| e.to_string())?; + + write_shim(home)?; + let mut keep = vec![version.as_str()]; + if let Some(previous) = installed_before.as_deref() { + if stable_version(previous).is_some() { + keep.push(previous); + } + } + prune_versions(&runtime, &keep)?; + + Ok(current) +} + +// A Finder-launched app inherits launchd's bare PATH (/usr/bin:/bin:…), which +// the server would pass on to agent sessions — stripping the user's tools and +// the `moi` shim agents shell out to. Resolve the login shell's PATH instead +// and put $MOI_HOME/bin on it. +fn server_path(home: &Path) -> String { + let inherited = std::env::var("PATH").unwrap_or_default(); + let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".into()); + let login_path = Command::new(&shell) + .args(["-lc", "printf %s \"$PATH\""]) + .output() + .ok() + .filter(|out| out.status.success()) + .map(|out| String::from_utf8_lossy(&out.stdout).trim().to_string()) + .filter(|path| !path.is_empty()) + .unwrap_or(inherited); + format!("{}/bin:{login_path}", home.display()) +} + +fn moi_command(current: &Path, home: &Path) -> Command { + let mut command = Command::new(current.join("bun")); + command + .arg(current.join("app/server/cli.ts")) + .env("MOI_HOME", home) + .env("MOI_STANDALONE_HOME", home) + .env("PATH", server_path(home)); + command +} + +fn service_install_command(current: &Path, home: &Path) -> Command { + let mut command = moi_command(current, home); + command.args(["service", "install"]); + command +} + +fn install_service(current: &Path, home: &Path) -> Result<(), String> { + let output = service_install_command(current, home) + .output() + .map_err(|e| format!("could not run moi service install: {e}"))?; + if output.status.success() { + return Ok(()); + } + + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); + let detail = if stderr.is_empty() { stdout } else { stderr }; + Err(if detail.is_empty() { + format!("moi service install exited with {}", output.status) + } else { + detail + }) +} + +fn spawn_server(current: &Path, home: &Path) -> Result { + moi_command(current, home) + .arg("start") + .spawn() + .map_err(|e| format!("could not start the moi server: {e}")) +} + +fn status(window: &tauri::WebviewWindow, text: &str) { + let _ = window.eval(format!( + "window.__moiStatus && window.__moiStatus({})", + js_str(text) + )); +} + +fn fail(window: &tauri::WebviewWindow, text: &str) { + let _ = window.eval(format!( + "window.__moiError && window.__moiError({})", + js_str(text) + )); +} + +fn js_str(text: &str) -> String { + serde_json::Value::String(text.into()).to_string() +} + +fn boot(handle: tauri::AppHandle) { + let window = handle + .get_webview_window("main") + .expect("main window from config"); + + // Server already running (CLI-started or a previous launch): just attach + // to it. Otherwise provision the shared runtime and prefer main's service. + if !moi_server_alive() { + if port_open() { + return fail( + &window, + &format!( + "Port {} is in use by another application. Stop that application, then try again.", + port() + ), + ); + } + let home = match moi_home() { + Ok(home) => home, + Err(e) => return fail(&window, &e), + }; + let resource_dir = match handle.path().resource_dir() { + Ok(dir) => dir, + Err(e) => return fail(&window, &format!("resource dir: {e}")), + }; + + let bundled = handle.package_info().version.to_string(); + match installed_version(&home.join("runtime/current")) { + None => status(&window, "Unpacking the moi runtime (first launch)"), + Some(installed) + if stable_version(&bundled) + .zip(stable_version(&installed)) + .is_some_and(|(bundled, installed)| bundled > installed) => + { + status(&window, "Updating the moi runtime") + } + _ => {} + } + let current = match ensure_runtime(&resource_dir, &home, &bundled) { + Ok(dir) => dir, + Err(e) => return fail(&window, &e), + }; + + status(&window, "Starting moi in the background"); + if let Err(service_error) = install_service(¤t, &home) { + eprintln!( + "moi desktop: background service unavailable, using an app-owned server: {service_error}" + ); + status( + &window, + "Background service unavailable; starting while the app is open", + ); + match spawn_server(¤t, &home) { + Ok(child) => *handle.state::().0.lock().unwrap() = Some(child), + Err(e) => return fail(&window, &e), + } + } + } + + let deadline = Instant::now() + BOOT_TIMEOUT; + while !moi_server_alive() { + if Instant::now() > deadline { + return fail( + &window, + "moi did not finish starting. Try again. If it keeps failing, run `moi start` in a terminal to see its output.", + ); + } + std::thread::sleep(Duration::from_millis(250)); + } + + let url = format!("http://localhost:{}", port()); + let _ = window.eval(format!("window.location.replace({})", js_str(&url))); +} + +fn start_boot(handle: tauri::AppHandle) { + if handle.state::().0.swap(true, Ordering::AcqRel) { + return; + } + std::thread::spawn(move || { + boot(handle.clone()); + handle.state::().0.store(false, Ordering::Release); + }); +} + +#[tauri::command] +fn retry_boot(app: tauri::AppHandle) { + start_boot(app); +} + +fn main() { + tauri::Builder::default() + .manage(SpawnedServer(Mutex::new(None))) + .manage(Booting(AtomicBool::new(false))) + .invoke_handler(tauri::generate_handler![retry_boot]) + .setup(|app| { + start_boot(app.handle().clone()); + Ok(()) + }) + .on_window_event(|window, event| { + // One window is the whole app: closing it quits (and tears down a + // server we spawned via the Exit handler below). + if let WindowEvent::CloseRequested { .. } = event { + window.app_handle().exit(0); + } + }) + .build(tauri::generate_context!()) + .expect("error while building moi") + .run(|app, event| { + if let RunEvent::Exit = event { + if let Some(mut child) = app.state::().0.lock().unwrap().take() { + // SIGTERM first so the server closes sockets and kills its + // function workers; SIGKILL only if it lingers. + unsafe { libc::kill(child.id() as i32, libc::SIGTERM) }; + let deadline = Instant::now() + Duration::from_secs(5); + loop { + match child.try_wait() { + Ok(Some(_)) => break, + Ok(None) if Instant::now() > deadline => { + let _ = child.kill(); + break; + } + Ok(None) => std::thread::sleep(Duration::from_millis(100)), + Err(_) => break, + } + } + } + } + }); +} + +#[cfg(test)] +mod tests { + use super::*; + use std::net::TcpListener; + + fn serve_once(body: &'static str) -> u16 { + let listener = TcpListener::bind(("127.0.0.1", 0)).unwrap(); + let port = listener.local_addr().unwrap().port(); + std::thread::spawn(move || { + let (mut stream, _) = listener.accept().unwrap(); + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", + body.len(), + body + ); + stream.write_all(response.as_bytes()).unwrap(); + }); + port + } + + #[test] + fn stable_versions_are_exact_triplets() { + assert_eq!(stable_version("1.2.3"), Some([1, 2, 3])); + assert_eq!(stable_version("1.2"), None); + assert_eq!(stable_version("01.2.3"), None); + assert_eq!(stable_version("1.2.3-next.1"), None); + assert_eq!(stable_version("not-a-version"), None); + } + + #[test] + fn shell_quote_preserves_shell_metacharacters() { + assert_eq!(shell_quote("/tmp/a'b$HOME"), "'/tmp/a'\\''b$HOME'"); + } + + #[test] + fn moi_home_rejects_broad_and_relative_paths() { + let home = Path::new("/Users/test"); + assert!(normalize_moi_home(Path::new("/"), home).is_err()); + assert!(normalize_moi_home(home, home).is_err()); + assert!(normalize_moi_home(Path::new("relative"), home).is_err()); + assert_eq!( + normalize_moi_home(Path::new("/Users/test/tools/../.moi"), home).unwrap(), + Path::new("/Users/test/.moi") + ); + } + + #[test] + fn service_install_runs_through_the_bundled_runtime() { + let command = + service_install_command(Path::new("/runtime/current"), Path::new("/Users/test/.moi")); + assert_eq!(command.get_program(), Path::new("/runtime/current/bun")); + assert_eq!( + command.get_args().collect::>(), + vec![ + std::ffi::OsStr::new("/runtime/current/app/server/cli.ts"), + std::ffi::OsStr::new("service"), + std::ffi::OsStr::new("install") + ] + ); + } + + #[test] + fn health_probe_rejects_an_unrelated_local_service() { + assert!(!moi_server_alive_at(serve_once("not moi\n"))); + assert!(moi_server_alive_at(serve_once("moi server status\n"))); + } +} diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json new file mode 100644 index 00000000..994dd0ca --- /dev/null +++ b/desktop/src-tauri/tauri.conf.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "moi", + "version": "../../package.json", + "identifier": "computer.moi.desktop", + "build": { + "frontendDist": "../dist" + }, + "app": { + "withGlobalTauri": true, + "windows": [ + { + "label": "main", + "url": "index.html", + "title": "moi", + "width": 1280, + "height": 860, + "minWidth": 480, + "minHeight": 480 + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "category": "DeveloperTool", + "targets": ["dmg", "appimage", "deb"], + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.png" + ], + "resources": ["resources/moi-runtime.tar.gz"] + } +} diff --git a/desktop/ui/index.html b/desktop/ui/index.html new file mode 100644 index 00000000..6d252528 --- /dev/null +++ b/desktop/ui/index.html @@ -0,0 +1,94 @@ + + + + + + moi + + + +
+
+ +
+

Starting moi

+ +
+ +
+
+ + + diff --git a/docs/standalone.md b/docs/standalone.md new file mode 100644 index 00000000..902d3692 --- /dev/null +++ b/docs/standalone.md @@ -0,0 +1,80 @@ +# Standalone distribution + +Stable releases can ship in three forms. They all run the same code; pick by +what you have installed: + +| Channel | Install | Update | Needs | +| -------------- | ----------------------------------------------------------------------------------------------- | ------------ | --------- | +| Standalone CLI | `curl -fsSL …/packaging/install.sh \| sh` | `moi update` | curl, tar | +| Desktop app | `.dmg` / `.AppImage` from the [latest release](https://github.com/molefrog/moi/releases/latest) | `moi update` | nothing | +| npm | `bun i -g moi-computer` | `moi update` | Bun ≥ 1.3 | + +Use the standalone CLI or desktop channels only when the latest release lists +the matching `moi-standalone-*` asset and checksum. Releases from before the +standalone distribution was introduced do not contain them; the installer +fails without changing the machine when either file is absent. + +## How the standalone install works + +The standalone build is a self-contained runtime tree — a pinned Bun binary +plus the published package with its production dependencies preinstalled: + +``` +~/.moi/ +├── bin/moi exec shim at a stable path +└── runtime/ + ├── current → 0.7.0 symlink, flipped atomically on update + └── 0.7.0/ + ├── bun exact Bun this release was built and tested with + └── app/ package + node_modules (agent SDK, tailwind, …) +``` + +The `moi` command is a small shell shim that execs the pinned Bun with the real +CLI. Every runtime path — the agent SDK spawning its CLI, applet +bundling with Tailwind, function workers, sharp — behaves exactly like a +normal `bun i -g` install, because from the server's perspective it is one. + +`moi update` downloads a strictly newer stable release for your platform, +requires and verifies its SHA-256, validates the extracted runtime, and flips +the `current` symlink. Install, update, uninstall, and desktop provisioning +share a cross-process lock. The previous version is kept for instant rollback +(re-point the symlink); anything older is pruned. `moi uninstall` removes `~/.moi` +(`--data` also removes the workspace registry and settings; workspace files +are never touched). If the desktop app is still installed, its next launch +re-provisions `~/.moi` from its bundled runtime — delete the app too for a +full removal. + +Set `MOI_HOME` to install somewhere other than `~/.moi`. + +## The desktop app + +The desktop app (`desktop/`) is a thin Tauri shell around the same runtime: +it extracts the bundled runtime into `~/.moi` on first launch (which also +installs the `moi` command), starts the user-level `moi service` if none is +running, and opens a webview at `localhost:13337`. The service keeps moi +available after the window closes. If service installation is unavailable, the +app falls back to an app-owned server that stops on quit. An existing moi server +is reused; an unrelated listener on that port is reported as a collision. See +[desktop/README.md](../desktop/README.md). + +## Release engineering + +Per-platform tarballs are built by `scripts/build-standalone.ts` on matching +CI runners (`.github/workflows/release.yml`) — `node_modules` is +platform-specific (sharp, Tailwind oxide, the agent SDK's native CLI), so +there is no cross-compilation. A stable tag must exactly match the package +version. CI verifies all runtime and desktop artifacts, uploads them to a draft, +then publishes one complete release: + +- `moi-standalone--.tar.gz` (+ `.sha256`) for + darwin-arm64, darwin-x64, linux-x64, linux-arm64 +- `moi__….dmg` / `.AppImage` / `.deb` desktop bundles + +To build any of it locally after pulling changes (no CI needed): +`bun scripts/build-standalone.ts` for the runtime tarball, +`bun scripts/build-desktop.ts` for the desktop bundle. + +macOS artifacts are ad-hoc signed, not notarized. `curl` downloads carry no +quarantine attribute and run as-is; a browser-downloaded `.dmg` needs +right-click → Open (or `xattr -d com.apple.quarantine /Applications/moi.app`) +on first launch. diff --git a/packaging/install.sh b/packaging/install.sh new file mode 100644 index 00000000..a1db2352 --- /dev/null +++ b/packaging/install.sh @@ -0,0 +1,253 @@ +#!/bin/sh +# moi standalone installer +# +# curl -fsSL https://raw.githubusercontent.com/molefrog/moi/main/packaging/install.sh | sh +# +# Installs the self-contained runtime (pinned bun + app + dependencies) into +# $MOI_HOME (default ~/.moi) and puts the `moi` shim on PATH. No Bun, Node, or +# git required. Update later with `moi update`; remove with `moi uninstall`. +set -eu + +REPO="molefrog/moi" +MOI_HOME="${MOI_HOME:-$HOME/.moi}" +tmp="" +lock_path="" +lock_owner="" +stage="" +next="" + +err() { printf '\033[31merror\033[0m: %s\n' "$1" >&2; exit 1; } +info() { printf '%s\n' "$1"; } + +case "$MOI_HOME" in + /*) ;; + *) err "MOI_HOME must be an absolute path" ;; +esac +case "/$MOI_HOME/" in + */../*) err "MOI_HOME must not contain '..' path segments" ;; +esac +[ -n "$(printf '%s' "$MOI_HOME" | tr -d '/')" ] || err "MOI_HOME must not be the filesystem root" +[ "$MOI_HOME" != "$HOME" ] || err "MOI_HOME must not be your home directory" + +shell_quote() { + printf "'" + printf '%s' "$1" | sed "s/'/'\\\\''/g" + printf "'" +} + +release_lock() { + [ -n "$lock_path" ] || return 0 + owner="$(readlink "$lock_path" 2>/dev/null || true)" + if [ "$owner" = "$lock_owner" ]; then rm -f "$lock_path"; fi + lock_path="" +} + +cleanup() { + release_lock + if [ -n "$next" ]; then rm -f "$next"; fi + if [ -n "$stage" ]; then rm -rf "$stage"; fi + if [ -n "$tmp" ]; then rm -rf "$tmp"; fi +} +trap cleanup EXIT +trap 'exit 130' INT +trap 'exit 143' TERM +trap 'exit 129' HUP + +acquire_lock() { + runtime_dir="$1" + lock_path="$runtime_dir/.install-lock" + lock_owner="$$-$(date +%s)" + deadline=$(($(date +%s) + 60)) + + while ! ln -s "$lock_owner" "$lock_path" 2>/dev/null; do + owner="$(readlink "$lock_path" 2>/dev/null || true)" + owner_pid="${owner%%-*}" + stale=0 + case "$owner_pid" in + '' | *[!0-9]*) stale=1 ;; + *) kill -0 "$owner_pid" 2>/dev/null || stale=1 ;; + esac + + if [ "$stale" = 1 ]; then + stale_path="$runtime_dir/.stale-lock-$$-$(date +%s)" + if mv "$lock_path" "$stale_path" 2>/dev/null; then + rm -rf "$stale_path" + continue + fi + fi + + [ "$(date +%s)" -lt "$deadline" ] || err "another moi install or update is still running" + sleep 1 + done +} + +version_is_newer() { + awk -v candidate="$1" -v current="$2" 'BEGIN { + split(candidate, a, "."); split(current, b, ".") + for (i = 1; i <= 3; i++) { + if (length(a[i]) > length(b[i])) exit 0 + if (length(a[i]) < length(b[i])) exit 1 + if (("x" a[i]) > ("x" b[i])) exit 0 + if (("x" a[i]) < ("x" b[i])) exit 1 + } + exit 1 + }' +} + +command -v curl >/dev/null 2>&1 || err "curl is required" +command -v tar >/dev/null 2>&1 || err "tar is required" + +# --- platform --------------------------------------------------------------- +os="$(uname -s)" +arch="$(uname -m)" +case "$os" in + Darwin) os="darwin" ;; + Linux) os="linux" ;; + *) err "unsupported OS: $os (macOS and Linux only)" ;; +esac +case "$arch" in + arm64 | aarch64) arch="arm64" ;; + x86_64 | amd64) arch="x64" ;; + *) err "unsupported architecture: $arch" ;; +esac +platform="$os-$arch" + +# --- resolve latest release (redirect trick, no jq) ------------------------- +info "Resolving latest release" +tag="$(curl --connect-timeout 30 --max-time 60 -fsSLI -o /dev/null -w '%{url_effective}' "https://github.com/$REPO/releases/latest" | sed 's|.*/tag/||')" +[ -n "$tag" ] || err "could not resolve the latest release tag" +version="${tag#v}" +[ "$tag" = "v$version" ] || err "latest release tag is not a stable vX.Y.Z tag: $tag" +printf '%s\n' "$version" | grep -Eq '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$' || + err "latest release tag is not a stable vX.Y.Z tag: $tag" + +asset="moi-standalone-$version-$platform.tar.gz" +base="https://github.com/$REPO/releases/download/$tag" + +# --- download + verify ------------------------------------------------------ +tmp="$(mktemp -d)" + +info "Downloading $asset" +curl --connect-timeout 30 --max-time 1800 -fSL --progress-bar -o "$tmp/$asset" "$base/$asset" || + err "no standalone build for $platform in release $tag" + +curl --connect-timeout 30 --max-time 60 -fsSL -o "$tmp/$asset.sha256" "$base/$asset.sha256" || + err "release $tag has no checksum for $asset — install unchanged" +expected="$(cut -d' ' -f1 <"$tmp/$asset.sha256" | tr '[:upper:]' '[:lower:]')" +printf '%s\n' "$expected" | grep -Eq '^[a-f0-9]{64}$' || err "published checksum is malformed" +if command -v sha256sum >/dev/null 2>&1; then + actual="$(sha256sum "$tmp/$asset" | cut -d' ' -f1)" +elif command -v shasum >/dev/null 2>&1; then + actual="$(shasum -a 256 "$tmp/$asset" | cut -d' ' -f1)" +else + err "sha256sum or shasum is required to verify the download" +fi +[ "$actual" = "$expected" ] || err "checksum mismatch — install unchanged" +info "Checksum verified" + +# --- install ---------------------------------------------------------------- +runtime_dir="$MOI_HOME/runtime" +info "Installing to $runtime_dir/$version" +mkdir -p "$runtime_dir" "$MOI_HOME/bin" +acquire_lock "$runtime_dir" + +previous="$(readlink "$runtime_dir/current" 2>/dev/null || true)" +selected="$version" +install_runtime=1 +if printf '%s\n' "$previous" | grep -Eq '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$'; then + if ! version_is_newer "$version" "$previous"; then + selected="$previous" + install_runtime=0 + info "Keeping installed moi $previous (latest published: $version)" + fi +fi + +if [ "$install_runtime" = 1 ]; then + stage="$runtime_dir/.stage-$$" + next="$runtime_dir/.current-next-$$" + rm -rf "$stage" + rm -f "$next" + mkdir -p "$stage" + tar -xzf "$tmp/$asset" -C "$stage" + + extracted="$stage/moi-runtime" + [ -x "$extracted/bun" ] || err "release payload has no executable Bun runtime" + [ -f "$extracted/app/server/cli.ts" ] || err "release payload has no moi CLI" + payload_version="$(sed -n 's/.*"version":[[:space:]]*"\([^"]*\)".*/\1/p' "$extracted/app/package.json" | head -n 1)" + [ "$payload_version" = "$version" ] || + err "release payload version $payload_version does not match $version" + + rm -rf "$runtime_dir/$version" + mv "$extracted" "$runtime_dir/$version" + rm -rf "$stage" + + ln -s "$version" "$next" + "$runtime_dir/$version/bun" -e \ + 'const { rename } = await import("node:fs/promises"); await rename(process.argv[1], process.argv[2])' \ + "$next" "$runtime_dir/current" + next="" + + stage="" +fi + +# Keep the selected runtime and the version it replaced for rollback. This also +# repairs accumulated old versions during a same-version reinstall. +for entry in "$runtime_dir"/*; do + [ -e "$entry" ] || [ -L "$entry" ] || continue + name="$(basename "$entry")" + case "$name" in + current | "$selected" | "$previous" | "$version") continue ;; + esac + rm -rf "$entry" +done + +# The chosen home is baked in as a shell-escaped default so a custom-MOI_HOME +# install works from fresh shells without interpreting characters in the path. +quoted_home="$(shell_quote "$MOI_HOME")" +cat >"$MOI_HOME/bin/moi" <"$HOME/.config/fish/conf.d/moi.fish" + info "Added $bin_dir to PATH (fish conf.d)" + elif [ -f "$rc" ] && grep -qF "$bin_dir" "$rc"; then + : # PATH line already present from an earlier install — don't append again + else + quoted_bin="$(shell_quote "$bin_dir")" + printf '\n# moi\nexport PATH=%s:"$PATH"\n' "$quoted_bin" >>"$rc" + info "Added $bin_dir to PATH in $rc" + fi + info "Open a new terminal (or 'source' your shell profile) to pick it up" +elif [ "$on_path" = 0 ]; then + info "$bin_dir is not on PATH; add it before running moi" +fi + +info "" +info "moi $selected installed — run 'moi start' to launch" diff --git a/scripts/build-desktop-icons.ts b/scripts/build-desktop-icons.ts new file mode 100644 index 00000000..ac4466ec --- /dev/null +++ b/scripts/build-desktop-icons.ts @@ -0,0 +1,29 @@ +// Regenerate native desktop icons from the main client's canonical app icon. +// The source is deliberately enlarged with nearest-neighbor sampling first so +// the founders' pixel-art treatment stays crisp in large OS icon slots. +import { mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' + +import sharp from 'sharp' + +const ROOT = resolve(import.meta.dir, '..') +const SOURCE = join(ROOT, 'client', 'assets', 'favicon.png') +const OUTPUT = join(ROOT, 'desktop', 'src-tauri', 'icons') +const temporary = await mkdtemp(join(tmpdir(), 'moi-desktop-icons-')) + +try { + const enlarged = join(temporary, 'favicon-1024.png') + await sharp(SOURCE).resize(1024, 1024, { kernel: sharp.kernel.nearest }).png().toFile(enlarged) + + await rm(OUTPUT, { recursive: true, force: true }) + await Bun.$`bunx --yes @tauri-apps/cli icon ${enlarged} --output ${OUTPUT}` + + // The desktop shell currently ships only macOS and Linux bundles. Keep the + // cross-platform desktop set while dropping mobile-only generated trees. + await rm(join(OUTPUT, 'android'), { recursive: true, force: true }) + await rm(join(OUTPUT, 'ios'), { recursive: true, force: true }) + console.log(`Generated desktop icons → ${OUTPUT}`) +} finally { + await rm(temporary, { recursive: true, force: true }) +} diff --git a/scripts/build-desktop.ts b/scripts/build-desktop.ts new file mode 100644 index 00000000..1cdd299c --- /dev/null +++ b/scripts/build-desktop.ts @@ -0,0 +1,77 @@ +// Build the moi desktop app (Tauri v2 shell in desktop/). +// +// Orchestrates: standalone runtime tarball (unless one is already staged or +// passed via --runtime) → stage it as the Tauri resource → `tauri build`. +// The shell provisions ~/.moi from that tarball on first launch — see +// desktop/src-tauri/src/main.rs. +// +// Usage: +// bun scripts/build-desktop.ts # full build, current platform +// bun scripts/build-desktop.ts --runtime # reuse an existing tarball +import { cp, mkdir, rm } from 'node:fs/promises' +import { join, resolve } from 'path' + +import tailwind from 'bun-plugin-tailwind' + +const ROOT = resolve(import.meta.dir, '..') +const DESKTOP = join(ROOT, 'desktop') +const RESOURCE = join(DESKTOP, 'src-tauri', 'resources', 'moi-runtime.tar.gz') +const BOOT_DIST = join(DESKTOP, 'dist') + +const pkg = (await Bun.file(join(ROOT, 'package.json')).json()) as { version: string } + +const runtimeFlag = process.argv.indexOf('--runtime') +let tarball: string +if (runtimeFlag !== -1) { + tarball = resolve(process.argv[runtimeFlag + 1]) +} else { + const platform = `${process.platform}-${process.arch}` + tarball = join(ROOT, 'dist-standalone', `moi-standalone-${pkg.version}-${platform}.tar.gz`) + if (!(await Bun.file(tarball).exists())) { + console.log('No runtime tarball found — building one first') + await Bun.$`bun ${join(ROOT, 'scripts', 'build-standalone.ts')}`.cwd(ROOT) + } +} + +if (!(await Bun.file(tarball).exists())) { + console.error(`Runtime tarball not found: ${tarball}`) + process.exit(1) +} + +// A manually supplied or stale runtime must not produce a desktop bundle whose +// advertised version differs from the payload it will provision on launch. +const payloadPackageText = await Bun.$`tar -xOzf ${tarball} moi-runtime/app/package.json`.text() +const payloadPackage = JSON.parse(payloadPackageText) as { version?: unknown } +if (payloadPackage.version !== pkg.version) { + console.error( + `Runtime payload version ${String(payloadPackage.version)} does not match package.json ${pkg.version}` + ) + process.exit(1) +} + +await mkdir(join(DESKTOP, 'src-tauri', 'resources'), { recursive: true }) +await cp(tarball, RESOURCE) +console.log(`Staged runtime resource from ${tarball}`) + +// Compile the pre-server boot page with the same Tailwind theme and favicon as +// the main client. Tauri receives plain static files and never maintains a +// separate desktop palette or type system. +await rm(BOOT_DIST, { recursive: true, force: true }) +const boot = await Bun.build({ + entrypoints: [join(DESKTOP, 'ui', 'index.html')], + outdir: BOOT_DIST, + minify: true, + sourcemap: 'none', + plugins: [tailwind] +}) +if (!boot.success) { + console.error('Desktop boot page build failed:') + for (const log of boot.logs) console.error(' ' + log.message) + process.exit(1) +} +await Bun.write(join(BOOT_DIST, '.gitkeep'), '') +console.log(`Built desktop boot page → ${BOOT_DIST}`) + +// tauri.conf.json reads its version directly from the root package.json, so +// main's existing package/tag version remains the only release version source. +await Bun.$`bunx --yes @tauri-apps/cli build`.cwd(DESKTOP) diff --git a/scripts/build-standalone.ts b/scripts/build-standalone.ts new file mode 100644 index 00000000..524f4f04 --- /dev/null +++ b/scripts/build-standalone.ts @@ -0,0 +1,69 @@ +// Build the standalone runtime tarball for the CURRENT platform: +// +// moi-standalone--.tar.gz (+ .sha256) +// └── moi-runtime/ +// ├── bun the exact bun running this script, pinned +// └── app/ packed package + production node_modules +// +// The tree mirrors what `bun i -g moi-computer` produces, so every runtime +// path (agent SDK spawn, applet bundling with tailwind oxide, function +// workers, sharp) behaves identically to a normal install — nothing in +// server/ needs to know it's standalone. node_modules is platform-specific +// (sharp, oxide, the SDK's native CLI), so CI runs this once per target on a +// matching runner. Consumed by packaging/install.sh and `moi update` +// (server/standalone.ts). +// +// Usage: bun scripts/build-standalone.ts [--outdir dist-standalone] +import { cp, mkdir, rm } from 'node:fs/promises' +import { join, resolve } from 'path' + +const ROOT = resolve(import.meta.dir, '..') +const outdirFlag = process.argv.indexOf('--outdir') +const OUT = resolve(ROOT, outdirFlag !== -1 ? process.argv[outdirFlag + 1] : 'dist-standalone') + +const pkg = (await Bun.file(join(ROOT, 'package.json')).json()) as { version: string } +const platform = `${process.platform}-${process.arch}` +const name = `moi-standalone-${pkg.version}-${platform}` + +const stage = join(OUT, '.stage') +const runtime = join(stage, 'moi-runtime') +const app = join(runtime, 'app') +await rm(stage, { recursive: true, force: true }) +await mkdir(app, { recursive: true }) + +// `bun pm pack` runs prepack, which builds the client into dist/. +console.log(`Packing moi-computer ${pkg.version}`) +await Bun.$`bun pm pack --destination ${stage}`.cwd(ROOT).quiet() +await Bun.$`tar -xzf ${join(stage, `moi-computer-${pkg.version}.tgz`)} -C ${app} --strip-components=1`.quiet() + +// Strip lifecycle scripts from the payload copy: `prepare` runs git config +// (fails outside a repo) and `prepack` would rebuild the client. The lockfile +// pins the exact dependency tree the repo tests against. +const appPkg = (await Bun.file(join(app, 'package.json')).json()) as Record +delete appPkg.scripts +await Bun.write(join(app, 'package.json'), JSON.stringify(appPkg, null, 2) + '\n') +await cp(join(ROOT, 'bun.lock'), join(app, 'bun.lock')) + +console.log('Installing production dependencies') +await Bun.$`bun install --production --frozen-lockfile`.cwd(app).quiet() + +// The bun executing this script becomes the payload's pinned runtime — build +// on the version you mean to ship (CI pins it via setup-bun). +await cp(process.execPath, join(runtime, 'bun')) +await Bun.$`chmod +x ${join(runtime, 'bun')}`.quiet() + +console.log('Compressing') +const tarball = join(OUT, `${name}.tar.gz`) +await rm(tarball, { force: true }) +await Bun.$`tar -czf ${tarball} -C ${stage} moi-runtime`.quiet() + +const hasher = new Bun.CryptoHasher('sha256') +hasher.update(await Bun.file(tarball).arrayBuffer()) +const sum = hasher.digest('hex') +await Bun.write(join(OUT, `${name}.tar.gz.sha256`), `${sum} ${name}.tar.gz\n`) + +await rm(stage, { recursive: true, force: true }) + +const size = (Bun.file(tarball).size / 1024 / 1024).toFixed(1) +console.log(`\n${name}.tar.gz (${size} MB, bun ${Bun.version})`) +console.log(`sha256: ${sum}`) diff --git a/server/cli.ts b/server/cli.ts index 7f208c61..1b747dcd 100755 --- a/server/cli.ts +++ b/server/cli.ts @@ -34,6 +34,7 @@ import { } from './cli-env' import { columns } from './cli-ui' import { CONTROL_PORT, PORT } from './constants' +import { isStandaloneInstall, uninstall, updateStandalone } from './standalone' import { type OpenClawAgent, discoverOpenClawAgents } from './harness/openclaw/discovery' import { liftToWorkspaceRoot, registerWorkspace } from './registry' import { serverCwd } from './server-cwd' @@ -2424,10 +2425,46 @@ const update = defineCommand({ type: 'boolean', default: false, description: - 'Only check the registry, change nothing. Exit 0: up to date (or nothing to update), 1: update available, 2: check failed.' + 'Only check for a release, change nothing. Exit 0: up to date (or nothing to update), 1: update available, 2: check failed.' } }, async run({ args }) { + if (isStandaloneInstall()) { + console.log('\n' + pc.bold('moi update') + pc.dim(` — standalone v${VERSION}`)) + try { + const result = await updateStandalone({ check: args.check }) + if (result.status === 'available') { + console.log( + ` update available: ${pc.bold('v' + result.latest)}` + + pc.dim(' — run `moi update` to install\n') + ) + process.exit(1) + } + if (result.status === 'up-to-date') { + const detail = + result.current === result.latest + ? `latest is v${result.latest}` + : `installed v${result.current}; latest published is v${result.latest}` + console.log(pc.green('✓') + ` Already up to date (${detail})`) + if (!args.check) await reportServerFreshness() + process.exit(0) + } + + console.log( + pc.green('✓') + + ` moi updated from ${pc.bold('v' + result.previous)} to ${pc.bold('v' + result.version)}` + ) + await reportServerFreshness(result.version) + console.log() + process.exit(0) + } catch (err) { + console.error( + '\n' + pc.red('✗') + ' ' + (err instanceof Error ? err.message : String(err)) + '\n' + ) + process.exit(args.check ? 2 : 1) + } + } + // A checkout has no owning package manager — updating means `git pull`. const analysis = analyzeInstall() if (analysis.kind === 'checkout') { @@ -2623,6 +2660,7 @@ const systemCommands = { status, service, update, + uninstall, openclaw, version } diff --git a/server/constants.ts b/server/constants.ts index 21fb0096..285cc817 100644 --- a/server/constants.ts +++ b/server/constants.ts @@ -1,3 +1,4 @@ export const PORT = Number(process.env.PORT) || 13337 -// M=13, E=5, I=9 -> 13059 -export const CONTROL_PORT = 13059 +// M=13, E=5, I=9 -> 13059. Env-overridable (like PORT) so a second instance — +// e.g. a standalone build under test — can run beside the dev server. +export const CONTROL_PORT = Number(process.env.MOI_CONTROL_PORT) || 13059 diff --git a/server/standalone.ts b/server/standalone.ts new file mode 100644 index 00000000..38d9ff76 --- /dev/null +++ b/server/standalone.ts @@ -0,0 +1,436 @@ +// Standalone install management behind `moi update` and `moi uninstall` for the +// self-contained distribution (see packaging/install.sh and +// scripts/build-standalone.ts). +// +// A standalone install lives under MOI_STANDALONE_HOME (default ~/.moi): +// +// $MOI_HOME/bin/moi exec shim on PATH (never changes) +// $MOI_HOME/runtime/current -> (symlink, flipped atomically) +// $MOI_HOME/runtime//{bun, app/...} +// +// The shim sets MOI_STANDALONE_HOME before exec'ing the pinned bun, which is +// how these commands know they're allowed to manage the tree. Under npm +// installs the env var is absent and both commands defer to the package +// manager instead. +import { existsSync, realpathSync } from 'node:fs' +import { mkdir, readFile, readdir, readlink, rename, rm, stat, symlink } from 'node:fs/promises' +import { homedir } from 'node:os' +import { isAbsolute, join, parse, resolve } from 'node:path' +import { defineCommand } from 'citty' + +import pc from './cli-pc' +import { VERSION } from './version' + +const REPO = 'molefrog/moi' +const DOWNLOAD_TIMEOUT_MS = 30 * 60_000 +const LOCK_WAIT_MS = 60_000 + +function existingRealpath(path: string): string { + try { + return realpathSync(path) + } catch { + return path + } +} + +export function resolveStandaloneHome(raw: string): string { + if (!isAbsolute(raw)) throw new Error('The standalone moi home must be an absolute path') + + const home = existingRealpath(resolve(raw)) + const root = parse(home).root + const userHome = existingRealpath(resolve(homedir())) + if (home === root || home === userHome) { + throw new Error(`Refusing to use unsafe standalone moi home: ${home}`) + } + return home +} + +function safeRemovalPath(raw: string, label: string): string { + if (!isAbsolute(raw)) throw new Error(`Refusing to remove relative ${label} path: ${raw}`) + + const path = existingRealpath(resolve(raw)) + const root = parse(path).root + const userHome = existingRealpath(resolve(homedir())) + if (path === root || path === userHome) { + throw new Error(`Refusing to remove unsafe ${label} path: ${path}`) + } + return path +} + +function standaloneHome(): string | null { + const raw = process.env.MOI_STANDALONE_HOME + return raw ? resolveStandaloneHome(raw) : null +} + +export function isStandaloneInstall(): boolean { + return Boolean(process.env.MOI_STANDALONE_HOME) +} + +// Release asset platform suffix, e.g. `darwin-arm64`. Matches the names +// scripts/build-standalone.ts produces. +function currentPlatform(): string { + return `${process.platform}-${process.arch}` +} + +async function activeInstalledVersion(runtimeDir: string): Promise { + try { + const pkg = JSON.parse( + await readFile(join(runtimeDir, 'current', 'app', 'package.json'), 'utf8') + ) as { version?: unknown } + return typeof pkg.version === 'string' ? pkg.version : null + } catch { + return null + } +} + +type ReleaseAsset = { name: string; browser_download_url: string } +type Release = { tag_name: string; assets: ReleaseAsset[] } + +function githubApiBase(): string { + return (process.env.MOI_GITHUB_API || 'https://api.github.com').replace(/\/$/, '') +} + +// Only exact stable semver tags are installable. This rejects prereleases, +// build metadata, shortened versions, and arbitrary release names before any +// value reaches a filesystem path. +export function stableVersionFromTag(tag: string): string | null { + const match = /^v?((?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*))$/.exec(tag) + return match?.[1] ?? null +} + +export function compareStableVersions(left: string, right: string): -1 | 0 | 1 { + const stableLeft = stableVersionFromTag(left) + const stableRight = stableVersionFromTag(right) + if (!stableLeft || !stableRight) + throw new Error(`Cannot compare invalid versions: ${left}, ${right}`) + + const leftParts = stableLeft.split('.').map(BigInt) + const rightParts = stableRight.split('.').map(BigInt) + for (let index = 0; index < leftParts.length; index++) { + if (leftParts[index] < rightParts[index]) return -1 + if (leftParts[index] > rightParts[index]) return 1 + } + return 0 +} + +// Release asset name for a platform, e.g. `moi-standalone-0.4.0-darwin-arm64.tar.gz`. +// Must match what scripts/build-standalone.ts produces. +export function standaloneAssetName(version: string, platform: string): string { + return `moi-standalone-${version}-${platform}.tar.gz` +} + +async function fetchLatestRelease(): Promise { + const res = await fetch(`${githubApiBase()}/repos/${REPO}/releases/latest`, { + headers: { accept: 'application/vnd.github+json', 'user-agent': 'moi-update' }, + signal: AbortSignal.timeout(30_000) + }) + if (!res.ok) throw new Error(`GitHub API responded with ${res.status}`) + const data = (await res.json().catch(() => null)) as unknown + if (!data || typeof data !== 'object' || !('tag_name' in data) || !('assets' in data)) { + throw new Error('GitHub API returned an invalid release payload') + } + const tagName = data.tag_name + const assets = data.assets + if ( + typeof tagName !== 'string' || + !Array.isArray(assets) || + !assets.every( + asset => + asset && + typeof asset === 'object' && + 'name' in asset && + typeof asset.name === 'string' && + 'browser_download_url' in asset && + typeof asset.browser_download_url === 'string' + ) + ) { + throw new Error('GitHub API returned an invalid release payload') + } + return { tag_name: tagName, assets: assets as ReleaseAsset[] } +} + +async function sha256(path: string): Promise { + const hasher = new Bun.CryptoHasher('sha256') + const reader = Bun.file(path).stream().getReader() + while (true) { + const chunk = await reader.read() + if (chunk.done) break + hasher.update(chunk.value) + } + return hasher.digest('hex') +} + +async function download(url: string, dest: string): Promise { + const res = await fetch(url, { + headers: { 'user-agent': 'moi-update' }, + signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) + }) + if (!res.ok || !res.body) throw new Error(`Download failed with ${res.status}: ${url}`) + await Bun.write(dest, res) +} + +function errorCode(error: unknown): string | null { + if (typeof error !== 'object' || error === null || !('code' in error)) return null + return typeof error.code === 'string' ? error.code : null +} + +function processIsAlive(pid: number): boolean { + try { + process.kill(pid, 0) + return true + } catch (error) { + return errorCode(error) !== 'ESRCH' + } +} + +async function lockIsStale(lockPath: string): Promise { + try { + const owner = await readlink(lockPath) + const pid = /^([1-9]\d*)-/.exec(owner)?.[1] + return !pid || !processIsAlive(Number(pid)) + } catch { + return true + } +} + +async function reapStaleLock(runtimeDir: string, lockPath: string): Promise { + if (!(await lockIsStale(lockPath))) return false + const stale = join(runtimeDir, `.stale-lock-${process.pid}-${crypto.randomUUID()}`) + try { + await rename(lockPath, stale) + await rm(stale, { recursive: true, force: true }) + return true + } catch (error) { + if (errorCode(error) === 'ENOENT') return true + return false + } +} + +type RuntimeLock = { release(): Promise } + +async function acquireRuntimeLock(runtimeDir: string): Promise { + await mkdir(runtimeDir, { recursive: true }) + const lockPath = join(runtimeDir, '.install-lock') + const owner = `${process.pid}-${crypto.randomUUID()}` + const deadline = Date.now() + LOCK_WAIT_MS + + while (true) { + try { + await symlink(owner, lockPath) + return { + async release() { + try { + if ((await readlink(lockPath)) === owner) await rm(lockPath, { force: true }) + } catch {} + } + } + } catch (error) { + if (errorCode(error) !== 'EEXIST') throw error + } + + if (await reapStaleLock(runtimeDir, lockPath)) continue + if (Date.now() >= deadline) { + throw new Error('Another moi install or update is still running') + } + await Bun.sleep(250) + } +} + +export async function withRuntimeLock( + runtimeDir: string, + operation: () => Promise +): Promise { + const lock = await acquireRuntimeLock(runtimeDir) + try { + return await operation() + } finally { + await lock.release() + } +} + +// Flip runtime/current to point at `version` atomically: build a process-unique +// link, then rename over the old one (rename replaces symlinks atomically on +// POSIX). Runtime-mutating callers still hold the cross-process install lock so +// the flip and subsequent prune are one transaction. +export async function flipCurrent(runtimeDir: string, version: string): Promise { + const tmp = join(runtimeDir, `.current-next-${process.pid}-${crypto.randomUUID()}`) + try { + await symlink(version, tmp) + await rename(tmp, join(runtimeDir, 'current')) + } finally { + await rm(tmp, { force: true }) + } +} + +// Keep the freshly installed version plus the one it replaced (instant +// rollback by re-pointing `current`); drop anything older. +export async function pruneVersions(runtimeDir: string, keep: string[]): Promise { + const entries = await readdir(runtimeDir) + for (const entry of entries) { + if (entry === 'current' || entry.startsWith('.') || keep.includes(entry)) continue + await rm(join(runtimeDir, entry), { recursive: true, force: true }) + } +} + +async function validateStagedRuntime(extracted: string, expectedVersion: string): Promise { + const pkg = JSON.parse(await readFile(join(extracted, 'app', 'package.json'), 'utf8')) as { + version?: unknown + } + if (pkg.version !== expectedVersion) { + throw new Error( + `Release payload version ${String(pkg.version)} does not match ${expectedVersion}` + ) + } + + const bun = await stat(join(extracted, 'bun')) + if (!bun.isFile() || (bun.mode & 0o111) === 0) { + throw new Error('Release payload has no executable Bun runtime') + } + if (!existsSync(join(extracted, 'app', 'server', 'cli.ts'))) { + throw new Error('Release payload has no moi CLI') + } +} + +export type StandaloneUpdateResult = + | { status: 'up-to-date'; current: string; latest: string } + | { status: 'available'; current: string; latest: string } + | { status: 'updated'; previous: string; version: string } + +export async function updateStandalone(options: { + check: boolean +}): Promise { + const home = standaloneHome() + if (!home) throw new Error('This install is not standalone') + + const platform = currentPlatform() + const current = VERSION + if (!stableVersionFromTag(current)) { + throw new Error(`Installed moi version is not a stable release: ${current}`) + } + console.log(pc.dim(` checking GitHub releases (${platform})`)) + + const release = await fetchLatestRelease() + const version = stableVersionFromTag(release.tag_name) + if (!version) { + throw new Error(`Latest release ${release.tag_name} is not an installable stable semver tag`) + } + if (compareStableVersions(current, version) >= 0) { + return { status: 'up-to-date', current, latest: version } + } + + const assetName = standaloneAssetName(version, platform) + const asset = release.assets.find(a => a.name === assetName) + const checksumAsset = release.assets.find(a => a.name === `${assetName}.sha256`) + if (!asset) throw new Error(`Release ${release.tag_name} has no standalone build for ${platform}`) + if (!checksumAsset) { + throw new Error(`Release ${release.tag_name} has no checksum for ${assetName}`) + } + if (options.check) return { status: 'available', current, latest: version } + + const runtimeDir = join(home, 'runtime') + const operationId = `${process.pid}-${crypto.randomUUID()}` + const stageDir = join(runtimeDir, `.stage-${version}-${operationId}`) + const tarball = join(runtimeDir, `.${assetName}.${operationId}`) + let replaced = current + + const changed = await withRuntimeLock(runtimeDir, async () => { + const active = await activeInstalledVersion(runtimeDir) + if (active) { + if (!stableVersionFromTag(active)) { + throw new Error(`Active moi runtime has an invalid version: ${active}`) + } + replaced = active + if (compareStableVersions(active, version) >= 0) return false + } + + try { + console.log(pc.dim(` downloading ${assetName}`)) + await download(asset.browser_download_url, tarball) + + const checksumRes = await fetch(checksumAsset.browser_download_url, { + headers: { 'user-agent': 'moi-update' }, + signal: AbortSignal.timeout(30_000) + }) + if (!checksumRes.ok) { + throw new Error(`Could not fetch the checksum (${checksumRes.status}) — install unchanged`) + } + const expected = (await checksumRes.text()).trim().split(/\s+/)[0]?.toLowerCase() + if (!expected || !/^[a-f0-9]{64}$/.test(expected)) { + throw new Error('Release checksum is malformed — install unchanged') + } + const actual = await sha256(tarball) + if (actual !== expected) { + throw new Error('Checksum mismatch — download discarded, install unchanged') + } + + // Tarball root is `moi-runtime/`; validate the extracted payload before + // it can replace a version directory or the current symlink. + await mkdir(stageDir, { recursive: true }) + await Bun.$`tar -xzf ${tarball} -C ${stageDir}`.quiet() + const extracted = join(stageDir, 'moi-runtime') + await validateStagedRuntime(extracted, version) + + const versionDir = join(runtimeDir, version) + await rm(versionDir, { recursive: true, force: true }) + await rename(extracted, versionDir) + await flipCurrent(runtimeDir, version) + await pruneVersions(runtimeDir, [version, replaced]) + return true + } finally { + await rm(tarball, { force: true }) + await rm(stageDir, { recursive: true, force: true }) + } + }) + + return changed + ? { status: 'updated', previous: replaced, version } + : { status: 'up-to-date', current: replaced, latest: version } +} + +export const uninstall = defineCommand({ + meta: { name: 'uninstall', description: 'Remove a standalone moi install' }, + args: { + data: { + type: 'boolean', + description: 'Also remove moi data (workspace registry, settings)', + default: false + } + }, + async run({ args }) { + const home = standaloneHome() + if (!home) { + console.error('This install is not standalone — remove the npm package instead:') + console.error(' bun rm -g moi-computer') + process.exit(1) + } + + if (!existsSync(home)) { + console.error(`Nothing to remove at ${home}`) + process.exit(1) + } + + console.log(`This removes ${home} (the moi runtime and command).`) + if (args.data) console.log('Workspace registry and settings will also be removed.') + const answer = prompt('Continue? [y/N]') + if (answer?.toLowerCase() !== 'y') { + console.log('Cancelled — nothing removed') + return + } + + // Resolve the lazily imported data path before deleting the runtime that + // contains this module, then serialize uninstall against any update or + // desktop provisioning process. + const dataDir = args.data + ? safeRemovalPath((await import('./data-dir')).DATA_DIR, 'moi data') + : null + await withRuntimeLock(join(home, 'runtime'), async () => { + await rm(home, { recursive: true, force: true }) + if (dataDir) await rm(dataDir, { recursive: true, force: true }) + }) + + console.log('moi removed. Workspaces and their files are untouched.') + console.log( + pc.dim(`Remove the PATH entry for ${join(home, 'bin')} from your shell profile if needed.`) + ) + } +}) diff --git a/server/test/standalone-installer.test.ts b/server/test/standalone-installer.test.ts new file mode 100644 index 00000000..43a2985f --- /dev/null +++ b/server/test/standalone-installer.test.ts @@ -0,0 +1,194 @@ +import { afterEach, describe, expect, test } from 'bun:test' +import { + chmodSync, + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + readlinkSync, + rmSync, + symlinkSync, + writeFileSync +} from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +const INSTALLER = join(import.meta.dir, '..', '..', 'packaging', 'install.sh') +const VERSION = '0.4.0' + +const tempRoots: string[] = [] + +afterEach(() => { + for (const root of tempRoots.splice(0)) rmSync(root, { recursive: true, force: true }) +}) + +function tempRoot(): string { + const root = mkdtempSync(join(tmpdir(), 'moi-installer-test-')) + tempRoots.push(root) + return root +} + +function writeExecutable(path: string, text: string): void { + writeFileSync(path, text) + chmodSync(path, 0o755) +} + +async function fixture(root: string): Promise<{ tarball: string; checksum: string }> { + const stage = join(root, 'payload') + const runtime = join(stage, 'moi-runtime') + mkdirSync(join(runtime, 'app', 'server'), { recursive: true }) + writeExecutable( + join(runtime, 'bun'), + [ + '#!/bin/sh', + 'if [ "$1" = "-e" ]; then exec "$MOCK_REAL_BUN" "$@"; fi', + 'printf \'%s\\n\' "$MOI_HOME" >"$MOCK_EXEC_HOME"', + 'printf \'%s\\n\' "$@" >"$MOCK_EXEC_ARGS"', + '' + ].join('\n') + ) + writeFileSync(join(runtime, 'app', 'server', 'cli.ts'), '') + writeFileSync(join(runtime, 'app', 'package.json'), JSON.stringify({ version: VERSION }) + '\n') + + const tarball = join(root, 'runtime.tar.gz') + const tar = Bun.spawn(['tar', '-czf', tarball, '-C', stage, 'moi-runtime'], { + env: { ...process.env, LC_ALL: 'C' }, + stderr: 'pipe' + }) + expect(await tar.exited).toBe(0) + + const hasher = new Bun.CryptoHasher('sha256') + hasher.update(await Bun.file(tarball).arrayBuffer()) + const checksum = join(root, 'runtime.tar.gz.sha256') + writeFileSync(checksum, `${hasher.digest('hex')} runtime.tar.gz\n`) + return { tarball, checksum } +} + +function mockCurl(bin: string): void { + mkdirSync(bin, { recursive: true }) + writeExecutable( + join(bin, 'curl'), + [ + '#!/bin/sh', + 'out=""', + 'format=""', + 'url=""', + 'while [ "$#" -gt 0 ]; do', + ' case "$1" in', + ' -o) out="$2"; shift 2 ;;', + ' -w) format="$2"; shift 2 ;;', + ' *) url="$1"; shift ;;', + ' esac', + 'done', + 'if [ -n "$format" ]; then', + ` printf 'https://github.com/molefrog/moi/releases/tag/v${VERSION}'`, + 'elif [ "${url##*.}" = "sha256" ]; then', + ' [ "${MOCK_NO_CHECKSUM:-}" = "1" ] && exit 22', + ' cp "$MOCK_CHECKSUM" "$out"', + 'else', + ' cp "$MOCK_TARBALL" "$out"', + 'fi', + '' + ].join('\n') + ) +} + +async function runInstaller( + root: string, + home: string, + options: { noChecksum?: boolean } = {} +): Promise<{ exitCode: number; stdout: string; stderr: string }> { + const files = await fixture(root) + const bin = join(root, 'bin') + mockCurl(bin) + const proc = Bun.spawn(['sh', INSTALLER], { + env: { + ...process.env, + HOME: join(root, 'user-home'), + MOI_HOME: home, + MOI_NO_MODIFY_PATH: '1', + MOCK_TARBALL: files.tarball, + MOCK_CHECKSUM: files.checksum, + MOCK_REAL_BUN: process.execPath, + MOCK_NO_CHECKSUM: options.noChecksum ? '1' : '', + LC_ALL: 'C', + PATH: `${bin}:${process.env.PATH ?? ''}` + }, + stdout: 'pipe', + stderr: 'pipe' + }) + const [exitCode, stdout, stderr] = await Promise.all([ + proc.exited, + new Response(proc.stdout).text(), + new Response(proc.stderr).text() + ]) + return { exitCode, stdout, stderr } +} + +describe('standalone shell installer', () => { + test('installs atomically, prunes old versions, and preserves an exotic MOI_HOME', async () => { + const root = tempRoot() + const home = join(root, "custom home a'b$HOME") + const runtime = join(home, 'runtime') + mkdirSync(join(runtime, '0.1.0'), { recursive: true }) + mkdirSync(join(runtime, '0.2.0')) + symlinkSync('0.2.0', join(runtime, 'current')) + + const result = await runInstaller(root, home) + expect(result.exitCode, result.stderr).toBe(0) + expect(readlinkSync(join(runtime, 'current'))).toBe(VERSION) + expect(existsSync(join(runtime, VERSION))).toBe(true) + expect(existsSync(join(runtime, '0.2.0'))).toBe(true) + expect(existsSync(join(runtime, '0.1.0'))).toBe(false) + expect(existsSync(join(runtime, '.install-lock'))).toBe(false) + + const execHome = join(root, 'exec-home.txt') + const execArgs = join(root, 'exec-args.txt') + const shimEnv: Record = { + ...process.env, + MOCK_EXEC_HOME: execHome, + MOCK_EXEC_ARGS: execArgs + } + delete shimEnv.MOI_HOME + const shim = Bun.spawn([join(home, 'bin', 'moi'), 'version'], { env: shimEnv }) + expect(await shim.exited).toBe(0) + expect(readFileSync(execHome, 'utf8').trim()).toBe(home) + expect(readFileSync(execArgs, 'utf8').trim().split('\n')).toEqual([ + join(home, 'runtime', 'current', 'app', 'server', 'cli.ts'), + 'version' + ]) + }) + + test('fails closed before mutating the install when the checksum is absent', async () => { + const root = tempRoot() + const home = join(root, 'standalone-home') + + const result = await runInstaller(root, home, { noChecksum: true }) + expect(result.exitCode).not.toBe(0) + expect(result.stderr).toContain('has no checksum') + expect(existsSync(home)).toBe(false) + }) + + test('does not replace a newer installed runtime', async () => { + const root = tempRoot() + const home = join(root, 'standalone-home') + const runtime = join(home, 'runtime') + mkdirSync(join(runtime, '0.5.0'), { recursive: true }) + symlinkSync('0.5.0', join(runtime, 'current')) + + const result = await runInstaller(root, home) + expect(result.exitCode).toBe(0) + expect(readlinkSync(join(runtime, 'current'))).toBe('0.5.0') + expect(result.stdout).toContain('Keeping installed moi 0.5.0') + }) + + test('refuses to install into the user home directory', async () => { + const root = tempRoot() + const home = join(root, 'user-home') + + const result = await runInstaller(root, home) + expect(result.exitCode).not.toBe(0) + expect(result.stderr).toContain('must not be your home directory') + expect(existsSync(home)).toBe(false) + }) +}) diff --git a/server/test/standalone.test.ts b/server/test/standalone.test.ts new file mode 100644 index 00000000..bc4ab949 --- /dev/null +++ b/server/test/standalone.test.ts @@ -0,0 +1,309 @@ +import { describe, expect, test } from 'bun:test' +import { + mkdirSync, + mkdtempSync, + readdirSync, + realpathSync, + readlinkSync, + rmSync, + symlinkSync, + writeFileSync +} from 'node:fs' +import { homedir, tmpdir } from 'node:os' +import { join } from 'node:path' + +import { + compareStableVersions, + flipCurrent, + pruneVersions, + resolveStandaloneHome, + stableVersionFromTag, + standaloneAssetName, + updateStandalone, + withRuntimeLock +} from '../standalone' +import { VERSION } from '../version' + +// Tag → version rules shared by `moi update` and the release workflow: only +// stable `vX.Y.Z` tags produce installable standalone releases; `-next.N` +// preview tags (see .agents/skills/publish-next) must never reach users. +describe('stableVersionFromTag', () => { + test('accepts stable tags, with or without the v prefix', () => { + expect(stableVersionFromTag('v0.4.0')).toBe('0.4.0') + expect(stableVersionFromTag('0.4.0')).toBe('0.4.0') + expect(stableVersionFromTag('v10.20.30')).toBe('10.20.30') + }) + + test('rejects prerelease tags', () => { + expect(stableVersionFromTag('v0.3.0-next.0')).toBeNull() + expect(stableVersionFromTag('v0.3.0-next.12')).toBeNull() + expect(stableVersionFromTag('v1.0.0-rc.1')).toBeNull() + }) + + test('rejects malformed and non-canonical stable tags', () => { + for (const tag of ['banana', 'v1', 'v1.2', 'v1.2.3.4', 'v01.2.3', 'v1.2.3+meta']) { + expect(stableVersionFromTag(tag)).toBeNull() + } + }) +}) + +describe('compareStableVersions', () => { + test('orders stable releases without number precision loss', () => { + expect(compareStableVersions('0.3.0', '0.4.0')).toBe(-1) + expect(compareStableVersions('v1.2.3', '1.2.3')).toBe(0) + expect(compareStableVersions('10.0.0', '2.999.999')).toBe(1) + expect(compareStableVersions('90071992547409930.0.0', '90071992547409929.999.999')).toBe(1) + }) + + test('refuses invalid inputs', () => { + expect(() => compareStableVersions('next', '1.0.0')).toThrow('invalid versions') + }) +}) + +describe('standaloneAssetName', () => { + test('matches the naming scripts/build-standalone.ts produces', () => { + expect(standaloneAssetName('0.4.0', 'darwin-arm64')).toBe( + 'moi-standalone-0.4.0-darwin-arm64.tar.gz' + ) + }) +}) + +describe('resolveStandaloneHome', () => { + test('accepts a dedicated absolute directory', () => { + const home = mkdtempSync(join(tmpdir(), 'moi-standalone-home-')) + try { + expect(resolveStandaloneHome(home)).toBe(realpathSync(home)) + } finally { + rmSync(home, { recursive: true, force: true }) + } + }) + + test('rejects relative, filesystem-root, and user-home paths', () => { + expect(() => resolveStandaloneHome('relative')).toThrow('absolute path') + expect(() => resolveStandaloneHome('/')).toThrow('unsafe') + expect(() => resolveStandaloneHome(homedir())).toThrow('unsafe') + }) +}) + +describe('updateStandalone', () => { + test('--check reports a platform release without mutating the runtime', async () => { + const home = mkdtempSync(join(tmpdir(), 'moi-standalone-check-')) + const version = '9.0.0' + const asset = standaloneAssetName(version, `${process.platform}-${process.arch}`) + const api = Bun.serve({ + port: 0, + hostname: '127.0.0.1', + fetch() { + return Response.json({ + tag_name: `v${version}`, + assets: [ + { name: asset, browser_download_url: `https://example.test/${asset}` }, + { + name: `${asset}.sha256`, + browser_download_url: `https://example.test/${asset}.sha256` + } + ] + }) + } + }) + const oldHome = process.env.MOI_STANDALONE_HOME + const oldApi = process.env.MOI_GITHUB_API + process.env.MOI_STANDALONE_HOME = home + process.env.MOI_GITHUB_API = `http://127.0.0.1:${api.port}` + + try { + await expect(updateStandalone({ check: true })).resolves.toEqual({ + status: 'available', + current: VERSION, + latest: version + }) + expect(readdirSync(home)).toEqual([]) + } finally { + api.stop(true) + if (oldHome === undefined) delete process.env.MOI_STANDALONE_HOME + else process.env.MOI_STANDALONE_HOME = oldHome + if (oldApi === undefined) delete process.env.MOI_GITHUB_API + else process.env.MOI_GITHUB_API = oldApi + rmSync(home, { recursive: true, force: true }) + } + }) +}) + +describe('flipCurrent', () => { + test('retargets the current symlink across repeated flips', async () => { + const runtime = mkdtempSync(join(tmpdir(), 'moi-standalone-')) + mkdirSync(join(runtime, '0.1.0')) + mkdirSync(join(runtime, '0.2.0')) + + await flipCurrent(runtime, '0.1.0') + expect(readlinkSync(join(runtime, 'current'))).toBe('0.1.0') + + await flipCurrent(runtime, '0.2.0') + expect(readlinkSync(join(runtime, 'current'))).toBe('0.2.0') + }) + + test('uses independent temporary links for concurrent flips', async () => { + const runtime = mkdtempSync(join(tmpdir(), 'moi-standalone-')) + mkdirSync(join(runtime, '0.1.0')) + mkdirSync(join(runtime, '0.2.0')) + + const flips = await Promise.allSettled([ + flipCurrent(runtime, '0.1.0'), + flipCurrent(runtime, '0.2.0') + ]) + expect(flips.every(result => result.status === 'fulfilled')).toBe(true) + expect(['0.1.0', '0.2.0']).toContain(readlinkSync(join(runtime, 'current'))) + }) +}) + +describe('withRuntimeLock', () => { + test('serializes concurrent runtime mutations', async () => { + const runtime = mkdtempSync(join(tmpdir(), 'moi-standalone-')) + let active = 0 + let maxActive = 0 + const operation = () => + withRuntimeLock(runtime, async () => { + active++ + maxActive = Math.max(maxActive, active) + await Bun.sleep(20) + active-- + }) + + await Promise.all([operation(), operation()]) + expect(maxActive).toBe(1) + }) + + test('reaps a lock whose owner process no longer exists', async () => { + const runtime = mkdtempSync(join(tmpdir(), 'moi-standalone-')) + symlinkSync('999999999-dead', join(runtime, '.install-lock')) + let ran = false + + await withRuntimeLock(runtime, async () => { + ran = true + }) + + expect(ran).toBe(true) + }) +}) + +describe('pruneVersions', () => { + test('keeps the kept versions, current, and dotted work files; drops the rest', async () => { + const runtime = mkdtempSync(join(tmpdir(), 'moi-standalone-')) + for (const version of ['0.1.0', '0.2.0', '0.3.0']) mkdirSync(join(runtime, version)) + symlinkSync('0.3.0', join(runtime, 'current')) + mkdirSync(join(runtime, '.stage-123')) + writeFileSync(join(runtime, '.download.tar.gz'), '') + + await pruneVersions(runtime, ['0.3.0', '0.2.0']) + + const left = new Set(readdirSync(runtime)) + expect(left.has('0.1.0')).toBe(false) + expect(left.has('0.2.0')).toBe(true) + expect(left.has('0.3.0')).toBe(true) + expect(left.has('current')).toBe(true) + expect(left.has('.stage-123')).toBe(true) + expect(left.has('.download.tar.gz')).toBe(true) + }) +}) + +// Pin the release workflow's load-bearing invariants: previews stay private, +// stable tags match package.json, build jobs cannot write repository contents, +// and only one final job publishes a complete draft. +describe('standalone artifacts in the release workflow', () => { + const workflow = join(import.meta.dir, '..', '..', '.github', 'workflows', 'release.yml') + + test('standalone jobs and the GitHub release skip prerelease tags', async () => { + const text = await Bun.file(workflow).text() + const guards = text + .split('\n') + .filter(line => line.includes("if: ${{ !contains(inputs.tag || github.ref_name, '-') }}")) + expect(guards.length).toBe(3) + expect(guards.every(line => line.trim().startsWith('if:'))).toBe(true) + }) + + test('publishes once, only after npm and both build matrices finish', async () => { + const text = await Bun.file(workflow).text() + expect(text).toContain("tags: ['v*']") + expect(text.split('uses: softprops/action-gh-release@v2').length - 1).toBe(1) + expect(text).toContain('needs: [publish, standalone, desktop]') + expect(text).toContain('draft: true') + expect(text).toContain('gh release edit "$RELEASE_TAG" --draft=false --latest') + }) + + test('validates the tag and narrows write permission to the GitHub publisher', async () => { + const text = await Bun.file(workflow).text() + expect(text).toContain('[ "$VERSION" = "$PKG" ]') + expect(text).toContain('permissions:\n contents: read') + expect(text.split('contents: write').length - 1).toBe(1) + }) + + test('requires every checksum before publishing', async () => { + const text = await Bun.file(workflow).text() + expect(text).toContain('test -f "release-assets/$asset.sha256"') + expect(text).toContain('sha256sum -c "$asset.sha256"') + }) + + test('builds licensed clients on current hosted runner labels', async () => { + const text = await Bun.file(workflow).text() + expect(text).toContain('PUBLIC_TLDRAW_LICENSE_KEY: ${{ vars.PUBLIC_TLDRAW_LICENSE_KEY }}') + expect(text).toContain('runner: macos-15-intel') + expect(text).not.toContain('runner: macos-13') + }) +}) + +describe('installer and desktop invariants', () => { + const installer = join(import.meta.dir, '..', '..', 'packaging', 'install.sh') + const desktop = join(import.meta.dir, '..', '..', 'desktop', 'src-tauri', 'src', 'main.rs') + + test('the shell installer fails closed without a checksum', async () => { + const text = await Bun.file(installer).text() + expect(text).toContain('has no checksum for $asset — install unchanged') + expect(text).not.toContain('skipping verification') + }) + + test('all standalone writers use the shared install lock name', async () => { + const sources = [ + await Bun.file(join(import.meta.dir, '..', 'standalone.ts')).text(), + await Bun.file(installer).text(), + await Bun.file(desktop).text() + ] + expect(sources.every(text => text.includes('.install-lock'))).toBe(true) + }) + + test('desktop identifies moi and prunes superseded runtimes', async () => { + const text = await Bun.file(desktop).text() + expect(text).toContain('GET /status HTTP/1.1') + expect(text).toContain('moi server status\\n') + expect(text).toContain('prune_versions(&runtime, &keep)?') + }) + + test('both shim writers shell-quote a custom home', async () => { + const installerText = await Bun.file(installer).text() + const desktopText = await Bun.file(desktop).text() + expect(installerText).toContain('shell_quote()') + expect(desktopText).toContain('fn shell_quote(value: &str)') + }) + + test('desktop boot branding and native icons use the canonical client asset', async () => { + const build = await Bun.file( + join(import.meta.dir, '..', '..', 'scripts', 'build-desktop.ts') + ).text() + const iconBuild = await Bun.file( + join(import.meta.dir, '..', '..', 'scripts', 'build-desktop-icons.ts') + ).text() + const bootPage = await Bun.file( + join(import.meta.dir, '..', '..', 'desktop', 'ui', 'index.html') + ).text() + expect(build).toContain('plugins: [tailwind]') + expect(iconBuild).toContain("join(ROOT, 'client', 'assets', 'favicon.png')") + expect(bootPage).toContain('src="../../client/assets/favicon.png"') + expect(bootPage).toContain('Copy diagnostics') + }) + + test('desktop version comes from the root package metadata', async () => { + const config = await Bun.file( + join(import.meta.dir, '..', '..', 'desktop', 'src-tauri', 'tauri.conf.json') + ).text() + expect(config).toContain('"version": "../../package.json"') + }) +})