diff --git a/AGENTS.md b/AGENTS.md index 2c6a11f..1b7cb2b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,6 +20,7 @@ cargo run -- - `run` - Run application on a device or emulator (supports `--daemon`, `--app-log-level`) - `device` - Manage device(s), include emulator and physical device (`list`) - `completion` - Generate shell completion scripts (supports unstable-dynamic completion) +- `update` - Update heco to the latest version (auto-detects Cargo/Homebrew/Winget) ## Key Config Paths diff --git a/Cargo.lock b/Cargo.lock index 7e3eda9..471f154 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 = "anstream" version = "1.0.0" @@ -58,6 +64,12 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[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" @@ -88,6 +100,16 @@ version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +[[package]] +name = "cc" +version = "1.2.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -183,6 +205,22 @@ dependencies = [ "windows-sys 0.61.2", ] +[[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" @@ -192,6 +230,15 @@ 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 = "crypto-common" version = "0.1.6" @@ -269,6 +316,17 @@ dependencies = [ "objc2", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "encode_unicode" version = "1.0.0" @@ -297,12 +355,52 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[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 = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[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-core" version = "0.3.32" @@ -411,15 +509,119 @@ dependencies = [ "serde_json", "serde_json5", "toml", + "ureq", "winreg", ] +[[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" @@ -505,6 +707,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 = "log" version = "0.4.29" @@ -517,6 +725,33 @@ 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 = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nix" version = "0.31.2" @@ -562,6 +797,50 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "openssl" +version = "0.10.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -584,6 +863,12 @@ version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pest" version = "2.8.6" @@ -633,12 +918,27 @@ 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 = "portable-atomic" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -703,6 +1003,38 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[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 = "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" @@ -795,12 +1127,30 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + [[package]] name = "slab" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "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" @@ -818,6 +1168,17 @@ dependencies = [ "unicode-ident", ] +[[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 = "tempfile" version = "3.27.0" @@ -860,6 +1221,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "toml" version = "1.1.2+spec-1.1.0" @@ -929,12 +1300,52 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64", + "flate2", + "log", + "native-tls", + "once_cell", + "serde", + "serde_json", + "url", +] + +[[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 = "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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -1326,12 +1737,95 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +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 = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +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 = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index c441036..18f9e14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ clap_complete = { version = "4.5", features = ["unstable-dynamic"] } serde_json = "1.0.149" indicatif = "0.17" console = "0.15.8" +ureq = { version = "2", default-features = false, features = ["json", "native-tls", "gzip"] } [target."cfg(windows)".dependencies] winreg = "0.56.0" diff --git a/README.md b/README.md index 5cc854a..6b8feed 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,7 @@ HecO 配置文件采用 TOML 格式(强制 **kebab-case** 规范)。 - `run` - 在设备或模拟器上运行应用,并追踪全生命周期日志 - `device` - 物理设备与模拟器设备连接状态管理 - `completion` - 生成对应 Shell 环境的自动补全脚本 +- `update` - 自动检测安装方式(Cargo/Homebrew/Winget)并更新 heco 至最新版本 ## 🤝 参与贡献 diff --git a/src/main.rs b/src/main.rs index 5f07266..bedd24f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,8 @@ mod lint; mod progress; mod project; mod run; +mod update; +mod updater; use crate::build::{BuildArgs, handle_build}; use clean::{CleanArgs, handle_clean}; @@ -88,6 +90,11 @@ enum Commands { /// Generate shell completion scripts #[command(name = "completion", display_order = 8)] Completion(CompletionArgs), + /// Update heco to the latest version + #[command(name = "update", display_order = 9)] + Update(crate::update::UpdateArgs), + #[command(name = "__internal_update_check", hide = true)] + InternalUpdateCheck, } fn main() -> anyhow::Result<()> { @@ -111,6 +118,23 @@ fn main() -> anyhow::Result<()> { ColorChoice::Auto => {} } + let is_internal_check = matches!(&cli.command, Commands::InternalUpdateCheck); + let is_update = matches!(&cli.command, Commands::Update(_)); + let is_completion = matches!(&cli.command, Commands::Completion(_)); + + if !is_internal_check + && !is_completion + && crate::updater::should_check_update() + && let Ok(exe) = std::env::current_exe() + { + let _ = std::process::Command::new(exe) + .arg("__internal_update_check") + .stdin(std::process::Stdio::null()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .spawn(); + } + match cli.command { Commands::Build(args) => { handle_build(args); @@ -136,6 +160,30 @@ fn main() -> anyhow::Result<()> { Commands::Completion(args) => { handle_completion(args)?; } + Commands::Update(args) => { + crate::update::handle_update(args)?; + } + Commands::InternalUpdateCheck => { + let _ = crate::updater::check_and_cache_update(); + return Ok(()); + } + } + + if !is_internal_check + && !is_update + && !is_completion + && let Some(latest) = crate::updater::get_cached_update() + { + println!( + "\n{}", + console::style(format!( + "🌟 A new version is available ({} -> {}). Run `heco update` to update.", + env!("CARGO_PKG_VERSION"), + latest + )) + .yellow() + ); } + Ok(()) } diff --git a/src/update.rs b/src/update.rs new file mode 100644 index 0000000..628c9ee --- /dev/null +++ b/src/update.rs @@ -0,0 +1,104 @@ +use clap::Parser; +use std::env; +use std::process::{Command, Stdio}; + +#[derive(Parser, Debug)] +pub struct UpdateArgs {} + +pub fn handle_update(_args: UpdateArgs) -> anyhow::Result<()> { + println!("Checking for updates..."); + + let current_version = env!("CARGO_PKG_VERSION"); + let latest_version = match crate::updater::fetch_latest_version() { + Ok(v) => v, + Err(e) => { + anyhow::bail!("Failed to fetch latest version: {}", e); + } + }; + + if !is_newer_version(current_version, &latest_version) { + println!( + "You are already using the latest version ({})", + current_version + ); + return Ok(()); + } + + println!( + "Found new version: {} -> {}", + current_version, latest_version + ); + println!("Updating heco..."); + + // Auto detect installation method based on executable path + let exe_path = match env::current_exe() { + Ok(path) => path, + Err(e) => { + anyhow::bail!("Failed to get current executable path: {}", e); + } + }; + + let exe_str = exe_path.to_string_lossy().to_lowercase(); + + let mut command = if exe_str.contains(".cargo/bin") || exe_str.contains(".cargo\\bin") { + println!("Detected installation method: Cargo"); + let mut cmd = Command::new("cargo"); + cmd.args(["install", "heco"]); + cmd + } else if exe_str.contains("homebrew") + || exe_str.contains("linuxbrew") + || exe_str.contains("cellar") + { + println!("Detected installation method: Homebrew"); + let mut cmd = Command::new("brew"); + cmd.args(["upgrade", "heco"]); + cmd + } else if exe_str.contains("windowsapps") + || exe_str.contains("winget") + || exe_str.contains("winget\\packages") + { + println!("Detected installation method: Winget"); + let mut cmd = Command::new("winget"); + cmd.args(["upgrade", "HecO-CLI.HecO", "-s", "winget"]); + cmd + } else { + println!( + "⚠️ Warning: Could not automatically detect the installation method. Please update heco manually or download from https://github.com/heco-cli/heco/releases/latest" + ); + return Ok(()); + }; + + let status = command + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .status()?; + + if status.success() { + println!("✨ Successfully updated heco to version {}", latest_version); + + // Update the cache so it doesn't prompt again immediately + let _ = crate::updater::update_cache(&latest_version); + } else { + anyhow::bail!("Update failed with status: {}", status); + } + + Ok(()) +} + +fn is_newer_version(current: &str, latest: &str) -> bool { + let current_parts: Vec<&str> = current.split('.').collect(); + let latest_parts: Vec<&str> = latest.split('.').collect(); + + for i in 0..std::cmp::min(current_parts.len(), latest_parts.len()) { + let curr_num: u32 = current_parts[i].parse().unwrap_or(0); + let latest_num: u32 = latest_parts[i].parse().unwrap_or(0); + + if latest_num > curr_num { + return true; + } else if latest_num < curr_num { + return false; + } + } + + latest_parts.len() > current_parts.len() +} diff --git a/src/updater.rs b/src/updater.rs new file mode 100644 index 0000000..7c2a007 --- /dev/null +++ b/src/updater.rs @@ -0,0 +1,125 @@ +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::PathBuf; +use std::time::{SystemTime, UNIX_EPOCH}; + +const CACHE_FILE_NAME: &str = "update_cache.json"; +const CACHE_VALID_SECONDS: u64 = 24 * 60 * 60; // 24 hours +const CRATES_IO_API: &str = "https://crates.io/api/v1/crates/heco"; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct UpdateCache { + pub latest_version: String, + pub last_check: u64, +} + +fn get_cache_path() -> Option { + let mut path = dirs::home_dir()?; + path.push(".config"); + path.push("heco"); + fs::create_dir_all(&path).ok()?; + path.push(CACHE_FILE_NAME); + Some(path) +} + +pub fn get_cached_update() -> Option { + let path = get_cache_path()?; + let content = fs::read_to_string(&path).ok()?; + let cache: UpdateCache = serde_json::from_str(&content).ok()?; + + // Even if cache is expired, we can still return the version it knows + // The main process will spawn a background check if it's expired. + let current_version = env!("CARGO_PKG_VERSION"); + + if is_newer_version(current_version, &cache.latest_version) { + Some(cache.latest_version) + } else { + None + } +} + +pub fn should_check_update() -> bool { + if let Some(path) = get_cache_path() + && let Ok(content) = fs::read_to_string(&path) + && let Ok(cache) = serde_json::from_str::(&content) + { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + if now >= cache.last_check && now - cache.last_check < CACHE_VALID_SECONDS { + return false; + } + } + true +} + +#[derive(Deserialize)] +struct CrateResponse { + #[serde(rename = "crate")] + crate_data: CrateData, +} + +#[derive(Deserialize)] +struct CrateData { + max_version: String, +} + +pub fn fetch_latest_version() -> anyhow::Result { + let agent = ureq::builder() + .user_agent(concat!("heco/", env!("CARGO_PKG_VERSION"))) + .build(); + + let resp = agent.get(CRATES_IO_API).call(); + match resp { + Ok(response) => { + let crate_resp: CrateResponse = response.into_json()?; + Ok(crate_resp.crate_data.max_version) + } + Err(e) => { + // If the crate doesn't exist yet (404), return an error + anyhow::bail!("{e}"); + } + } +} + +pub fn update_cache(latest_version: &str) -> anyhow::Result<()> { + if let Some(path) = get_cache_path() { + let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + let cache = UpdateCache { + latest_version: latest_version.to_string(), + last_check: now, + }; + fs::write(&path, serde_json::to_string(&cache)?)?; + } + Ok(()) +} + +pub fn check_and_cache_update() -> anyhow::Result<()> { + if !should_check_update() { + return Ok(()); + } + + let latest_version = fetch_latest_version()?; + update_cache(&latest_version)?; + + Ok(()) +} + +fn is_newer_version(current: &str, latest: &str) -> bool { + let current_parts: Vec<&str> = current.split('.').collect(); + let latest_parts: Vec<&str> = latest.split('.').collect(); + + for i in 0..std::cmp::min(current_parts.len(), latest_parts.len()) { + let curr_num: u32 = current_parts[i].parse().unwrap_or(0); + let latest_num: u32 = latest_parts[i].parse().unwrap_or(0); + + if latest_num > curr_num { + return true; + } else if latest_num < curr_num { + return false; + } + } + + latest_parts.len() > current_parts.len() +}