From a7c48275f9c4eec817dcfdf672a06760a73f592b Mon Sep 17 00:00:00 2001 From: BB-fat <1056871944@qq.com> Date: Wed, 8 Jul 2026 14:14:00 +0800 Subject: [PATCH] feat: add bsk CLI auto update Co-authored-by: Cursor --- .github/workflows/release-cli.yml | 1 + Cargo.lock | 1226 ++++++++++++++++++++- Cargo.toml | 5 + crates/bsk-cli/Cargo.toml | 5 + crates/bsk-cli/src/cli/daemon.rs | 2 +- crates/bsk-cli/src/cli/error.rs | 5 +- crates/bsk-cli/src/cli/mod.rs | 5 + crates/bsk-cli/src/cli/update.rs | 883 +++++++++++++++ crates/bsk-cli/src/daemon/paths.rs | 5 + crates/bsk-cli/src/daemon/queue.rs | 17 +- crates/bsk-cli/src/main.rs | 2 + crates/bsk-cli/tests/cancel_forwarding.rs | 8 +- crates/bsk-cli/tests/cli_parse.rs | 11 + crates/bsk-cli/tests/per_session_queue.rs | 19 +- crates/bsk-cli/tests/support/mod.rs | 10 +- scripts/render-version-json.mjs | 34 +- scripts/render-version-json.test.mjs | 52 + 17 files changed, 2255 insertions(+), 35 deletions(-) create mode 100644 crates/bsk-cli/src/cli/update.rs create mode 100644 scripts/render-version-json.test.mjs diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 0d161d9..566e370 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -139,6 +139,7 @@ jobs: --repo "${{ github.repository }}" \ --server-url "${{ github.server_url }}" \ --branch "${{ github.event.repository.default_branch }}" \ + --dist dist \ --out dist/version.json - name: Publish GitHub Release diff --git a/Cargo.lock b/Cargo.lock index 16afdbc..dc8b442 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # 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" @@ -67,6 +73,41 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "aws-lc-rs" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "2.11.1" @@ -82,6 +123,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "bsk" version = "0.1.7" @@ -92,14 +142,18 @@ dependencies = [ "console", "dialoguer", "dirs", + "flate2", "fs2", "futures-util", "libc", "nix", - "rand", + "rand 0.8.6", + "reqwest", "semver", "serde", "serde_json", + "sha2", + "tar", "tempfile", "thiserror 2.0.18", "tokio", @@ -109,6 +163,7 @@ dependencies = [ "tracing-subscriber", "uuid", "windows-sys 0.59.0", + "zip", ] [[package]] @@ -140,6 +195,18 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "cc" +version = "1.2.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -152,6 +219,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + [[package]] name = "clap" version = "4.6.1" @@ -192,12 +270,31 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + [[package]] name = "colorchoice" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "console" version = "0.15.11" @@ -211,6 +308,28 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[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 = "cpufeatures" version = "0.2.17" @@ -220,6 +339,24 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -245,6 +382,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "data-encoding" version = "2.11.0" @@ -279,8 +425,19 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", ] [[package]] @@ -304,6 +461,23 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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" @@ -338,12 +512,48 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[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", + "zlib-rs", +] + [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[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 = "fs2" version = "0.4.3" @@ -354,12 +564,34 @@ dependencies = [ "winapi", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + [[package]] name = "futures-core" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[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" @@ -390,9 +622,11 @@ 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", ] @@ -414,8 +648,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -425,10 +661,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", + "rand_core 0.10.1", "wasip2", "wasip3", + "wasm-bindgen", ] [[package]] @@ -462,18 +701,211 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + [[package]] name = "httparse" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "typenum", +] + +[[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-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[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 = "id-arena" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +[[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 = "2.14.0" @@ -486,6 +918,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -498,6 +936,65 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", +] + +[[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", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.2", + "libc", +] + [[package]] name = "js-sys" version = "0.3.98" @@ -543,6 +1040,12 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +[[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" @@ -558,6 +1061,12 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "matchers" version = "0.2.0" @@ -573,6 +1082,16 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[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.0" @@ -623,6 +1142,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "option-ext" version = "0.2.0" @@ -652,12 +1177,33 @@ dependencies = [ "windows-link", ] +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[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 = "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" @@ -692,6 +1238,63 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.4.2", + "lru-slab", + "rand 0.10.2", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + [[package]] name = "quote" version = "1.0.45" @@ -715,7 +1318,18 @@ checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", ] [[package]] @@ -725,7 +1339,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -737,6 +1351,21 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -774,6 +1403,72 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.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 = "rustix" version = "1.1.4" @@ -787,12 +1482,105 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "schemars" version = "0.8.22" @@ -823,6 +1611,29 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.28" @@ -894,8 +1705,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -913,6 +1735,12 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -923,6 +1751,28 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "slab" version = "0.4.12" @@ -945,12 +1795,24 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "symlink" version = "0.1.0" @@ -968,6 +1830,37 @@ dependencies = [ "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", +] + +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "tempfile" version = "3.27.0" @@ -1061,6 +1954,31 @@ dependencies = [ "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.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +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.52.3" @@ -1089,6 +2007,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-tungstenite" version = "0.24.0" @@ -1101,6 +2029,51 @@ dependencies = [ "tungstenite", ] +[[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", + "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" @@ -1188,6 +2161,12 @@ dependencies = [ "tracing-serde", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "tungstenite" version = "0.24.0" @@ -1200,12 +2179,18 @@ dependencies = [ "http", "httparse", "log", - "rand", + "rand 0.8.6", "sha1", "thiserror 1.0.69", "utf-8", ] +[[package]] +name = "typed-path" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" + [[package]] name = "typenum" version = "1.20.0" @@ -1230,12 +2215,36 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + [[package]] name = "utf-8" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -1266,6 +2275,25 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[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" @@ -1303,6 +2331,16 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.121" @@ -1369,6 +2407,35 @@ dependencies = [ "semver", ] +[[package]] +name = "web-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1385,6 +2452,15 @@ 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" @@ -1406,6 +2482,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -1639,6 +2724,45 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "yoke" +version = "0.8.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", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.48" @@ -1659,14 +2783,100 @@ dependencies = [ "syn", ] +[[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", + "synstructure", +] + [[package]] name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +[[package]] +name = "zerotrie" +version = "0.2.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", +] + +[[package]] +name = "zip" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c42e33efc22a0650c311c2ef19115ce232583abbe80850bc8b66509ebef02de0" +dependencies = [ + "crc32fast", + "flate2", + "indexmap", + "memchr", + "typed-path", + "zopfli", +] + +[[package]] +name = "zlib-rs" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5431d5661c32445236631278f27946e444ddafe4684cac70b185272d4f9c52d5" + [[package]] name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] diff --git a/Cargo.toml b/Cargo.toml index fe18925..6e41be9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,11 @@ console = "0.15" dialoguer = "0.11" dirs = "5" fs2 = "0.4" +reqwest = { version = "0.13.4", default-features = false, features = ["blocking", "rustls"] } +flate2 = "1.1.9" +tar = "0.4.46" +zip = { version = "7.2.0", default-features = false, features = ["deflate"] } +sha2 = "0.11.0" tempfile = "3" nix = { version = "0.29", features = ["signal", "process"] } rand = "0.8" diff --git a/crates/bsk-cli/Cargo.toml b/crates/bsk-cli/Cargo.toml index 5f30e5f..afc980f 100644 --- a/crates/bsk-cli/Cargo.toml +++ b/crates/bsk-cli/Cargo.toml @@ -48,6 +48,11 @@ fs2 = { workspace = true } rand = { workspace = true } uuid = { workspace = true } bsk-protocol = { workspace = true } +reqwest = { workspace = true } +flate2 = { workspace = true } +tar = { workspace = true } +zip = { workspace = true } +sha2 = { workspace = true } [target.'cfg(unix)'.dependencies] nix = { workspace = true } diff --git a/crates/bsk-cli/src/cli/daemon.rs b/crates/bsk-cli/src/cli/daemon.rs index bac52c9..0651873 100644 --- a/crates/bsk-cli/src/cli/daemon.rs +++ b/crates/bsk-cli/src/cli/daemon.rs @@ -25,7 +25,7 @@ pub enum DaemonCmd { Restart(StartArgs), } -#[derive(Debug, Clone, clap::Args)] +#[derive(Debug, Clone, Default, clap::Args)] pub struct StartArgs { /// Override the WebSocket port (default 52800). #[arg(long, value_name = "PORT")] diff --git a/crates/bsk-cli/src/cli/error.rs b/crates/bsk-cli/src/cli/error.rs index 27137d2..4490a81 100644 --- a/crates/bsk-cli/src/cli/error.rs +++ b/crates/bsk-cli/src/cli/error.rs @@ -162,7 +162,10 @@ fn render_info_for(err: &CliError) -> Option { /// Hint line for human / JSON rendering, including the local-failure /// fallback when there is no daemon [`ErrorCode`]. -fn hint_for(err: &CliError, render_info: Option<&render_error::RenderInfo>) -> Option<&'static str> { +fn hint_for( + err: &CliError, + render_info: Option<&render_error::RenderInfo>, +) -> Option<&'static str> { render_info .and_then(|info| info.hint) .or(if matches!(err, CliError::Local(_)) { diff --git a/crates/bsk-cli/src/cli/mod.rs b/crates/bsk-cli/src/cli/mod.rs index 85b1ffb..6d10446 100644 --- a/crates/bsk-cli/src/cli/mod.rs +++ b/crates/bsk-cli/src/cli/mod.rs @@ -24,6 +24,7 @@ pub mod session; pub mod snapshot; pub mod status; pub mod tab; +pub mod update; pub mod waits; use clap::{Args, Parser, Subcommand}; @@ -40,6 +41,7 @@ use crate::cli::screenshot::ScreenshotArgs; use crate::cli::session::SessionCmd; use crate::cli::snapshot::SnapshotArgs; use crate::cli::tab::TabCmd; +use crate::cli::update::UpdateArgs; use crate::cli::waits::{WaitForNavigationArgs, WaitMsArgs}; /// Tool calls wait slightly longer than the daemon's 30s tool timeout so @@ -94,6 +96,9 @@ pub enum Command { #[command(name = "install-skill")] InstallSkill(InstallSkillArgs), + /// Check for and install bsk CLI updates. + Update(UpdateArgs), + /// Print (and optionally follow) the daemon log file. Logs(LogsCmd), diff --git a/crates/bsk-cli/src/cli/update.rs b/crates/bsk-cli/src/cli/update.rs new file mode 100644 index 0000000..679e0d2 --- /dev/null +++ b/crates/bsk-cli/src/cli/update.rs @@ -0,0 +1,883 @@ +//! `bsk update` — check for and install CLI updates. + +use std::collections::BTreeMap; +use std::io::{Cursor, Read, Write}; +use std::path::{Path, PathBuf}; +use std::time::{Duration, SystemTime}; + +use anyhow::{Context, Result, bail}; +use clap::Args; +use flate2::read::GzDecoder; +use semver::Version; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +use crate::cli::daemon::StartArgs; +use crate::cli::error::{CliError, Format}; + +pub const DEFAULT_MANIFEST_URL: &str = + "https://github.com/Tencent/BrowserSkill/releases/latest/download/version.json"; +const FETCH_TIMEOUT: Duration = Duration::from_secs(10); +const ARCHIVE_FETCH_TIMEOUT: Duration = Duration::from_secs(60); +const UPDATE_CHECK_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60); + +#[derive(Debug, Clone)] +pub struct UpdateManifest { + pub version: Version, + pub tag: String, + pub release_url: Option, + pub assets: BTreeMap, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ManifestAsset { + pub url: String, + pub sha256: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UpdateCandidate { + pub current: Version, + pub latest: Version, + pub tag: String, + pub release_url: Option, + pub asset: ManifestAsset, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum InstallAction { + Replaced, + Staged, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StagedReplacementPaths { + pub binary_path: PathBuf, + pub script_path: PathBuf, +} + +#[derive(Debug, Clone, Serialize)] +struct UpdateReport { + status: &'static str, + current_version: String, + latest_version: Option, + release_url: Option, + asset_url: Option, + install_action: Option<&'static str>, + message: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct UpdateCheckCache { + pub checked_at_epoch_secs: u64, + pub latest_version: String, +} + +impl UpdateCheckCache { + pub fn is_fresh(&self, now_epoch_secs: u64, interval: Duration) -> bool { + now_epoch_secs.saturating_sub(self.checked_at_epoch_secs) <= interval.as_secs() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ArchiveKind { + TarGz, + Zip, +} + +#[derive(Debug, Deserialize)] +struct RawManifest { + version: String, + tag: Option, + release_url: Option, + assets: BTreeMap, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +enum RawManifestAsset { + Url(String), + Rich { + url: String, + #[serde(default)] + sha256: Option, + }, +} + +impl UpdateManifest { + pub fn from_slice(bytes: &[u8]) -> Result { + let raw: RawManifest = serde_json::from_slice(bytes).context("parse update manifest")?; + let version = Version::parse(raw.version.trim_start_matches('v')) + .context("parse manifest version")?; + let tag = raw.tag.unwrap_or_else(|| format!("cli-v{version}")); + let assets = raw + .assets + .into_iter() + .map(|(platform, asset)| { + let asset = match asset { + RawManifestAsset::Url(url) => ManifestAsset { url, sha256: None }, + RawManifestAsset::Rich { url, sha256 } => ManifestAsset { url, sha256 }, + }; + (platform, asset) + }) + .collect(); + + Ok(Self { + version, + tag, + release_url: raw.release_url, + assets, + }) + } + + pub fn update_candidate( + &self, + current_version: &str, + platform_key: &str, + ) -> Result> { + let current = Version::parse(current_version.trim_start_matches('v')) + .context("parse current bsk version")?; + if self.version <= current { + return Ok(None); + } + + let asset = self + .assets + .get(platform_key) + .with_context(|| format!("no bsk release asset for platform `{platform_key}`"))? + .clone(); + + Ok(Some(UpdateCandidate { + current, + latest: self.version.clone(), + tag: self.tag.clone(), + release_url: self.release_url.clone(), + asset, + })) + } +} + +impl ArchiveKind { + pub fn from_url(url: &str) -> Result { + if url.ends_with(".tar.gz") || url.ends_with(".tgz") { + Ok(Self::TarGz) + } else if url.ends_with(".zip") { + Ok(Self::Zip) + } else { + bail!("unsupported bsk archive type: {url}"); + } + } +} + +#[derive(Debug, Clone, Args, Default)] +pub struct UpdateArgs { + /// Only check whether a newer version is available; do not install it. + #[arg(long)] + pub check: bool, + + /// Skip interactive confirmation prompts. + #[arg(short = 'y', long)] + pub yes: bool, + + /// Do not restart the daemon after replacing the CLI binary. + #[arg(long = "no-restart-daemon", default_value_t = true, action = clap::ArgAction::SetFalse)] + pub restart_daemon: bool, +} + +pub fn dispatch(args: UpdateArgs, format: Format) -> Result<(), CliError> { + run(args, format).map_err(CliError::Local) +} + +fn run(args: UpdateArgs, format: Format) -> Result<()> { + let client = update_http_client(ARCHIVE_FETCH_TIMEOUT)?; + let manifest = fetch_manifest_with_client(&client, &manifest_url())?; + let platform = current_platform_key()?; + let current_version = env!("CARGO_PKG_VERSION"); + let Some(candidate) = manifest.update_candidate(current_version, platform)? else { + return render_report( + format, + &UpdateReport { + status: "up_to_date", + current_version: current_version.to_string(), + latest_version: Some(manifest.version.to_string()), + release_url: manifest.release_url, + asset_url: None, + install_action: None, + message: format!("bsk {current_version} is already up to date"), + }, + ); + }; + + if args.check { + return render_report( + format, + &UpdateReport { + status: "update_available", + current_version: candidate.current.to_string(), + latest_version: Some(candidate.latest.to_string()), + release_url: candidate.release_url.clone(), + asset_url: Some(candidate.asset.url.clone()), + install_action: None, + message: format!( + "bsk {} is available (current {})", + candidate.latest, candidate.current + ), + }, + ); + } + + if !args.yes && !confirm_update(&candidate)? { + return render_report( + format, + &UpdateReport { + status: "cancelled", + current_version: candidate.current.to_string(), + latest_version: Some(candidate.latest.to_string()), + release_url: candidate.release_url, + asset_url: Some(candidate.asset.url), + install_action: None, + message: "update cancelled".to_string(), + }, + ); + } + + let action = install_candidate_with_client(&candidate, args.restart_daemon, &client)?; + let action_label = match action { + InstallAction::Replaced => "replaced", + InstallAction::Staged => "staged", + }; + + render_report( + format, + &UpdateReport { + status: "updated", + current_version: candidate.current.to_string(), + latest_version: Some(candidate.latest.to_string()), + release_url: candidate.release_url, + asset_url: Some(candidate.asset.url), + install_action: Some(action_label), + message: format!( + "updated bsk from {} to {}", + candidate.current, candidate.latest + ), + }, + ) +} + +pub fn current_platform_key() -> Result<&'static str> { + match (std::env::consts::OS, std::env::consts::ARCH) { + ("macos", "aarch64") => Ok("darwin-arm64"), + ("macos", "x86_64") => Ok("darwin-x64"), + ("linux", "aarch64") => Ok("linux-arm64"), + ("linux", "x86_64") => Ok("linux-x64"), + ("windows", "x86_64") => Ok("windows-x64"), + (os, arch) => bail!("unsupported platform for bsk auto-update: {os}-{arch}"), + } +} + +pub fn fetch_bytes(url: &str, timeout: Duration) -> Result> { + let client = update_http_client(timeout)?; + fetch_bytes_with_client(&client, url) +} + +fn update_http_client(timeout: Duration) -> Result { + reqwest::blocking::Client::builder() + .timeout(timeout) + .build() + .context("build update HTTP client") +} + +fn fetch_bytes_with_client(client: &reqwest::blocking::Client, url: &str) -> Result> { + let response = client + .get(url) + .send() + .with_context(|| format!("download {url}"))? + .error_for_status() + .with_context(|| format!("download {url}"))?; + Ok(response + .bytes() + .context("read update response body")? + .to_vec()) +} + +pub fn fetch_manifest(url: &str) -> Result { + let client = update_http_client(FETCH_TIMEOUT)?; + fetch_manifest_with_client(&client, url) +} + +fn fetch_manifest_with_client( + client: &reqwest::blocking::Client, + url: &str, +) -> Result { + let bytes = fetch_bytes_with_client(client, url)?; + UpdateManifest::from_slice(&bytes) +} + +fn install_candidate_with_client( + candidate: &UpdateCandidate, + restart_daemon: bool, + client: &reqwest::blocking::Client, +) -> Result { + let expected_sha = candidate.asset.sha256.as_deref().with_context(|| { + format!( + "release {} does not include a sha256 checksum; cannot safely auto-update", + candidate.tag + ) + })?; + let archive = fetch_bytes_with_client(client, &candidate.asset.url)?; + verify_sha256(&archive, expected_sha)?; + let kind = ArchiveKind::from_url(&candidate.asset.url)?; + let binary = extract_bsk_binary(&archive, kind)?; + let target = std::env::current_exe().context("locate current bsk executable")?; + + let daemon_was_running = restart_daemon && crate::daemon::info::read_valid()?.is_some(); + if daemon_was_running { + crate::daemon::start::run_stop().context("stop bsk daemon before update")?; + } + + let action = replace_binary_at_path(&target, &binary)?; + + if daemon_was_running && matches!(action, InstallAction::Replaced) { + crate::daemon::start::run_start(StartArgs::default()) + .context("restart bsk daemon after update")?; + } + + Ok(action) +} + +pub fn verify_sha256(bytes: &[u8], expected_hex: &str) -> Result<()> { + let actual = hex_sha256(bytes); + let expected = expected_hex.trim().to_ascii_lowercase(); + if actual != expected { + bail!("downloaded bsk archive checksum mismatch: expected {expected}, got {actual}"); + } + Ok(()) +} + +fn manifest_url() -> String { + std::env::var("BSK_UPDATE_MANIFEST_URL").unwrap_or_else(|_| DEFAULT_MANIFEST_URL.to_string()) +} + +pub fn update_hint_for_manifest( + manifest: &UpdateManifest, + current_version: &str, + platform_key: &str, +) -> Result> { + Ok(manifest + .update_candidate(current_version, platform_key)? + .map(|candidate| { + format!( + "A new bsk version is available: {} -> {}. Run `bsk update`.", + candidate.current, candidate.latest + ) + })) +} + +fn update_hint_for_cache(cache: &UpdateCheckCache, current_version: &str) -> Option { + let latest = cache.latest_version.as_str(); + let current = Version::parse(current_version.trim_start_matches('v')).ok()?; + let latest_version = Version::parse(latest.trim_start_matches('v')).ok()?; + (latest_version > current).then(|| { + format!("A new bsk version is available: {current} -> {latest_version}. Run `bsk update`.") + }) +} + +pub fn read_update_cache(path: &Path) -> Result> { + match std::fs::read(path) { + Ok(bytes) => { + let cache = serde_json::from_slice(&bytes) + .with_context(|| format!("parse {}", path.display()))?; + Ok(Some(cache)) + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(err) => Err(anyhow::Error::from(err).context(format!("read {}", path.display()))), + } +} + +pub fn write_update_cache(path: &Path, cache: &UpdateCheckCache) -> Result<()> { + let dir = path + .parent() + .with_context(|| format!("cache path has no parent: {}", path.display()))?; + std::fs::create_dir_all(dir).with_context(|| format!("create {}", dir.display()))?; + let tmp = path.with_file_name(format!( + "{}.tmp.{}", + path.file_name() + .and_then(|name| name.to_str()) + .unwrap_or("update-check.json"), + std::process::id() + )); + let payload = serde_json::to_vec_pretty(cache).context("encode update cache")?; + { + let mut file = + std::fs::File::create(&tmp).with_context(|| format!("create {}", tmp.display()))?; + file.write_all(&payload) + .with_context(|| format!("write {}", tmp.display()))?; + file.flush() + .with_context(|| format!("flush {}", tmp.display()))?; + file.sync_all() + .with_context(|| format!("sync {}", tmp.display()))?; + } + std::fs::rename(&tmp, path) + .with_context(|| format!("rename {} to {}", tmp.display(), path.display()))?; + sync_dir(dir); + Ok(()) +} + +fn now_epoch_secs() -> u64 { + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .map(|duration| duration.as_secs()) + .unwrap_or(0) +} + +fn refresh_update_cache(cache_path: &Path) -> Result> { + let manifest = fetch_manifest(&manifest_url())?; + let platform = current_platform_key()?; + let hint = update_hint_for_manifest(&manifest, env!("CARGO_PKG_VERSION"), platform)?; + write_update_cache( + cache_path, + &UpdateCheckCache { + checked_at_epoch_secs: now_epoch_secs(), + latest_version: manifest.version.to_string(), + }, + )?; + Ok(hint) +} + +pub fn maybe_spawn_background_check(flags: &super::GlobalFlags, command: &super::Command) { + if flags.quiet + || flags.json + || matches!( + command, + super::Command::Daemon(_) | super::Command::Update(_) + ) + { + return; + } + + let Ok(cache_path) = crate::daemon::paths::update_check_path() else { + return; + }; + let now = now_epoch_secs(); + match read_update_cache(&cache_path) { + Ok(Some(cache)) => { + if let Some(hint) = update_hint_for_cache(&cache, env!("CARGO_PKG_VERSION")) { + eprintln!("{hint}"); + } + if cache.is_fresh(now, UPDATE_CHECK_INTERVAL) { + return; + } + } + Ok(None) => {} + Err(err) => { + tracing::debug!(error = %err, "update cache read failed"); + } + } + + let _ = std::thread::Builder::new() + .name("bsk-update-check".to_string()) + .spawn(move || match refresh_update_cache(&cache_path) { + Ok(Some(hint)) => eprintln!("{hint}"), + Ok(None) => {} + Err(err) => tracing::debug!(error = %err, "background update check failed"), + }); +} + +fn confirm_update(candidate: &UpdateCandidate) -> Result { + dialoguer::Confirm::new() + .with_prompt(format!( + "Update bsk from {} to {}?", + candidate.current, candidate.latest + )) + .default(true) + .interact() + .context("read update confirmation") +} + +fn render_report(format: Format, report: &UpdateReport) -> Result<()> { + match format { + Format::Human => { + println!("{}", report.message); + if let Some(release_url) = &report.release_url { + println!("release: {release_url}"); + } + if matches!(report.install_action, Some("staged")) { + println!( + "restart your terminal and run `bsk --version` to verify the staged update" + ); + } + } + Format::Json => { + println!( + "{}", + serde_json::to_string_pretty(report).context("encode update report as JSON")? + ); + } + } + Ok(()) +} + +pub fn extract_bsk_binary(archive_bytes: &[u8], kind: ArchiveKind) -> Result> { + match kind { + ArchiveKind::TarGz => extract_bsk_from_tar_gz(archive_bytes), + ArchiveKind::Zip => extract_bsk_from_zip(archive_bytes), + } +} + +pub fn replace_binary_at_path(target: &Path, binary: &[u8]) -> Result { + #[cfg(windows)] + { + stage_windows_replacement(target, binary) + } + + #[cfg(not(windows))] + { + replace_binary_atomically(target, binary) + } +} + +#[cfg(not(windows))] +fn replace_binary_atomically(target: &Path, binary: &[u8]) -> Result { + let dir = target + .parent() + .with_context(|| format!("target path has no parent: {}", target.display()))?; + std::fs::create_dir_all(dir).with_context(|| format!("create {}", dir.display()))?; + + let file_name = target + .file_name() + .and_then(|name| name.to_str()) + .context("target path must have a UTF-8 file name")?; + let tmp = target.with_file_name(format!(".{file_name}.update-{}.tmp", std::process::id())); + + { + let mut file = + std::fs::File::create(&tmp).with_context(|| format!("create {}", tmp.display()))?; + file.write_all(binary) + .with_context(|| format!("write {}", tmp.display()))?; + file.flush() + .with_context(|| format!("flush {}", tmp.display()))?; + file.sync_all() + .with_context(|| format!("sync {}", tmp.display()))?; + } + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(0o755)) + .with_context(|| format!("chmod 0755 {}", tmp.display()))?; + } + + std::fs::rename(&tmp, target) + .with_context(|| format!("rename {} to {}", tmp.display(), target.display()))?; + sync_dir(dir); + Ok(InstallAction::Replaced) +} + +#[cfg(windows)] +fn stage_windows_replacement(target: &Path, binary: &[u8]) -> Result { + let paths = staged_replacement_paths(target, std::process::id())?; + std::fs::write(&paths.binary_path, binary) + .with_context(|| format!("write {}", paths.binary_path.display()))?; + std::fs::write( + &paths.script_path, + windows_replacement_script(target, &paths.binary_path), + ) + .with_context(|| format!("write {}", paths.script_path.display()))?; + Ok(InstallAction::Staged) +} + +pub fn staged_replacement_paths(target: &Path, pid: u32) -> Result { + let file_name = target + .file_name() + .and_then(|name| name.to_str()) + .context("target path must have a UTF-8 file name")?; + let binary_path = target.with_file_name(format!("{file_name}.update-{pid}")); + let script_path = target.with_file_name(format!("{file_name}.update-{pid}.cmd")); + Ok(StagedReplacementPaths { + binary_path, + script_path, + }) +} + +#[cfg(windows)] +fn windows_replacement_script(target: &Path, staged_binary: &Path) -> String { + format!( + "@echo off\r\n\ + setlocal\r\n\ + :retry\r\n\ + move /Y \"{}\" \"{}\" >nul 2>nul\r\n\ + if errorlevel 1 (\r\n\ + timeout /t 1 /nobreak >nul\r\n\ + goto retry\r\n\ + )\r\n", + staged_binary.display(), + target.display() + ) +} + +fn sync_dir(dir: &Path) { + if let Ok(dir_file) = std::fs::File::open(dir) { + let _ = dir_file.sync_all(); + } +} + +fn extract_bsk_from_tar_gz(archive_bytes: &[u8]) -> Result> { + let decoder = GzDecoder::new(Cursor::new(archive_bytes)); + let mut archive = tar::Archive::new(decoder); + for entry in archive.entries().context("read bsk tar.gz entries")? { + let mut entry = entry.context("read bsk tar.gz entry")?; + let path = entry.path().context("read bsk tar.gz entry path")?; + if path.file_name().is_some_and(|name| name == "bsk") { + let mut bytes = Vec::new(); + entry + .read_to_end(&mut bytes) + .context("read bsk binary from tar.gz")?; + return Ok(bytes); + } + } + bail!("bsk binary not found in tar.gz archive"); +} + +fn extract_bsk_from_zip(archive_bytes: &[u8]) -> Result> { + let cursor = Cursor::new(archive_bytes); + let mut archive = zip::ZipArchive::new(cursor).context("read bsk zip archive")?; + for index in 0..archive.len() { + let mut file = archive + .by_index(index) + .with_context(|| format!("read zip entry {index}"))?; + let name = std::path::Path::new(file.name()) + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or(""); + if name == "bsk.exe" || name == "bsk" { + let mut bytes = Vec::new(); + file.read_to_end(&mut bytes) + .context("read bsk binary from zip")?; + return Ok(bytes); + } + } + bail!("bsk binary not found in zip archive"); +} + +fn hex_sha256(bytes: &[u8]) -> String { + let digest = Sha256::digest(bytes); + let mut out = String::with_capacity(digest.len() * 2); + for byte in digest { + use std::fmt::Write as _; + let _ = write!(out, "{byte:02x}"); + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + use flate2::Compression; + use flate2::write::GzEncoder; + use std::io::Write; + + #[test] + fn parses_new_and_legacy_manifest_asset_shapes() { + let manifest = UpdateManifest::from_slice( + br#"{ + "name": "bsk", + "version": "0.2.0", + "tag": "cli-v0.2.0", + "release_url": "https://github.com/Tencent/BrowserSkill/releases/tag/cli-v0.2.0", + "assets": { + "darwin-arm64": { + "url": "https://example.test/bsk.tar.gz", + "sha256": "abc123" + }, + "linux-x64": "https://example.test/legacy.tar.gz" + } + }"#, + ) + .unwrap(); + + assert_eq!(manifest.version.to_string(), "0.2.0"); + assert_eq!( + manifest.assets["darwin-arm64"].url, + "https://example.test/bsk.tar.gz" + ); + assert_eq!( + manifest.assets["darwin-arm64"].sha256.as_deref(), + Some("abc123") + ); + assert_eq!( + manifest.assets["linux-x64"].url, + "https://example.test/legacy.tar.gz" + ); + assert!(manifest.assets["linux-x64"].sha256.is_none()); + } + + #[test] + fn classifies_newer_manifest_as_update_candidate() { + let manifest = UpdateManifest::from_slice( + br#"{ + "name": "bsk", + "version": "0.2.0", + "tag": "cli-v0.2.0", + "release_url": "https://github.com/Tencent/BrowserSkill/releases/tag/cli-v0.2.0", + "assets": { + "linux-x64": { + "url": "https://example.test/bsk.tar.gz", + "sha256": "abc123" + } + } + }"#, + ) + .unwrap(); + + let candidate = manifest + .update_candidate("0.1.7", "linux-x64") + .unwrap() + .unwrap(); + assert_eq!(candidate.latest.to_string(), "0.2.0"); + assert_eq!(candidate.asset.sha256.as_deref(), Some("abc123")); + assert!( + manifest + .update_candidate("0.2.0", "linux-x64") + .unwrap() + .is_none() + ); + } + + #[test] + fn verifies_sha256_before_installing_archive() { + let bytes = b"archive bytes"; + let expected = hex_sha256(bytes); + verify_sha256(bytes, &expected).unwrap(); + assert!(verify_sha256(bytes, "0000").is_err()); + } + + #[test] + fn extracts_bsk_binary_from_tar_gz_archive() { + let archive = tar_gz_with_bsk(b"new binary"); + let extracted = extract_bsk_binary(&archive, ArchiveKind::TarGz).unwrap(); + assert_eq!(extracted, b"new binary"); + } + + #[test] + fn extracts_bsk_binary_from_zip_archive() { + let archive = zip_with_bsk_exe(b"windows binary"); + let extracted = extract_bsk_binary(&archive, ArchiveKind::Zip).unwrap(); + assert_eq!(extracted, b"windows binary"); + } + + #[test] + fn replaces_binary_at_path() { + let tmp = tempfile::TempDir::new().unwrap(); + let target = tmp.path().join("bsk"); + std::fs::write(&target, b"old binary").unwrap(); + + let action = replace_binary_at_path(&target, b"new binary").unwrap(); + + assert_eq!(std::fs::read(&target).unwrap(), b"new binary"); + assert!(matches!(action, InstallAction::Replaced)); + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mode = std::fs::metadata(&target).unwrap().permissions().mode() & 0o777; + assert_eq!(mode, 0o755); + } + } + + #[test] + fn staged_replacement_paths_are_next_to_target() { + let target = std::path::Path::new("/tmp/bsk.exe"); + let paths = staged_replacement_paths(target, 42).unwrap(); + assert_eq!( + paths.binary_path, + std::path::Path::new("/tmp/bsk.exe.update-42") + ); + assert_eq!( + paths.script_path, + std::path::Path::new("/tmp/bsk.exe.update-42.cmd") + ); + } + + #[test] + fn cache_freshness_uses_epoch_seconds() { + let cache = UpdateCheckCache { + checked_at_epoch_secs: 100, + latest_version: "0.2.0".to_string(), + }; + assert!(cache.is_fresh(120, Duration::from_secs(30))); + assert!(!cache.is_fresh(131, Duration::from_secs(30))); + } + + #[test] + fn update_hint_is_generated_only_for_newer_versions() { + let manifest = UpdateManifest::from_slice( + br#"{ + "name": "bsk", + "version": "0.2.0", + "tag": "cli-v0.2.0", + "release_url": "https://github.com/Tencent/BrowserSkill/releases/tag/cli-v0.2.0", + "assets": { + "linux-x64": { + "url": "https://example.test/bsk.tar.gz", + "sha256": "abc123" + } + } + }"#, + ) + .unwrap(); + + let hint = update_hint_for_manifest(&manifest, "0.1.7", "linux-x64").unwrap(); + assert_eq!( + hint.as_deref(), + Some("A new bsk version is available: 0.1.7 -> 0.2.0. Run `bsk update`.") + ); + assert!( + update_hint_for_manifest(&manifest, "0.2.0", "linux-x64") + .unwrap() + .is_none() + ); + } + + #[test] + fn update_check_cache_round_trips_to_disk() { + let tmp = tempfile::TempDir::new().unwrap(); + let path = tmp.path().join("update-check.json"); + let cache = UpdateCheckCache { + checked_at_epoch_secs: 123, + latest_version: "0.2.0".to_string(), + }; + + write_update_cache(&path, &cache).unwrap(); + assert_eq!(read_update_cache(&path).unwrap(), Some(cache)); + } + + fn tar_gz_with_bsk(binary: &[u8]) -> Vec { + let mut tar_bytes = Vec::new(); + { + let mut builder = tar::Builder::new(&mut tar_bytes); + let mut header = tar::Header::new_gnu(); + header.set_size(binary.len() as u64); + header.set_mode(0o755); + header.set_cksum(); + builder.append_data(&mut header, "bsk", binary).unwrap(); + builder.finish().unwrap(); + } + + let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); + encoder.write_all(&tar_bytes).unwrap(); + encoder.finish().unwrap() + } + + fn zip_with_bsk_exe(binary: &[u8]) -> Vec { + let mut cursor = Cursor::new(Vec::new()); + { + let mut writer = zip::ZipWriter::new(&mut cursor); + writer + .start_file("bsk.exe", zip::write::SimpleFileOptions::default()) + .unwrap(); + writer.write_all(binary).unwrap(); + writer.finish().unwrap(); + } + cursor.into_inner() + } +} diff --git a/crates/bsk-cli/src/daemon/paths.rs b/crates/bsk-cli/src/daemon/paths.rs index 7f17797..1cf2bb5 100644 --- a/crates/bsk-cli/src/daemon/paths.rs +++ b/crates/bsk-cli/src/daemon/paths.rs @@ -81,6 +81,10 @@ pub fn log_path() -> Result { Ok(bsk_home()?.join("daemon.log")) } +pub fn update_check_path() -> Result { + Ok(bsk_home()?.join("update-check.json")) +} + pub fn log_dir() -> Result { bsk_home() } @@ -157,6 +161,7 @@ mod tests { assert_eq!(lock_path().unwrap(), home.join("daemon.lock")); assert_eq!(info_path().unwrap(), home.join("daemon.json")); assert_eq!(log_path().unwrap(), home.join("daemon.log")); + assert_eq!(update_check_path().unwrap(), home.join("update-check.json")); assert_eq!(sock_path().unwrap(), home.join("run").join("daemon.sock")); }); } diff --git a/crates/bsk-cli/src/daemon/queue.rs b/crates/bsk-cli/src/daemon/queue.rs index 732300f..3e79e5c 100644 --- a/crates/bsk-cli/src/daemon/queue.rs +++ b/crates/bsk-cli/src/daemon/queue.rs @@ -218,9 +218,7 @@ impl ToolQueueRegistry { /// Whether `sid`'s queue is still accepting new tool dispatches. pub fn is_accepting(&self, sid: &SessionId) -> bool { let guard = self.queues.lock().expect("tool queue registry poisoned"); - guard - .get(sid) - .is_some_and(|entry| entry.accepting) + guard.get(sid).is_some_and(|entry| entry.accepting) } /// Spawn the worker task for a session id. Idempotent: spawning the @@ -342,10 +340,21 @@ impl ToolQueueRegistry { entry.accepting = false; (entry.sender.clone(), Arc::clone(&entry.state)) }; - dispatch_with_sender(sender, state, sid.clone(), method, params, timeout, true, None).await + dispatch_with_sender( + sender, + state, + sid.clone(), + method, + params, + timeout, + true, + None, + ) + .await } } +#[allow(clippy::too_many_arguments)] async fn dispatch_with_sender( sender: mpsc::Sender, state: Arc>, diff --git a/crates/bsk-cli/src/main.rs b/crates/bsk-cli/src/main.rs index c5f461a..ab13d10 100644 --- a/crates/bsk-cli/src/main.rs +++ b/crates/bsk-cli/src/main.rs @@ -25,6 +25,7 @@ fn main() -> ExitCode { if !matches!(cli.command, Command::Daemon(_)) { init_cli_tracing(&cli.flags); } + cli::update::maybe_spawn_background_check(&cli.flags, &cli.command); let format = if cli.flags.json { Format::Json @@ -67,6 +68,7 @@ fn dispatch(cli: Cli, format: Format) -> Result<(), CliError> { }; cli::install_skill::dispatch(args, output) } + Command::Update(args) => cli::update::dispatch(args, format), Command::Logs(cmd) => cli::logs::run(cli::logs::LogsArgs { follow: cmd.follow, lines: cmd.lines, diff --git a/crates/bsk-cli/tests/cancel_forwarding.rs b/crates/bsk-cli/tests/cancel_forwarding.rs index eae82b4..b9913ca 100644 --- a/crates/bsk-cli/tests/cancel_forwarding.rs +++ b/crates/bsk-cli/tests/cancel_forwarding.rs @@ -25,9 +25,7 @@ use tokio_tungstenite::tungstenite::handshake::client::generate_key; use tokio_tungstenite::tungstenite::http::Request; use tokio_tungstenite::tungstenite::protocol::Message; -use support::{ - wait_for_abort_registered, wait_for_inflight_forwarded, wait_until, -}; +use support::{wait_for_abort_registered, wait_for_inflight_forwarded, wait_until}; const TEST_EXT_ID: &str = "abcdefghijklmnopabcdefghijklmnop"; @@ -551,9 +549,7 @@ async fn cancel_arriving_during_promote_critical_section_keeps_request_cancel_in wait_until( "extension snapshot/cancel counters to match", Duration::from_secs(2), - || { - snapshots_clone.load(AOrdering::SeqCst) == cancels_clone.load(AOrdering::SeqCst) - }, + || snapshots_clone.load(AOrdering::SeqCst) == cancels_clone.load(AOrdering::SeqCst), ) .await; diff --git a/crates/bsk-cli/tests/cli_parse.rs b/crates/bsk-cli/tests/cli_parse.rs index 1111607..f3a4737 100644 --- a/crates/bsk-cli/tests/cli_parse.rs +++ b/crates/bsk-cli/tests/cli_parse.rs @@ -110,6 +110,17 @@ fn parses_install_skill_subcommand() { assert!(matches!(cli.command, Command::InstallSkill(_))); } +#[test] +fn parses_update_subcommand_with_flags() { + let cli = parse(&["bsk", "update", "--check", "--yes", "--no-restart-daemon"]); + let Command::Update(args) = cli.command else { + panic!("expected update subcommand"); + }; + assert!(args.check); + assert!(args.yes); + assert!(!args.restart_daemon); +} + #[test] fn duration_parser_accepts_units() { assert_eq!(parse_duration("750ms").unwrap(), Duration::from_millis(750)); diff --git a/crates/bsk-cli/tests/per_session_queue.rs b/crates/bsk-cli/tests/per_session_queue.rs index 63d0d11..f548ee6 100644 --- a/crates/bsk-cli/tests/per_session_queue.rs +++ b/crates/bsk-cli/tests/per_session_queue.rs @@ -272,9 +272,7 @@ async fn same_session_dispatches_run_after_previous_completes() { .expect("request did not reach extension") .expect("request channel closed"); assert_eq!(tag, format!("job-{i}")); - reply_tx - .send((rpc_id, json!({"acked": true}))) - .unwrap(); + reply_tx.send((rpc_id, json!({"acked": true}))).unwrap(); let r = dispatch_task.await.unwrap(); assert!(r.is_ok(), "dispatch should succeed, got {r:?}"); } @@ -333,7 +331,10 @@ async fn session_stop_fast_fails_while_tool_is_in_flight() { ) .await; assert!( - matches!(stop_result, Err(bsk::daemon::sessions::StopSessionError::SessionBusy)), + matches!( + stop_result, + Err(bsk::daemon::sessions::StopSessionError::SessionBusy) + ), "session.stop should fast-fail while a tool is active, got {stop_result:?}" ); assert!( @@ -437,7 +438,10 @@ async fn concurrent_same_session_dispatch_returns_session_busy_immediately() { let rpc = DispatchError::SessionBusy.into_rpc(); assert_eq!(rpc.code, ErrorCode::Timeout); assert_eq!( - rpc.data.as_ref().and_then(|d| d.get("reason")).and_then(|v| v.as_str()), + rpc.data + .as_ref() + .and_then(|d| d.get("reason")) + .and_then(|v| v.as_str()), Some(bsk::rpc_reason::SESSION_BUSY) ); @@ -471,7 +475,10 @@ async fn concurrent_same_session_dispatch_returns_session_busy_immediately() { .send((after_rpc_id, json!({"acked": true}))) .unwrap(); let after = after_task.await.unwrap(); - assert!(after.is_ok(), "dispatch should succeed after first completes, got {after:?}"); + assert!( + after.is_ok(), + "dispatch should succeed after first completes, got {after:?}" + ); handle.shutdown().await; } diff --git a/crates/bsk-cli/tests/support/mod.rs b/crates/bsk-cli/tests/support/mod.rs index 9d2227a..0ef9d53 100644 --- a/crates/bsk-cli/tests/support/mod.rs +++ b/crates/bsk-cli/tests/support/mod.rs @@ -8,9 +8,9 @@ use std::sync::Arc; use std::time::{Duration, Instant}; +use bsk::daemon::queue::ToolQueueRegistry; use bsk::daemon::sessions::SessionId; use bsk::daemon::state::DaemonState; -use bsk::daemon::queue::ToolQueueRegistry; use bsk_protocol::RpcId; const DEFAULT_TIMEOUT: Duration = Duration::from_secs(2); @@ -81,11 +81,9 @@ pub async fn wait_for_inflight_forwarded(state: &Arc, rpc_id: &RpcI pub async fn wait_for_abort_registered(state: &Arc, rpc_id: &RpcId) { let id = rpc_id.clone(); - wait_until( - &format!("abort token for {id}"), - DEFAULT_TIMEOUT, - || state.abort_registry.contains(&id), - ) + wait_until(&format!("abort token for {id}"), DEFAULT_TIMEOUT, || { + state.abort_registry.contains(&id) + }) .await; } diff --git a/scripts/render-version-json.mjs b/scripts/render-version-json.mjs index 39479cf..88e97be 100644 --- a/scripts/render-version-json.mjs +++ b/scripts/render-version-json.mjs @@ -8,10 +8,13 @@ * --repo Tencent/BrowserSkill \ * --server-url https://github.com \ * --branch main \ + * --dist dist \ * --out version.json */ -import { writeFileSync } from "node:fs"; +import { createHash } from "node:crypto"; +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; import { parseArgs } from "node:util"; /** Platform key -> Rust target triple */ @@ -29,6 +32,7 @@ const { values } = parseArgs({ repo: { type: "string" }, "server-url": { type: "string", default: "https://github.com" }, branch: { type: "string", default: "main" }, + dist: { type: "string" }, out: { type: "string" }, }, }); @@ -37,11 +41,12 @@ const version = values.version?.replace(/^v/, ""); const repo = values.repo; const serverUrl = values["server-url"]?.replace(/\/$/, "") ?? "https://github.com"; const branch = values.branch ?? "main"; +const dist = values.dist; const out = values.out; if (!version || !repo || !out) { console.error( - "Usage: --version --repo --out [--server-url URL] [--branch main]", + "Usage: --version --repo --out [--server-url URL] [--branch main] [--dist dist]", ); process.exit(1); } @@ -56,10 +61,33 @@ function assetFilename(platformKey, triple) { return `bsk-v${version}-${triple}.tar.gz`; } +function sha256File(path) { + return createHash("sha256").update(readFileSync(path)).digest("hex"); +} + +function assetEntry(platformKey, triple) { + const filename = assetFilename(platformKey, triple); + const url = `${releaseBase}/${filename}`; + if (!dist) { + return url; + } + + const path = join(dist, filename); + if (!existsSync(path)) { + console.error(`missing release asset for checksum: ${path}`); + process.exit(1); + } + + return { + url, + sha256: sha256File(path), + }; +} + const assets = Object.fromEntries( Object.entries(PLATFORMS).map(([key, triple]) => [ key, - `${releaseBase}/${assetFilename(key, triple)}`, + assetEntry(key, triple), ]), ); diff --git a/scripts/render-version-json.test.mjs b/scripts/render-version-json.test.mjs new file mode 100644 index 0000000..5dc896d --- /dev/null +++ b/scripts/render-version-json.test.mjs @@ -0,0 +1,52 @@ +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { execFileSync } from "node:child_process"; +import test from "node:test"; + +const scriptDir = dirname(fileURLToPath(import.meta.url)); +const scriptPath = join(scriptDir, "render-version-json.mjs"); + +test("renders asset URLs with sha256 checksums from dist files", () => { + const tmp = mkdtempSync(join(tmpdir(), "bsk-version-json-")); + const dist = join(tmp, "dist"); + const out = join(tmp, "version.json"); + const version = "9.8.7"; + const archive = `bsk-v${version}-aarch64-apple-darwin.tar.gz`; + const archiveBytes = Buffer.from("fake darwin arm64 archive\n"); + mkdirSync(dist); + writeFileSync(join(dist, archive), archiveBytes); + writeFileSync(join(dist, `bsk-v${version}-x86_64-apple-darwin.tar.gz`), "darwin x64\n"); + writeFileSync( + join(dist, `bsk-v${version}-x86_64-unknown-linux-musl.tar.gz`), + "linux x64\n", + ); + writeFileSync( + join(dist, `bsk-v${version}-aarch64-unknown-linux-musl.tar.gz`), + "linux arm64\n", + ); + writeFileSync(join(dist, `bsk-v${version}-x86_64-pc-windows-msvc.zip`), "windows x64\n"); + + execFileSync(process.execPath, [ + scriptPath, + "--version", + version, + "--repo", + "Tencent/BrowserSkill", + "--dist", + dist, + "--out", + out, + ]); + + const manifest = JSON.parse(readFileSync(out, "utf8")); + const expectedSha = createHash("sha256").update(archiveBytes).digest("hex"); + assert.equal( + manifest.assets["darwin-arm64"].url, + `https://github.com/Tencent/BrowserSkill/releases/download/cli-v${version}/${archive}`, + ); + assert.equal(manifest.assets["darwin-arm64"].sha256, expectedSha); +});