diff --git a/Cargo.lock b/Cargo.lock index 002e6826cb..edbfd5feee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -173,6 +173,12 @@ dependencies = [ "gimli", ] +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "adler2" version = "2.0.1" @@ -1415,7 +1421,7 @@ version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25f535879a207fce0db74b679cfc3e91a3159c8144d717d55f5832aea9eef46e" dependencies = [ - "base64-simd", + "base64-simd 0.8.0", "bytes", "bytes-utils", "futures-core", @@ -1454,7 +1460,7 @@ dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", "aws-smithy-types", - "rustc_version", + "rustc_version 0.4.1", "tracing", ] @@ -1584,6 +1590,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "az" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" + [[package]] name = "azure-speech" version = "0.10.0" @@ -1639,7 +1651,7 @@ dependencies = [ "addr2line", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.8.9", "object 0.37.3", "rustc-demangle", "windows-link 0.2.1", @@ -1669,13 +1681,22 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64-simd" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "781dd20c3aff0bd194fe7d2a977dd92f21c173891f3a03b677359e5fa457e5d5" +dependencies = [ + "simd-abstraction", +] + [[package]] name = "base64-simd" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" dependencies = [ - "outref", + "outref 0.5.2", "vsimd", ] @@ -1714,7 +1735,7 @@ dependencies = [ "path_abs", "plist", "regex", - "semver", + "semver 1.0.27", "serde", "serde_derive", "serde_with", @@ -1767,7 +1788,7 @@ dependencies = [ "rustc-hash 1.1.0", "shlex", "syn 2.0.108", - "which", + "which 4.4.2", ] [[package]] @@ -1790,7 +1811,27 @@ dependencies = [ "rustc-hash 1.1.0", "shlex", "syn 2.0.108", - "which", + "which 4.4.2", +] + +[[package]] +name = "bindgen" +version = "0.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" +dependencies = [ + "bitflags 2.10.0", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.108", ] [[package]] @@ -1874,6 +1915,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block" version = "0.1.6" @@ -2430,6 +2483,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "capacity_builder" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58ec49028cb308564429cd8fac4ef21290067a0afe8f5955330a8d487d0d790c" +dependencies = [ + "itoa", +] + [[package]] name = "cargo-husky" version = "1.5.0" @@ -2453,7 +2515,7 @@ checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" dependencies = [ "camino", "cargo-platform", - "semver", + "semver 1.0.27", "serde", "serde_json", "thiserror 2.0.17", @@ -2997,6 +3059,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cooked-waker" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147be55d677052dabc6b22252d5dd0fd4c29c8c27aa4f2fbef0f94aa003b406f" + [[package]] name = "cookie" version = "0.18.1" @@ -3800,6 +3868,117 @@ dependencies = [ "uuid", ] +[[package]] +name = "deno_core" +version = "0.338.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "113f3f08bd5daf99f1a7876c0f99cd8c3c609439fa0b808311ec856a253e95f0" +dependencies = [ + "anyhow", + "az", + "bincode", + "bit-set", + "bit-vec", + "bytes", + "capacity_builder", + "cooked-waker", + "deno_core_icudata", + "deno_error", + "deno_ops", + "deno_path_util", + "deno_unsync", + "futures", + "indexmap 2.12.0", + "libc", + "memoffset", + "parking_lot 0.12.5", + "percent-encoding", + "pin-project", + "serde", + "serde_json", + "serde_v8", + "smallvec 1.15.1", + "sourcemap", + "static_assertions", + "thiserror 2.0.17", + "tokio", + "url", + "v8", + "wasm_dep_analyzer", +] + +[[package]] +name = "deno_core_icudata" +version = "0.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4dccb6147bb3f3ba0c7a48e993bfeb999d2c2e47a81badee80e2b370c8d695" + +[[package]] +name = "deno_error" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e983933fb4958fbe1e0a63c1e89a2af72b12c409e86404e547955564e6e217b8" +dependencies = [ + "deno_error_macro", + "libc", + "serde", + "serde_json", + "tokio", + "url", +] + +[[package]] +name = "deno_error_macro" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ad5ae3ef15db33e917d6ed54b53d0a98d068c4d1217eb35a4997423203c3ef" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "deno_ops" +version = "0.214.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ad885bf882be535f7714c713042129acba6f31a8efb5e6b2298f6e40cab9b16" +dependencies = [ + "indexmap 2.12.0", + "proc-macro-rules", + "proc-macro2", + "quote", + "stringcase", + "strum 0.25.0", + "strum_macros 0.25.3", + "syn 2.0.108", + "thiserror 2.0.17", +] + +[[package]] +name = "deno_path_util" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8850326ea9cb786aafd938f3de9866432904c0bae3aa0139a7a4e570b0174f6" +dependencies = [ + "deno_error", + "percent-encoding", + "sys_traits", + "thiserror 2.0.17", + "url", +] + +[[package]] +name = "deno_unsync" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6742a724e8becb372a74c650a1aefb8924a5b8107f7d75b3848763ea24b27a87" +dependencies = [ + "futures-util", + "parking_lot 0.12.5", + "tokio", +] + [[package]] name = "der" version = "0.6.1" @@ -3895,7 +4074,7 @@ dependencies = [ "convert_case 0.4.0", "proc-macro2", "quote", - "rustc_version", + "rustc_version 0.4.1", "syn 2.0.108", ] @@ -3946,6 +4125,7 @@ dependencies = [ "tauri-plugin-deeplink2", "tauri-plugin-detect", "tauri-plugin-dialog", + "tauri-plugin-extensions", "tauri-plugin-fs", "tauri-plugin-hooks", "tauri-plugin-http", @@ -4326,7 +4506,7 @@ checksum = "55a075fc573c64510038d7ee9abc7990635863992f83ebc52c8b433b8411a02e" dependencies = [ "cc", "memchr", - "rustc_version", + "rustc_version 0.4.1", "toml 0.9.8", "vswhom", "winreg 0.55.0", @@ -4525,6 +4705,18 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "extensions-runtime" +version = "0.1.0" +dependencies = [ + "deno_core", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tracing", +] + [[package]] name = "eyre" version = "0.6.12" @@ -4644,7 +4836,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" dependencies = [ "memoffset", - "rustc_version", + "rustc_version 0.4.1", ] [[package]] @@ -4729,7 +4921,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.8.9", ] [[package]] @@ -4817,6 +5009,22 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "fslock" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04412b8935272e3a9bae6f48c7bfff74c2911f60525404edfdd28e49884c3bfb" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futf" version = "0.1.5" @@ -6439,7 +6647,7 @@ dependencies = [ "google-cloud-gax", "http 1.3.1", "reqwest 0.12.24", - "rustc_version", + "rustc_version 0.4.1", "rustls 0.23.34", "rustls-pemfile 2.2.0", "serde", @@ -6483,7 +6691,7 @@ dependencies = [ "http-body-util", "percent-encoding", "reqwest 0.12.24", - "rustc_version", + "rustc_version 0.4.1", "serde", "serde_json", "thiserror 2.0.17", @@ -6795,6 +7003,15 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "gzip-header" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95cc527b92e6029a62960ad99aa8a6660faa4555fe5f731aab13aa6a921795a2" +dependencies = [ + "crc32fast", +] + [[package]] name = "h2" version = "0.3.27" @@ -9118,6 +9335,15 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e856fdd13623a2f5f2f54676a4ee49502a96a80ef4a62bcedd23d52427c44d43" +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -9506,6 +9732,7 @@ checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", + "rand 0.8.5", "serde", ] @@ -10464,6 +10691,12 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "outref" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f222829ae9293e33a9f5e9f440c6760a3d450a64affe1846486b140db81c1f4" + [[package]] name = "outref" version = "0.5.2" @@ -11115,7 +11348,7 @@ dependencies = [ "crc32fast", "fdeflate", "flate2", - "miniz_oxide", + "miniz_oxide 0.8.9", ] [[package]] @@ -11128,7 +11361,7 @@ dependencies = [ "crc32fast", "fdeflate", "flate2", - "miniz_oxide", + "miniz_oxide 0.8.9", ] [[package]] @@ -11321,6 +11554,29 @@ version = "0.5.20+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" +[[package]] +name = "proc-macro-rules" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07c277e4e643ef00c1233393c673f655e3672cf7eb3ba08a00bdd0ea59139b5f" +dependencies = [ + "proc-macro-rules-macros", + "proc-macro2", + "syn 2.0.108", +] + +[[package]] +name = "proc-macro-rules-macros" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "207fffb0fe655d1d47f6af98cc2793405e85929bdbc420d685554ff07be27ac7" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "proc-macro2" version = "1.0.103" @@ -11616,6 +11872,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.7.3" @@ -12416,13 +12678,22 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver 0.9.0", +] + [[package]] name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver", + "semver 1.0.27", ] [[package]] @@ -12927,6 +13198,15 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16c2f82143577edb4921b71ede051dac62ca3c16084e918bf7b40c96ae10eb33" +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + [[package]] name = "semver" version = "1.0.27" @@ -12937,6 +13217,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + [[package]] name = "sentencepiece" version = "0.11.3" @@ -13016,7 +13302,7 @@ dependencies = [ "hostname", "libc", "os_info", - "rustc_version", + "rustc_version 0.4.1", "sentry-core", "uname", ] @@ -13322,6 +13608,20 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_v8" +version = "0.247.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12bbfafb7b707cbed49d1eaf48f4aa41b5ff57f813d1a80f77244e6e2fa4507e" +dependencies = [ + "deno_error", + "num-bigint", + "serde", + "smallvec 1.15.1", + "thiserror 2.0.17", + "v8", +] + [[package]] name = "serde_with" version = "3.15.1" @@ -13589,6 +13889,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "simd-abstraction" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cadb29c57caadc51ff8346233b5cec1d240b68ce55cf1afc764818791876987" +dependencies = [ + "outref 0.1.0", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -13843,6 +14152,25 @@ dependencies = [ "system-deps", ] +[[package]] +name = "sourcemap" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "208d40b9e8cad9f93613778ea295ed8f3c2b1824217c6cfc7219d3f6f45b96d4" +dependencies = [ + "base64-simd 0.7.0", + "bitvec", + "data-encoding", + "debugid", + "if_chain", + "rustc-hash 1.1.0", + "rustc_version 0.2.3", + "serde", + "serde_json", + "unicode-id-start", + "url", +] + [[package]] name = "specta" version = "2.0.0-rc.22" @@ -14006,6 +14334,12 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "stringcase" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04028eeb851ed08af6aba5caa29f2d59a13ed168cee4d6bd753aeefcf1d636b0" + [[package]] name = "stringprep" version = "0.1.5" @@ -14386,6 +14720,26 @@ dependencies = [ "libc", ] +[[package]] +name = "sys_traits" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1495a604cd38eeb30c408724966cd31ca1b68b5a97e3afc474c0d719bfeec5a" +dependencies = [ + "sys_traits_macros", +] + +[[package]] +name = "sys_traits_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "181f22127402abcf8ee5c83ccd5b408933fec36a6095cf82cda545634692657e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "sysctl" version = "0.5.5" @@ -14548,6 +14902,12 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tar" version = "0.4.44" @@ -14641,7 +15001,7 @@ dependencies = [ "heck 0.5.0", "json-patch 3.0.1", "schemars 0.8.22", - "semver", + "semver 1.0.27", "serde", "serde_json", "tauri-utils", @@ -14664,7 +15024,7 @@ dependencies = [ "png 0.17.16", "proc-macro2", "quote", - "semver", + "semver 1.0.27", "serde", "serde_json", "sha2", @@ -14976,6 +15336,23 @@ dependencies = [ "url", ] +[[package]] +name = "tauri-plugin-extensions" +version = "0.1.0" +dependencies = [ + "extensions-runtime", + "serde", + "serde_json", + "specta", + "specta-typescript", + "tauri", + "tauri-plugin", + "tauri-specta", + "thiserror 2.0.17", + "tokio", + "tracing", +] + [[package]] name = "tauri-plugin-fs" version = "2.4.4" @@ -15533,7 +15910,7 @@ dependencies = [ "osakit", "percent-encoding", "reqwest 0.12.24", - "semver", + "semver 1.0.27", "serde", "serde_json", "tar", @@ -15695,7 +16072,7 @@ dependencies = [ "quote", "regex", "schemars 0.8.22", - "semver", + "semver 1.0.27", "serde", "serde-untagged", "serde_json", @@ -17376,6 +17753,23 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "v8" +version = "134.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21c7a224a7eaf3f98c1bad772fbaee56394dce185ef7b19a2e0ca5e3d274165d" +dependencies = [ + "bindgen 0.70.1", + "bitflags 2.10.0", + "fslock", + "gzip-header", + "home", + "miniz_oxide 0.7.4", + "once_cell", + "paste", + "which 6.0.3", +] + [[package]] name = "vad-ext" version = "0.1.0" @@ -17665,6 +18059,16 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasm_dep_analyzer" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eeee3bdea6257cc36d756fa745a70f9d393571e47d69e0ed97581676a5369ca" +dependencies = [ + "deno_error", + "thiserror 2.0.17", +] + [[package]] name = "wayland-backend" version = "0.3.11" @@ -17901,6 +18305,18 @@ dependencies = [ "rustix 0.38.44", ] +[[package]] +name = "which" +version = "6.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" +dependencies = [ + "either", + "home", + "rustix 0.38.44", + "winsafe", +] + [[package]] name = "whisper" version = "0.1.0" @@ -18705,6 +19121,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + [[package]] name = "wiremock" version = "0.5.22" @@ -18833,6 +19255,15 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x11" version = "2.21.0" diff --git a/Cargo.toml b/Cargo.toml index c343bd8ee6..9469c0f339 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ hypr-db-user = { path = "crates/db-user", package = "db-user" } hypr-detect = { path = "crates/detect", package = "detect" } hypr-docs = { path = "crates/docs", package = "docs" } hypr-download-interface = { path = "crates/download-interface", package = "download-interface" } +hypr-extensions-runtime = { path = "crates/extensions-runtime", package = "extensions-runtime" } hypr-file = { path = "crates/file", package = "file" } hypr-gbnf = { path = "crates/gbnf", package = "gbnf" } hypr-gguf = { path = "crates/gguf", package = "gguf" } @@ -117,6 +118,7 @@ tauri-plugin-db = { path = "plugins/db" } tauri-plugin-db2 = { path = "plugins/db2" } tauri-plugin-deeplink2 = { path = "plugins/deeplink2" } tauri-plugin-detect = { path = "plugins/detect" } +tauri-plugin-extensions = { path = "plugins/extensions" } tauri-plugin-hooks = { path = "plugins/hooks" } tauri-plugin-importer = { path = "plugins/importer" } tauri-plugin-listener = { path = "plugins/listener" } diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 762239d211..f8911d2ff6 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -12,7 +12,9 @@ description = "Hyprnote desktop App" # to make the lib name unique and wouldn't conflict with the bin name. # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 name = "hyprnote_desktop_lib" -crate-type = ["staticlib", "cdylib", "rlib"] +# Note: cdylib removed because V8 (used by deno_core) has TLS relocations incompatible +# with shared libraries on Linux. Mobile support is not needed (macOS/Linux/Windows only). +crate-type = ["staticlib", "rlib"] [build-dependencies] tauri-build = { workspace = true, features = [] } @@ -31,6 +33,7 @@ tauri-plugin-deep-link = { workspace = true } tauri-plugin-deeplink2 = { workspace = true } tauri-plugin-detect = { workspace = true } tauri-plugin-dialog = { workspace = true } +tauri-plugin-extensions = { workspace = true } tauri-plugin-fs = { workspace = true } tauri-plugin-hooks = { workspace = true } tauri-plugin-http = { workspace = true } diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 90b24d9e1b..315be0211d 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -74,6 +74,7 @@ pub async fn main() { .plugin(tauri_plugin_template::init()) .plugin(tauri_plugin_http::init()) .plugin(tauri_plugin_detect::init()) + .plugin(tauri_plugin_extensions::init()) .plugin(tauri_plugin_notification::init()) .plugin(tauri_plugin_clipboard_manager::init()) .plugin(tauri_plugin_tray::init()) diff --git a/apps/desktop/src/components/main/body/extensions/index.tsx b/apps/desktop/src/components/main/body/extensions/index.tsx new file mode 100644 index 0000000000..55731b4a02 --- /dev/null +++ b/apps/desktop/src/components/main/body/extensions/index.tsx @@ -0,0 +1,113 @@ +import { PuzzleIcon, XIcon } from "lucide-react"; +import { Reorder, useDragControls } from "motion/react"; +import type { PointerEvent } from "react"; + +import { Button } from "@hypr/ui/components/ui/button"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuTrigger, +} from "@hypr/ui/components/ui/context-menu"; +import { cn } from "@hypr/utils"; + +import type { Tab } from "../../../../store/zustand/tabs"; +import { StandardTabWrapper } from "../index"; +import { getExtensionComponent } from "./registry"; + +type ExtensionTab = Extract; + +export function TabItemExtension({ + tab, + tabIndex, + handleCloseThis, + handleSelectThis, + handleCloseOthers, + handleCloseAll, +}: { + tab: ExtensionTab; + tabIndex?: number; + handleCloseThis: (tab: Tab) => void; + handleSelectThis: (tab: Tab) => void; + handleCloseOthers: () => void; + handleCloseAll: () => void; +}) { + const controls = useDragControls(); + + return ( + + + handleSelectThis(tab)} + onPointerDown={(e: PointerEvent) => controls.start(e)} + > + + + {tab.extensionId} + + {tabIndex && ( + + {tabIndex} + + )} + + + + + handleCloseThis(tab)}> + Close + + + Close Others + + + Close All + + + ); +} + +export function TabContentExtension({ tab }: { tab: ExtensionTab }) { + const Component = getExtensionComponent(tab.extensionId); + + if (!Component) { + return ( + +
+
+ +

+ Extension not found: {tab.extensionId} +

+
+
+
+ ); + } + + return ( + + + + ); +} diff --git a/apps/desktop/src/components/main/body/extensions/registry.ts b/apps/desktop/src/components/main/body/extensions/registry.ts new file mode 100644 index 0000000000..7166bbde0f --- /dev/null +++ b/apps/desktop/src/components/main/body/extensions/registry.ts @@ -0,0 +1,29 @@ +import type { ComponentType } from "react"; + +import type { ExtensionViewProps } from "../../../../types/extensions"; + +const extensionModules = import.meta.glob<{ + default: ComponentType; +}>("@extensions/*/ui.tsx", { eager: true }); + +export const extensionComponents: Record< + string, + ComponentType +> = {}; + +for (const path in extensionModules) { + const mod = extensionModules[path]; + const parts = path.split("/"); + const extensionId = parts[parts.length - 2]; + extensionComponents[extensionId] = mod.default; +} + +export function getExtensionComponent( + extensionId: string, +): ComponentType | undefined { + return extensionComponents[extensionId]; +} + +export function getAvailableExtensions(): string[] { + return Object.keys(extensionComponents); +} diff --git a/apps/desktop/src/components/main/body/index.tsx b/apps/desktop/src/components/main/body/index.tsx index 85717757a6..f4a17143c3 100644 --- a/apps/desktop/src/components/main/body/index.tsx +++ b/apps/desktop/src/components/main/body/index.tsx @@ -24,6 +24,7 @@ import { TabContentCalendar, TabItemCalendar } from "./calendars"; import { TabContentContact, TabItemContact } from "./contacts"; import { TabContentEmpty, TabItemEmpty } from "./empty"; import { TabContentEvent, TabItemEvent } from "./events"; +import { TabContentExtension, TabItemExtension } from "./extensions"; import { TabContentFolder, TabItemFolder } from "./folders"; import { TabContentHuman, TabItemHuman } from "./humans"; import { Search } from "./search"; @@ -285,6 +286,18 @@ function TabItem({ /> ); } + if (tab.type === "extension") { + return ( + + ); + } return null; } @@ -311,6 +324,9 @@ function ContentWrapper({ tab }: { tab: Tab }) { if (tab.type === "empty") { return ; } + if (tab.type === "extension") { + return ; + } return null; } diff --git a/apps/desktop/src/store/zustand/tabs/schema.ts b/apps/desktop/src/store/zustand/tabs/schema.ts index 597c947b88..97e156ddaf 100644 --- a/apps/desktop/src/store/zustand/tabs/schema.ts +++ b/apps/desktop/src/store/zustand/tabs/schema.ts @@ -72,6 +72,11 @@ export const tabSchema = z.discriminatedUnion("type", [ baseTabSchema.extend({ type: z.literal("empty"), }), + baseTabSchema.extend({ + type: z.literal("extension"), + extensionId: z.string(), + state: z.record(z.string(), z.unknown()).default({}), + }), ]); export type Tab = z.infer; @@ -94,7 +99,8 @@ export type TabInput = | { type: "organizations"; id: string } | { type: "folders"; id: string | null } | { type: "calendars"; month: Date } - | { type: "empty" }; + | { type: "empty" } + | { type: "extension"; extensionId: string; state?: Record }; export const rowIdfromTab = (tab: Tab): string => { switch (tab.type) { @@ -109,6 +115,7 @@ export const rowIdfromTab = (tab: Tab): string => { case "calendars": case "contacts": case "empty": + case "extension": throw new Error("invalid_resource"); case "folders": if (!tab.id) { @@ -136,6 +143,8 @@ export const uniqueIdfromTab = (tab: Tab): string => { return `folders-${tab.id ?? "all"}`; case "empty": return `empty-${tab.slotId}`; + case "extension": + return `extension-${tab.extensionId}`; } }; diff --git a/apps/desktop/src/types/extensions.d.ts b/apps/desktop/src/types/extensions.d.ts new file mode 100644 index 0000000000..8e41d45003 --- /dev/null +++ b/apps/desktop/src/types/extensions.d.ts @@ -0,0 +1,6 @@ +import type { ComponentType } from "react"; + +export interface ExtensionViewProps { + extensionId: string; + state?: Record; +} diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json index a7fc6fbf23..82fd7e3a4b 100644 --- a/apps/desktop/tsconfig.json +++ b/apps/desktop/tsconfig.json @@ -18,7 +18,13 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + + /* Path aliases */ + "baseUrl": ".", + "paths": { + "@extensions/*": ["../../extensions/*"] + } }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] diff --git a/apps/desktop/vite.config.ts b/apps/desktop/vite.config.ts index fa453f3be2..d12852d8c6 100644 --- a/apps/desktop/vite.config.ts +++ b/apps/desktop/vite.config.ts @@ -1,6 +1,7 @@ /// import { tanstackRouter } from "@tanstack/router-plugin/vite"; import react from "@vitejs/plugin-react"; +import path from "node:path"; import { defineConfig, type UserConfig } from "vite"; const host = process.env.TAURI_DEV_HOST; @@ -12,12 +13,14 @@ export default defineConfig(() => ({ react(), ], resolve: { - alias: - process.env.NODE_ENV === "development" + alias: { + ...(process.env.NODE_ENV === "development" ? { "@tauri-apps/plugin-updater": "/src/mocks/updater.ts", } - : {}, + : {}), + "@extensions": path.resolve(__dirname, "../../extensions"), + }, }, test: { reporters: "default", diff --git a/crates/extensions-runtime/Cargo.toml b/crates/extensions-runtime/Cargo.toml new file mode 100644 index 0000000000..fc5f6d1423 --- /dev/null +++ b/crates/extensions-runtime/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "extensions-runtime" +version = "0.1.0" +edition = "2021" + +[dependencies] +deno_core = "0.338" + +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread", "sync"] } +tracing = { workspace = true } diff --git a/crates/extensions-runtime/src/error.rs b/crates/extensions-runtime/src/error.rs new file mode 100644 index 0000000000..7ee7a32cdf --- /dev/null +++ b/crates/extensions-runtime/src/error.rs @@ -0,0 +1,25 @@ +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Extension not found: {0}")] + ExtensionNotFound(String), + + #[error("Invalid manifest: {0}")] + InvalidManifest(String), + + #[error("Runtime error: {0}")] + RuntimeError(String), + + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error("JSON error: {0}")] + Json(#[from] serde_json::Error), + + #[error("Channel send error")] + ChannelSend, + + #[error("Channel receive error")] + ChannelRecv, +} + +pub type Result = std::result::Result; diff --git a/crates/extensions-runtime/src/lib.rs b/crates/extensions-runtime/src/lib.rs new file mode 100644 index 0000000000..c204c59579 --- /dev/null +++ b/crates/extensions-runtime/src/lib.rs @@ -0,0 +1,8 @@ +mod error; +mod manifest; +mod ops; +mod runtime; + +pub use error::*; +pub use manifest::*; +pub use runtime::*; diff --git a/crates/extensions-runtime/src/manifest.rs b/crates/extensions-runtime/src/manifest.rs new file mode 100644 index 0000000000..51d39e3edc --- /dev/null +++ b/crates/extensions-runtime/src/manifest.rs @@ -0,0 +1,56 @@ +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExtensionManifest { + pub id: String, + pub name: String, + pub version: String, + pub description: Option, + pub entry: String, + #[serde(default)] + pub permissions: ExtensionPermissions, +} + +/// Extension permissions declaration. +/// Note: Permissions are currently not enforced. The extension runtime runs with full capabilities. +/// This struct is defined for future use when permission enforcement is implemented. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct ExtensionPermissions { + #[serde(default)] + pub db: Vec, + #[serde(default)] + pub network: Vec, + #[serde(default)] + pub filesystem: Vec, +} + +#[derive(Debug, Clone)] +pub struct Extension { + pub manifest: ExtensionManifest, + pub path: PathBuf, +} + +impl Extension { + pub fn load(path: PathBuf) -> crate::Result { + let manifest_path = path.join("extension.json"); + let manifest_content = std::fs::read_to_string(&manifest_path)?; + let manifest: ExtensionManifest = serde_json::from_str(&manifest_content) + .map_err(|e| crate::Error::InvalidManifest(e.to_string()))?; + + let entry_path = path.join(&manifest.entry); + let canonical_base = path.canonicalize().map_err(|e| crate::Error::Io(e))?; + let canonical_entry = entry_path.canonicalize().map_err(|e| crate::Error::Io(e))?; + if !canonical_entry.starts_with(&canonical_base) { + return Err(crate::Error::InvalidManifest( + "entry path escapes extension directory".to_string(), + )); + } + + Ok(Self { manifest, path }) + } + + pub fn entry_path(&self) -> PathBuf { + self.path.join(&self.manifest.entry) + } +} diff --git a/crates/extensions-runtime/src/ops.rs b/crates/extensions-runtime/src/ops.rs new file mode 100644 index 0000000000..dcde88c690 --- /dev/null +++ b/crates/extensions-runtime/src/ops.rs @@ -0,0 +1,8 @@ +use deno_core::op2; + +#[op2] +#[string] +pub fn op_hypr_log(#[string] message: String) -> String { + tracing::info!(target: "extension", "{}", message); + "ok".to_string() +} diff --git a/crates/extensions-runtime/src/runtime.rs b/crates/extensions-runtime/src/runtime.rs new file mode 100644 index 0000000000..ff8fda2cfe --- /dev/null +++ b/crates/extensions-runtime/src/runtime.rs @@ -0,0 +1,297 @@ +use crate::ops::*; +use crate::{Error, Extension, Result}; +use deno_core::serde_json::Value; +use deno_core::serde_v8; +use deno_core::v8; +use deno_core::JsRuntime; +use deno_core::RuntimeOptions; +use std::collections::HashMap; +use tokio::sync::{mpsc, oneshot}; + +deno_core::extension!(hypr_extension, ops = [op_hypr_log],); + +pub enum RuntimeRequest { + CallFunction { + extension_id: String, + function_name: String, + args: Vec, + responder: oneshot::Sender>, + }, + LoadExtension { + extension: Extension, + responder: oneshot::Sender>, + }, + ExecuteCode { + extension_id: String, + code: String, + responder: oneshot::Sender>, + }, + Shutdown, +} + +#[derive(Clone)] +pub struct ExtensionsRuntime { + sender: mpsc::Sender, +} + +impl ExtensionsRuntime { + pub fn new() -> Self { + let (tx, rx) = mpsc::channel(100); + + std::thread::spawn(move || { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + + rt.block_on(runtime_loop(rx)); + }); + + Self { sender: tx } + } + + pub async fn load_extension(&self, extension: Extension) -> Result<()> { + let (tx, rx) = oneshot::channel(); + self.sender + .send(RuntimeRequest::LoadExtension { + extension, + responder: tx, + }) + .await + .map_err(|_| Error::ChannelSend)?; + + rx.await.map_err(|_| Error::ChannelRecv)? + } + + pub async fn call_function( + &self, + extension_id: &str, + function_name: &str, + args: Vec, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.sender + .send(RuntimeRequest::CallFunction { + extension_id: extension_id.to_string(), + function_name: function_name.to_string(), + args, + responder: tx, + }) + .await + .map_err(|_| Error::ChannelSend)?; + + rx.await.map_err(|_| Error::ChannelRecv)? + } + + pub async fn execute_code(&self, extension_id: &str, code: &str) -> Result { + let (tx, rx) = oneshot::channel(); + self.sender + .send(RuntimeRequest::ExecuteCode { + extension_id: extension_id.to_string(), + code: code.to_string(), + responder: tx, + }) + .await + .map_err(|_| Error::ChannelSend)?; + + rx.await.map_err(|_| Error::ChannelRecv)? + } + + pub async fn shutdown(&self) -> Result<()> { + self.sender + .send(RuntimeRequest::Shutdown) + .await + .map_err(|_| Error::ChannelSend)?; + Ok(()) + } +} + +impl Default for ExtensionsRuntime { + fn default() -> Self { + Self::new() + } +} + +struct ExtensionState { + extension: Extension, + functions: HashMap>, +} + +async fn runtime_loop(mut rx: mpsc::Receiver) { + let mut js_runtime = JsRuntime::new(RuntimeOptions { + extensions: vec![hypr_extension::init_ops()], + ..Default::default() + }); + + js_runtime + .execute_script( + "", + r#" + globalThis.hypr = { + log: (msg) => Deno.core.ops.op_hypr_log(String(msg)), + }; + "#, + ) + .expect("Failed to initialize hypr global"); + + let mut extensions: HashMap = HashMap::new(); + + while let Some(request) = rx.recv().await { + match request { + RuntimeRequest::LoadExtension { + extension, + responder, + } => { + let result = load_extension_impl(&mut js_runtime, extension, &mut extensions); + let _ = responder.send(result); + } + RuntimeRequest::CallFunction { + extension_id, + function_name, + args, + responder, + } => { + let result = call_function_impl( + &mut js_runtime, + &extensions, + &extension_id, + &function_name, + args, + ) + .await; + let _ = responder.send(result); + } + RuntimeRequest::ExecuteCode { + extension_id: _, + code, + responder, + } => { + let result = execute_code_impl(&mut js_runtime, code); + let _ = responder.send(result); + } + RuntimeRequest::Shutdown => { + break; + } + } + } +} + +fn load_extension_impl( + js_runtime: &mut JsRuntime, + extension: Extension, + extensions: &mut HashMap, +) -> Result<()> { + let entry_path = extension.entry_path(); + let code = std::fs::read_to_string(&entry_path)?; + + let wrapper = format!( + r#" + (function() {{ + const __hypr_extension = {{}}; + {} + return __hypr_extension; + }})() + "#, + code + ); + + // deno_core's execute_script requires a 'static script name for stack traces. + // We intentionally promote the extension id to 'static to satisfy this requirement. + // This leaks one small string per extension load, which is acceptable since extensions + // are loaded once and remain for the lifetime of the app. + let script_name: &'static str = Box::leak(extension.manifest.id.clone().into_boxed_str()); + let result = js_runtime + .execute_script(script_name, wrapper) + .map_err(|e| Error::RuntimeError(e.to_string()))?; + + let scope = &mut js_runtime.handle_scope(); + let local = v8::Local::new(scope, result); + + let mut functions = HashMap::new(); + + if let Ok(obj) = v8::Local::::try_from(local) { + if let Some(names) = obj.get_own_property_names(scope, v8::GetPropertyNamesArgs::default()) + { + for i in 0..names.length() { + if let Some(key) = names.get_index(scope, i) { + let key_str = key.to_rust_string_lossy(scope); + if let Some(value) = obj.get(scope, key) { + if let Ok(func) = v8::Local::::try_from(value) { + let global_func = v8::Global::new(scope, func); + functions.insert(key_str, global_func); + } + } + } + } + } + } + + extensions.insert( + extension.manifest.id.clone(), + ExtensionState { + extension: extension.clone(), + functions, + }, + ); + + tracing::info!( + "Loaded extension: {} v{}", + extension.manifest.name, + extension.manifest.version + ); + + Ok(()) +} + +fn execute_code_impl(js_runtime: &mut JsRuntime, code: String) -> Result { + let result = js_runtime + .execute_script("", code) + .map_err(|e| Error::RuntimeError(e.to_string()))?; + + let scope = &mut js_runtime.handle_scope(); + let local = v8::Local::new(scope, result); + let value: Value = + serde_v8::from_v8(scope, local).map_err(|e| Error::RuntimeError(e.to_string()))?; + + Ok(value) +} + +async fn call_function_impl( + js_runtime: &mut JsRuntime, + extensions: &HashMap, + extension_id: &str, + function_name: &str, + args: Vec, +) -> Result { + let ext_state = extensions + .get(extension_id) + .ok_or_else(|| Error::ExtensionNotFound(extension_id.to_string()))?; + + let func = ext_state + .functions + .get(function_name) + .ok_or_else(|| Error::RuntimeError(format!("Function not found: {}", function_name)))?; + + let v8_args = { + let scope = &mut js_runtime.handle_scope(); + let mut result = Vec::with_capacity(args.len()); + for arg in &args { + let v8_val = + serde_v8::to_v8(scope, arg).map_err(|e| Error::RuntimeError(e.to_string()))?; + result.push(v8::Global::new(scope, v8_val)); + } + result + }; + + let result = js_runtime + .call_with_args(func, &v8_args) + .await + .map_err(|e| Error::RuntimeError(e.to_string()))?; + + let scope = &mut js_runtime.handle_scope(); + let local = v8::Local::new(scope, result); + let value: Value = + serde_v8::from_v8(scope, local).map_err(|e| Error::RuntimeError(e.to_string()))?; + + Ok(value) +} diff --git a/extensions/hello-world/extension.json b/extensions/hello-world/extension.json new file mode 100644 index 0000000000..cab3cde6c6 --- /dev/null +++ b/extensions/hello-world/extension.json @@ -0,0 +1,8 @@ +{ + "id": "hello-world", + "name": "Hello World", + "version": "0.1.0", + "description": "A minimal example extension to validate the Deno runtime architecture", + "entry": "main.js", + "permissions": {} +} diff --git a/extensions/hello-world/main.js b/extensions/hello-world/main.js new file mode 100644 index 0000000000..3577692efb --- /dev/null +++ b/extensions/hello-world/main.js @@ -0,0 +1,18 @@ +__hypr_extension.greet = function (name) { + hypr.log(`Hello, ${name}!`); + return `Hello, ${name}!`; +}; + +__hypr_extension.add = function (a, b) { + const result = a + b; + hypr.log(`${a} + ${b} = ${result}`); + return result; +}; + +__hypr_extension.getInfo = function () { + return { + name: "Hello World Extension", + version: "0.1.0", + description: "A minimal example extension", + }; +}; diff --git a/extensions/hello-world/ui.tsx b/extensions/hello-world/ui.tsx new file mode 100644 index 0000000000..a4adaa8dc8 --- /dev/null +++ b/extensions/hello-world/ui.tsx @@ -0,0 +1,62 @@ +import { useState } from "react"; + +import { Button } from "@hypr/ui/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@hypr/ui/components/ui/card"; + +export interface ExtensionViewProps { + extensionId: string; + state?: Record; +} + +export default function HelloWorldExtensionView({ + extensionId, +}: ExtensionViewProps) { + const [greeting, setGreeting] = useState(null); + const [count, setCount] = useState(0); + + const handleGreet = () => { + setGreeting(`Hello from ${extensionId}!`); + }; + + const handleIncrement = () => { + setCount((prev) => prev + 1); + }; + + return ( +
+ + + Hello World Extension + + A minimal example extension using @hypr/ui components + + + +
+ {greeting &&

{greeting}

} +

+ Counter: {count} +

+
+
+ +
+ + +
+
+
+
+ ); +} diff --git a/extensions/package.json b/extensions/package.json new file mode 100644 index 0000000000..7b3ff7b357 --- /dev/null +++ b/extensions/package.json @@ -0,0 +1,13 @@ +{ + "name": "@hypr/extensions", + "version": "0.0.0", + "private": true, + "type": "module", + "devDependencies": { + "@hypr/ui": "workspace:^", + "@hypr/utils": "workspace:^", + "@types/react": "^19.2.7", + "react": "^19.2.0", + "typescript": "~5.6.3" + } +} diff --git a/extensions/tsconfig.json b/extensions/tsconfig.json new file mode 100644 index 0000000000..5f7c6de2d0 --- /dev/null +++ b/extensions/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "baseUrl": ".", + "paths": { + "@hypr/ui/*": ["../packages/ui/src/*"], + "@hypr/utils": ["../packages/utils/src/index.ts"] + } + }, + "include": ["./**/*.ts", "./**/*.tsx"] +} diff --git a/plugins/extensions/Cargo.toml b/plugins/extensions/Cargo.toml new file mode 100644 index 0000000000..735bfb3d5d --- /dev/null +++ b/plugins/extensions/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "tauri-plugin-extensions" +version = "0.1.0" +authors = ["You"] +edition = "2021" +exclude = ["/js", "/node_modules"] +links = "tauri-plugin-extensions" +description = "Tauri plugin for managing Deno-powered extensions" + +[build-dependencies] +tauri-plugin = { workspace = true, features = ["build"] } + +[dev-dependencies] +specta-typescript = { workspace = true } + +[dependencies] +hypr-extensions-runtime = { workspace = true } + +tauri = { workspace = true, features = ["test"] } +tauri-specta = { workspace = true, features = ["derive", "typescript"] } + +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +specta = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } diff --git a/plugins/extensions/build.rs b/plugins/extensions/build.rs new file mode 100644 index 0000000000..dfa4390c81 --- /dev/null +++ b/plugins/extensions/build.rs @@ -0,0 +1,10 @@ +const COMMANDS: &[&str] = &[ + "load_extension", + "call_function", + "execute_code", + "list_extensions", +]; + +fn main() { + tauri_plugin::Builder::new(COMMANDS).build(); +} diff --git a/plugins/extensions/permissions/autogenerated/commands/call_function.toml b/plugins/extensions/permissions/autogenerated/commands/call_function.toml new file mode 100644 index 0000000000..cdb94afc29 --- /dev/null +++ b/plugins/extensions/permissions/autogenerated/commands/call_function.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-call-function" +description = "Enables the call_function command without any pre-configured scope." +commands.allow = ["call_function"] + +[[permission]] +identifier = "deny-call-function" +description = "Denies the call_function command without any pre-configured scope." +commands.deny = ["call_function"] diff --git a/plugins/extensions/permissions/autogenerated/commands/execute_code.toml b/plugins/extensions/permissions/autogenerated/commands/execute_code.toml new file mode 100644 index 0000000000..476f28aba4 --- /dev/null +++ b/plugins/extensions/permissions/autogenerated/commands/execute_code.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-execute-code" +description = "Enables the execute_code command without any pre-configured scope." +commands.allow = ["execute_code"] + +[[permission]] +identifier = "deny-execute-code" +description = "Denies the execute_code command without any pre-configured scope." +commands.deny = ["execute_code"] diff --git a/plugins/extensions/permissions/autogenerated/commands/list_extensions.toml b/plugins/extensions/permissions/autogenerated/commands/list_extensions.toml new file mode 100644 index 0000000000..9ea5898f05 --- /dev/null +++ b/plugins/extensions/permissions/autogenerated/commands/list_extensions.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-list-extensions" +description = "Enables the list_extensions command without any pre-configured scope." +commands.allow = ["list_extensions"] + +[[permission]] +identifier = "deny-list-extensions" +description = "Denies the list_extensions command without any pre-configured scope." +commands.deny = ["list_extensions"] diff --git a/plugins/extensions/permissions/autogenerated/commands/load_extension.toml b/plugins/extensions/permissions/autogenerated/commands/load_extension.toml new file mode 100644 index 0000000000..16264af6a9 --- /dev/null +++ b/plugins/extensions/permissions/autogenerated/commands/load_extension.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-load-extension" +description = "Enables the load_extension command without any pre-configured scope." +commands.allow = ["load_extension"] + +[[permission]] +identifier = "deny-load-extension" +description = "Denies the load_extension command without any pre-configured scope." +commands.deny = ["load_extension"] diff --git a/plugins/extensions/permissions/autogenerated/reference.md b/plugins/extensions/permissions/autogenerated/reference.md new file mode 100644 index 0000000000..c2f1dee8a3 --- /dev/null +++ b/plugins/extensions/permissions/autogenerated/reference.md @@ -0,0 +1,113 @@ +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`extensions:allow-call-function` + + + +Enables the call_function command without any pre-configured scope. + +
+ +`extensions:deny-call-function` + + + +Denies the call_function command without any pre-configured scope. + +
+ +`extensions:allow-execute-code` + + + +Enables the execute_code command without any pre-configured scope. + +
+ +`extensions:deny-execute-code` + + + +Denies the execute_code command without any pre-configured scope. + +
+ +`extensions:allow-list-extensions` + + + +Enables the list_extensions command without any pre-configured scope. + +
+ +`extensions:deny-list-extensions` + + + +Denies the list_extensions command without any pre-configured scope. + +
+ +`extensions:allow-load-extension` + + + +Enables the load_extension command without any pre-configured scope. + +
+ +`extensions:deny-load-extension` + + + +Denies the load_extension command without any pre-configured scope. + +
diff --git a/plugins/extensions/permissions/schemas/schema.json b/plugins/extensions/permissions/schemas/schema.json new file mode 100644 index 0000000000..0483c5d808 --- /dev/null +++ b/plugins/extensions/permissions/schemas/schema.json @@ -0,0 +1,348 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PermissionFile", + "description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.", + "type": "object", + "properties": { + "default": { + "description": "The default permission set for the plugin", + "anyOf": [ + { + "$ref": "#/definitions/DefaultPermission" + }, + { + "type": "null" + } + ] + }, + "set": { + "description": "A list of permissions sets defined", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionSet" + } + }, + "permission": { + "description": "A list of inlined permissions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Permission" + } + } + }, + "definitions": { + "DefaultPermission": { + "description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.", + "type": "object", + "required": [ + "permissions" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionSet": { + "description": "A set of direct permissions grouped together under a new name.", + "type": "object", + "required": [ + "description", + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does.", + "type": "string" + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionKind" + } + } + } + }, + "Permission": { + "description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.", + "type": "object", + "required": [ + "identifier" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri internal convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "commands": { + "description": "Allowed or denied commands when using this permission.", + "default": { + "allow": [], + "deny": [] + }, + "allOf": [ + { + "$ref": "#/definitions/Commands" + } + ] + }, + "scope": { + "description": "Allowed or denied scoped when using this permission.", + "allOf": [ + { + "$ref": "#/definitions/Scopes" + } + ] + }, + "platforms": { + "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "Commands": { + "description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.", + "type": "object", + "properties": { + "allow": { + "description": "Allowed command.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "deny": { + "description": "Denied command, which takes priority.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "Scopes": { + "description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```", + "type": "object", + "properties": { + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "PermissionKind": { + "type": "string", + "oneOf": [ + { + "description": "Enables the call_function command without any pre-configured scope.", + "type": "string", + "const": "allow-call-function", + "markdownDescription": "Enables the call_function command without any pre-configured scope." + }, + { + "description": "Denies the call_function command without any pre-configured scope.", + "type": "string", + "const": "deny-call-function", + "markdownDescription": "Denies the call_function command without any pre-configured scope." + }, + { + "description": "Enables the execute_code command without any pre-configured scope.", + "type": "string", + "const": "allow-execute-code", + "markdownDescription": "Enables the execute_code command without any pre-configured scope." + }, + { + "description": "Denies the execute_code command without any pre-configured scope.", + "type": "string", + "const": "deny-execute-code", + "markdownDescription": "Denies the execute_code command without any pre-configured scope." + }, + { + "description": "Enables the list_extensions command without any pre-configured scope.", + "type": "string", + "const": "allow-list-extensions", + "markdownDescription": "Enables the list_extensions command without any pre-configured scope." + }, + { + "description": "Denies the list_extensions command without any pre-configured scope.", + "type": "string", + "const": "deny-list-extensions", + "markdownDescription": "Denies the list_extensions command without any pre-configured scope." + }, + { + "description": "Enables the load_extension command without any pre-configured scope.", + "type": "string", + "const": "allow-load-extension", + "markdownDescription": "Enables the load_extension command without any pre-configured scope." + }, + { + "description": "Denies the load_extension command without any pre-configured scope.", + "type": "string", + "const": "deny-load-extension", + "markdownDescription": "Denies the load_extension command without any pre-configured scope." + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/extensions/src/commands.rs b/plugins/extensions/src/commands.rs new file mode 100644 index 0000000000..1be47312b0 --- /dev/null +++ b/plugins/extensions/src/commands.rs @@ -0,0 +1,42 @@ +use std::path::PathBuf; + +use crate::{Error, ExtensionsPluginExt}; + +#[tauri::command] +#[specta::specta] +pub async fn load_extension( + app: tauri::AppHandle, + path: String, +) -> Result<(), Error> { + app.load_extension(PathBuf::from(path)).await +} + +#[tauri::command] +#[specta::specta] +pub async fn call_function( + app: tauri::AppHandle, + extension_id: String, + function_name: String, + args_json: String, +) -> Result { + app.call_function(extension_id, function_name, args_json) + .await +} + +#[tauri::command] +#[specta::specta] +pub async fn execute_code( + app: tauri::AppHandle, + extension_id: String, + code: String, +) -> Result { + app.execute_code(extension_id, code).await +} + +#[tauri::command] +#[specta::specta] +pub async fn list_extensions( + _app: tauri::AppHandle, +) -> Result, Error> { + Ok(vec![]) +} diff --git a/plugins/extensions/src/error.rs b/plugins/extensions/src/error.rs new file mode 100644 index 0000000000..07360465ef --- /dev/null +++ b/plugins/extensions/src/error.rs @@ -0,0 +1,31 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, thiserror::Error, Serialize, Deserialize, specta::Type)] +pub enum Error { + #[error("Extension not found: {0}")] + ExtensionNotFound(String), + #[error("Runtime error: {0}")] + RuntimeError(String), + #[error("Invalid manifest: {0}")] + InvalidManifest(String), + #[error("IO error: {0}")] + Io(String), +} + +impl From for Error { + fn from(err: hypr_extensions_runtime::Error) -> Self { + match err { + hypr_extensions_runtime::Error::ExtensionNotFound(id) => Error::ExtensionNotFound(id), + hypr_extensions_runtime::Error::RuntimeError(msg) => Error::RuntimeError(msg), + hypr_extensions_runtime::Error::InvalidManifest(msg) => Error::InvalidManifest(msg), + hypr_extensions_runtime::Error::Io(e) => Error::Io(e.to_string()), + hypr_extensions_runtime::Error::Json(e) => Error::RuntimeError(e.to_string()), + hypr_extensions_runtime::Error::ChannelSend => { + Error::RuntimeError("Channel send error".to_string()) + } + hypr_extensions_runtime::Error::ChannelRecv => { + Error::RuntimeError("Channel receive error".to_string()) + } + } + } +} diff --git a/plugins/extensions/src/ext.rs b/plugins/extensions/src/ext.rs new file mode 100644 index 0000000000..14111420cf --- /dev/null +++ b/plugins/extensions/src/ext.rs @@ -0,0 +1,78 @@ +use std::path::PathBuf; + +use tauri::{Manager, Runtime}; + +use crate::ManagedState; + +pub trait ExtensionsPluginExt { + fn load_extension( + &self, + path: PathBuf, + ) -> impl std::future::Future>; + + fn call_function( + &self, + extension_id: String, + function_name: String, + args_json: String, + ) -> impl std::future::Future>; + + fn execute_code( + &self, + extension_id: String, + code: String, + ) -> impl std::future::Future>; +} + +impl> ExtensionsPluginExt for T { + async fn load_extension(&self, path: PathBuf) -> Result<(), crate::Error> { + let extension = hypr_extensions_runtime::Extension::load(path)?; + + let runtime = { + let state = self.state::(); + let guard = state.lock().await; + guard.runtime.clone() + }; + + runtime.load_extension(extension).await?; + Ok(()) + } + + async fn call_function( + &self, + extension_id: String, + function_name: String, + args_json: String, + ) -> Result { + let args: Vec = serde_json::from_str(&args_json) + .map_err(|e| crate::Error::RuntimeError(e.to_string()))?; + + let runtime = { + let state = self.state::(); + let guard = state.lock().await; + guard.runtime.clone() + }; + + let result = runtime + .call_function(&extension_id, &function_name, args) + .await?; + + serde_json::to_string(&result).map_err(|e| crate::Error::RuntimeError(e.to_string())) + } + + async fn execute_code( + &self, + extension_id: String, + code: String, + ) -> Result { + let runtime = { + let state = self.state::(); + let guard = state.lock().await; + guard.runtime.clone() + }; + + let result = runtime.execute_code(&extension_id, &code).await?; + + serde_json::to_string(&result).map_err(|e| crate::Error::RuntimeError(e.to_string())) + } +} diff --git a/plugins/extensions/src/lib.rs b/plugins/extensions/src/lib.rs new file mode 100644 index 0000000000..a238fcaa27 --- /dev/null +++ b/plugins/extensions/src/lib.rs @@ -0,0 +1,63 @@ +mod commands; +mod error; +mod ext; + +pub use error::*; +pub use ext::*; + +use std::sync::Arc; +use tauri::Manager; +use tokio::sync::Mutex; + +const PLUGIN_NAME: &str = "extensions"; + +pub struct State { + pub runtime: hypr_extensions_runtime::ExtensionsRuntime, +} + +pub type ManagedState = Arc>; + +fn make_specta_builder() -> tauri_specta::Builder { + tauri_specta::Builder::::new() + .plugin_name(PLUGIN_NAME) + .commands(tauri_specta::collect_commands![ + commands::load_extension::, + commands::call_function::, + commands::execute_code::, + commands::list_extensions::, + ]) + .error_handling(tauri_specta::ErrorHandlingMode::Result) +} + +pub fn init() -> tauri::plugin::TauriPlugin { + let specta_builder = make_specta_builder(); + + tauri::plugin::Builder::new(PLUGIN_NAME) + .invoke_handler(specta_builder.invoke_handler()) + .setup(|app, _api| { + let state = State { + runtime: hypr_extensions_runtime::ExtensionsRuntime::new(), + }; + app.manage(Arc::new(Mutex::new(state))); + Ok(()) + }) + .build() +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn export_types() { + make_specta_builder::() + .export( + specta_typescript::Typescript::default() + .header("// @ts-nocheck\n\n") + .formatter(specta_typescript::formatter::prettier) + .bigint(specta_typescript::BigIntExportBehavior::Number), + "./js/bindings.gen.ts", + ) + .unwrap() + } +}