diff --git a/.github/workflows/extensions_cd.yaml b/.github/workflows/extensions_cd.yaml index fe841a97b4..d53f65350b 100644 --- a/.github/workflows/extensions_cd.yaml +++ b/.github/workflows/extensions_cd.yaml @@ -1,10 +1,19 @@ on: workflow_dispatch: + inputs: + publish: + description: "Publish extensions to R2" + required: false + type: boolean + default: false concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + EXTENSIONS_BUCKET: hyprnote-extensions + jobs: build: runs-on: depot-ubuntu-24.04-8 @@ -13,10 +22,93 @@ jobs: - uses: denoland/setup-deno@v2 with: deno-version: v2.x - - working-directory: ./extensions + - name: Build extensions + working-directory: ./extensions run: deno task build + + - name: Package extensions + run: | + mkdir -p dist/extensions + for dir in extensions/*/; do + if [ -f "$dir/extension.json" ] && [ -d "$dir/dist" ]; then + name=$(basename "$dir") + version=$(jq -r '.version' "$dir/extension.json") + + # Create a clean package directory + mkdir -p "dist/packages/$name" + cp "$dir/extension.json" "dist/packages/$name/" + cp "$dir/main.js" "dist/packages/$name/" 2>/dev/null || true + cp -r "$dir/dist" "dist/packages/$name/" + + # Create zip archive + (cd "dist/packages" && zip -r "../extensions/$name-$version.zip" "$name") + + # Calculate checksum + sha256sum "dist/extensions/$name-$version.zip" | cut -d' ' -f1 > "dist/extensions/$name-$version.zip.sha256" + + echo "Packaged: $name v$version" + fi + done + + - name: Generate registry + run: | + echo '{"version":1,"extensions":[' > dist/extensions/registry.json + first=true + for dir in extensions/*/; do + if [ -f "$dir/extension.json" ] && [ -d "$dir/dist" ]; then + name=$(basename "$dir") + version=$(jq -r '.version' "$dir/extension.json") + ext_name=$(jq -r '.name' "$dir/extension.json") + description=$(jq -r '.description // ""' "$dir/extension.json") + api_version=$(jq -r '.api_version // "0.1"' "$dir/extension.json") + checksum=$(cat "dist/extensions/$name-$version.zip.sha256") + size=$(stat -c%s "dist/extensions/$name-$version.zip") + + if [ "$first" = true ]; then + first=false + else + echo ',' >> dist/extensions/registry.json + fi + + jq -n \ + --arg id "$name" \ + --arg name "$ext_name" \ + --arg version "$version" \ + --arg api_version "$api_version" \ + --arg description "$description" \ + --arg download_url "https://pub-hyprnote.r2.dev/extensions/$name-$version.zip" \ + --arg checksum "$checksum" \ + --argjson size "$size" \ + '{id: $id, name: $name, version: $version, api_version: $api_version, description: $description, download_url: $download_url, checksum: $checksum, size: $size}' \ + >> dist/extensions/registry.json + fi + done + echo ']}' >> dist/extensions/registry.json + cat dist/extensions/registry.json + - uses: actions/upload-artifact@v4 with: name: extensions - path: extensions/*/dist/ + path: dist/extensions/ retention-days: 7 + + - name: Upload to R2 + if: ${{ inputs.publish }} + run: | + # Upload extension packages + for file in dist/extensions/*.zip; do + aws s3 cp "$file" "s3://${{ env.EXTENSIONS_BUCKET }}/extensions/$(basename "$file")" \ + --endpoint-url ${{ secrets.CLOUDFLARE_R2_ENDPOINT_URL }} \ + --region auto + done + + # Upload registry + aws s3 cp dist/extensions/registry.json "s3://${{ env.EXTENSIONS_BUCKET }}/extensions/registry.json" \ + --endpoint-url ${{ secrets.CLOUDFLARE_R2_ENDPOINT_URL }} \ + --region auto \ + --content-type "application/json" + + echo "Published extensions to R2" + env: + AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }} diff --git a/Cargo.lock b/Cargo.lock index 6dab1900a0..6a72485e97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2310,6 +2310,25 @@ dependencies = [ "bytes", ] +[[package]] +name = "bzip2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" +dependencies = [ + "bzip2-sys", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "cached" version = "0.55.1" @@ -3119,6 +3138,12 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "content_inspector" version = "0.2.4" @@ -3960,6 +3985,12 @@ dependencies = [ "byteorder", ] +[[package]] +name = "deflate64" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" + [[package]] name = "deno_core" version = "0.338.0" @@ -9328,6 +9359,27 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "mac" version = "0.1.1" @@ -11407,6 +11459,16 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "peeking_take_while" version = "0.1.2" @@ -15822,8 +15884,11 @@ name = "tauri-plugin-extensions" version = "0.1.0" dependencies = [ "extensions-runtime", + "hex", + "reqwest 0.12.24", "serde", "serde_json", + "sha2", "specta", "specta-typescript", "tauri", @@ -15832,6 +15897,7 @@ dependencies = [ "thiserror 2.0.17", "tokio", "tracing", + "zip 2.4.2", ] [[package]] @@ -19942,6 +20008,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4de5f056fb9dc8b7908754867544e26145767187aaac5a98495e88ad7cb8a80f" +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] + [[package]] name = "yaml-rust" version = "0.4.5" @@ -20237,6 +20312,36 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "deflate64", + "displaydoc", + "flate2", + "getrandom 0.3.4", + "hmac", + "indexmap 2.12.0", + "lzma-rs", + "memchr", + "pbkdf2", + "sha1", + "thiserror 2.0.17", + "time", + "xz2", + "zeroize", + "zopfli", + "zstd", +] + [[package]] name = "zip" version = "4.6.1" @@ -20249,6 +20354,46 @@ dependencies = [ "memchr", ] +[[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", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "zune-core" version = "0.4.12" diff --git a/apps/desktop/src/components/main/body/extensions/index.tsx b/apps/desktop/src/components/main/body/extensions/index.tsx index d801a4ab5b..f4b4fc58cb 100644 --- a/apps/desktop/src/components/main/body/extensions/index.tsx +++ b/apps/desktop/src/components/main/body/extensions/index.tsx @@ -1,5 +1,11 @@ import { convertFileSrc } from "@tauri-apps/api/core"; -import { AlertTriangleIcon, BlocksIcon, PuzzleIcon, XIcon } from "lucide-react"; +import { + AlertTriangleIcon, + BlocksIcon, + PuzzleIcon, + StoreIcon, + XIcon, +} from "lucide-react"; import { Reorder, useDragControls } from "motion/react"; import { Component, @@ -26,6 +32,7 @@ import { ResizablePanel, ResizablePanelGroup, } from "@hypr/ui/components/ui/resizable"; +import { Tabs, TabsList, TabsTrigger } from "@hypr/ui/components/ui/tabs"; import { cn } from "@hypr/utils"; import { createIframeSynchronizer } from "../../../../store/tinybase/iframe-sync"; @@ -36,6 +43,7 @@ import { type TabItem, TabItemBase } from "../shared"; import { ExtensionDetailsColumn } from "./details"; import { ExtensionsListColumn } from "./list"; import { getPanelInfoByExtensionId } from "./registry"; +import { ExtensionStoreColumn } from "./store"; type ExtensionTab = Extract; type ExtensionsTab = Extract; @@ -74,6 +82,9 @@ function ExtensionsView({ tab }: { tab: ExtensionsTab }) { const updateExtensionsTabState = useTabs( (state) => state.updateExtensionsTabState, ); + const [activeTab, setActiveTab] = useState<"installed" | "store">( + "installed", + ); const { selectedExtension } = tab.state; @@ -88,18 +99,46 @@ function ExtensionsView({ tab }: { tab: ExtensionsTab }) { ); return ( - - - - - - - - - +
+
+ setActiveTab(v as "installed" | "store")} + > + + + + Installed + + + + Store + + + +
+
+ + + {activeTab === "installed" ? ( + + ) : ( + + )} + + + + + + +
+
); } diff --git a/apps/desktop/src/components/main/body/extensions/store.tsx b/apps/desktop/src/components/main/body/extensions/store.tsx new file mode 100644 index 0000000000..143610a000 --- /dev/null +++ b/apps/desktop/src/components/main/body/extensions/store.tsx @@ -0,0 +1,210 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { Check, Download, Loader2, Store, Trash2 } from "lucide-react"; +import { useMemo, useState } from "react"; + +import { commands, type RegistryEntry } from "@hypr/plugin-extensions"; +import { Button } from "@hypr/ui/components/ui/button"; +import { cn } from "@hypr/utils"; + +import { listInstalledExtensions } from "./registry"; + +export function ExtensionStoreColumn({ + selectedExtension, + setSelectedExtension, +}: { + selectedExtension: string | null; + setSelectedExtension: (id: string | null) => void; +}) { + const queryClient = useQueryClient(); + + const { data: registry, isLoading: isLoadingRegistry } = useQuery({ + queryKey: ["extensions", "registry"], + queryFn: async () => { + const result = await commands.fetchRegistry(); + if (result.status === "ok") { + return result.data; + } + throw new Error(result.error as unknown as string); + }, + }); + + const { data: installedExtensions = [] } = useQuery({ + queryKey: ["extensions", "list"], + queryFn: listInstalledExtensions, + }); + + const installedIds = useMemo( + () => new Set(installedExtensions.map((ext) => ext.id)), + [installedExtensions], + ); + + const installMutation = useMutation({ + mutationFn: async (entry: RegistryEntry) => { + const result = await commands.downloadExtension( + entry.id, + entry.download_url, + entry.checksum, + ); + if (result.status === "error") { + throw new Error(result.error as unknown as string); + } + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["extensions", "list"] }); + }, + }); + + const uninstallMutation = useMutation({ + mutationFn: async (extensionId: string) => { + const result = await commands.uninstallExtension(extensionId); + if (result.status === "error") { + throw new Error(result.error as unknown as string); + } + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["extensions", "list"] }); + }, + }); + + if (isLoadingRegistry) { + return ( +
+ +
+ ); + } + + if (!registry) { + return ( +
+
+ +

+ Unable to load extension store +

+
+
+ ); + } + + return ( +
+
+
+

Available Extensions

+
+
+ +
+
+ {registry.extensions.length === 0 ? ( +
+ +

No extensions available

+
+ ) : ( + registry.extensions.map((entry) => ( + setSelectedExtension(entry.id)} + onInstall={() => installMutation.mutate(entry)} + onUninstall={() => uninstallMutation.mutate(entry.id)} + /> + )) + )} +
+
+
+ ); +} + +function StoreExtensionItem({ + entry, + isInstalled, + isSelected, + isInstalling, + isUninstalling, + onClick, + onInstall, + onUninstall, +}: { + entry: RegistryEntry; + isInstalled: boolean; + isSelected: boolean; + isInstalling: boolean; + isUninstalling: boolean; + onClick: () => void; + onInstall: () => void; + onUninstall: () => void; +}) { + const [isHovered, setIsHovered] = useState(false); + + const handleAction = (e: React.MouseEvent) => { + e.stopPropagation(); + if (isInstalled) { + onUninstall(); + } else { + onInstall(); + } + }; + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + className={cn([ + "w-full text-left px-3 py-2 rounded-md text-sm border hover:bg-neutral-100 transition-colors cursor-pointer", + isSelected ? "border-neutral-500 bg-neutral-100" : "border-transparent", + ])} + > +
+ +
+
+ {entry.name} + + v{entry.version} + + {isInstalled && ( + + )} +
+ {entry.description && ( +
+ {entry.description} +
+ )} +
+ {(isHovered || isInstalling || isUninstalling) && ( + + )} +
+
+ ); +} diff --git a/plugins/extensions/Cargo.toml b/plugins/extensions/Cargo.toml index 735bfb3d5d..fd8e4701f9 100644 --- a/plugins/extensions/Cargo.toml +++ b/plugins/extensions/Cargo.toml @@ -19,9 +19,13 @@ hypr-extensions-runtime = { workspace = true } tauri = { workspace = true, features = ["test"] } tauri-specta = { workspace = true, features = ["derive", "typescript"] } +hex = "0.4" +reqwest = { workspace = true, features = ["json"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } +sha2 = "0.10" specta = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } +zip = "2.2" diff --git a/plugins/extensions/js/bindings.gen.ts b/plugins/extensions/js/bindings.gen.ts index e88c3be286..440c00c012 100644 --- a/plugins/extensions/js/bindings.gen.ts +++ b/plugins/extensions/js/bindings.gen.ts @@ -54,6 +54,30 @@ async getExtension(extensionId: string) : Promise> if(e instanceof Error) throw e; else return { status: "error", error: e as any }; } +}, +async fetchRegistry() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("plugin:extensions|fetch_registry") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async downloadExtension(extensionId: string, downloadUrl: string, expectedChecksum: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("plugin:extensions|download_extension", { extensionId, downloadUrl, expectedChecksum }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async uninstallExtension(extensionId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("plugin:extensions|uninstall_extension", { extensionId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} } } @@ -67,9 +91,11 @@ async getExtension(extensionId: string) : Promise> /** user-defined types **/ -export type Error = { ExtensionNotFound: string } | { RuntimeError: string } | { InvalidManifest: string } | { Io: string } +export type Error = { ExtensionNotFound: string } | { RuntimeError: string } | { InvalidManifest: string } | { Io: string } | "RuntimeUnavailable" | { Network: string } | { ChecksumMismatch: { expected: string; actual: string } } | { ZipError: string } export type ExtensionInfo = { id: string; name: string; version: string; api_version: string; description: string | null; path: string; panels: PanelInfo[] } export type PanelInfo = { id: string; title: string; entry: string; entry_path: string | null; styles_path: string | null } +export type RegistryEntry = { id: string; name: string; version: string; api_version: string; description: string; download_url: string; checksum: string; size: number } +export type RegistryResponse = { version: number; extensions: RegistryEntry[] } /** tauri-specta globals **/ diff --git a/plugins/extensions/src/commands.rs b/plugins/extensions/src/commands.rs index a7efcec6b3..86a355fc9e 100644 --- a/plugins/extensions/src/commands.rs +++ b/plugins/extensions/src/commands.rs @@ -1,8 +1,10 @@ +use std::io::{Cursor, Read, Write}; use std::path::PathBuf; +use sha2::{Digest, Sha256}; use tauri::Manager; -use crate::{Error, ExtensionInfo, ExtensionsPluginExt, PanelInfo}; +use crate::{Error, ExtensionInfo, ExtensionsPluginExt, PanelInfo, RegistryResponse}; #[tauri::command] #[specta::specta] @@ -142,3 +144,131 @@ pub async fn get_extension( }) .ok_or(Error::ExtensionNotFound(extension_id)) } + +const REGISTRY_URL: &str = "https://pub-hyprnote.r2.dev/extensions/registry.json"; + +#[tauri::command] +#[specta::specta] +pub async fn fetch_registry() -> Result { + let response = reqwest::get(REGISTRY_URL) + .await + .map_err(|e| Error::Network(e.to_string()))?; + + let registry: RegistryResponse = response + .json() + .await + .map_err(|e| Error::Network(e.to_string()))?; + + Ok(registry) +} + +#[tauri::command] +#[specta::specta] +pub async fn download_extension( + app: tauri::AppHandle, + extension_id: String, + download_url: String, + expected_checksum: String, +) -> Result<(), Error> { + let extensions_dir = app + .path() + .app_data_dir() + .map_err(|e| Error::Io(e.to_string()))? + .join("extensions"); + + if !extensions_dir.exists() { + std::fs::create_dir_all(&extensions_dir).map_err(|e| Error::Io(e.to_string()))?; + } + + let response = reqwest::get(&download_url) + .await + .map_err(|e| Error::Network(e.to_string()))?; + + let bytes = response + .bytes() + .await + .map_err(|e| Error::Network(e.to_string()))?; + + let mut hasher = Sha256::new(); + hasher.update(&bytes); + let actual_checksum = hex::encode(hasher.finalize()); + + if actual_checksum != expected_checksum { + return Err(Error::ChecksumMismatch { + expected: expected_checksum, + actual: actual_checksum, + }); + } + + let cursor = Cursor::new(bytes); + let mut archive = zip::ZipArchive::new(cursor).map_err(|e| Error::ZipError(e.to_string()))?; + + let target_dir = extensions_dir.join(&extension_id); + if target_dir.exists() { + std::fs::remove_dir_all(&target_dir).map_err(|e| Error::Io(e.to_string()))?; + } + std::fs::create_dir_all(&target_dir).map_err(|e| Error::Io(e.to_string()))?; + + for i in 0..archive.len() { + let mut file = archive + .by_index(i) + .map_err(|e| Error::ZipError(e.to_string()))?; + + let outpath = match file.enclosed_name() { + Some(path) => { + let components: Vec<_> = path.components().collect(); + if components.len() > 1 { + target_dir.join(components[1..].iter().collect::()) + } else { + continue; + } + } + None => continue, + }; + + if file.is_dir() { + std::fs::create_dir_all(&outpath).map_err(|e| Error::Io(e.to_string()))?; + } else { + if let Some(parent) = outpath.parent() { + if !parent.exists() { + std::fs::create_dir_all(parent).map_err(|e| Error::Io(e.to_string()))?; + } + } + let mut outfile = + std::fs::File::create(&outpath).map_err(|e| Error::Io(e.to_string()))?; + let mut buffer = Vec::new(); + file.read_to_end(&mut buffer) + .map_err(|e| Error::Io(e.to_string()))?; + outfile + .write_all(&buffer) + .map_err(|e| Error::Io(e.to_string()))?; + } + } + + tracing::info!("Installed extension: {}", extension_id); + Ok(()) +} + +#[tauri::command] +#[specta::specta] +pub async fn uninstall_extension( + app: tauri::AppHandle, + extension_id: String, +) -> Result<(), Error> { + let extensions_dir = app + .path() + .app_data_dir() + .map_err(|e| Error::Io(e.to_string()))? + .join("extensions"); + + let extension_dir = extensions_dir.join(&extension_id); + + if !extension_dir.exists() { + return Err(Error::ExtensionNotFound(extension_id)); + } + + std::fs::remove_dir_all(&extension_dir).map_err(|e| Error::Io(e.to_string()))?; + + tracing::info!("Uninstalled extension: {}", extension_id); + Ok(()) +} diff --git a/plugins/extensions/src/error.rs b/plugins/extensions/src/error.rs index 11dd05d512..b74e1d48f7 100644 --- a/plugins/extensions/src/error.rs +++ b/plugins/extensions/src/error.rs @@ -12,6 +12,12 @@ pub enum Error { Io(String), #[error("Runtime unavailable: V8 engine failed to initialize")] RuntimeUnavailable, + #[error("Network error: {0}")] + Network(String), + #[error("Checksum mismatch: expected {expected}, got {actual}")] + ChecksumMismatch { expected: String, actual: String }, + #[error("Zip extraction error: {0}")] + ZipError(String), } impl From for Error { diff --git a/plugins/extensions/src/lib.rs b/plugins/extensions/src/lib.rs index 3233008a44..5b6cfb0ad8 100644 --- a/plugins/extensions/src/lib.rs +++ b/plugins/extensions/src/lib.rs @@ -32,6 +32,24 @@ pub struct PanelInfo { pub styles_path: Option, } +#[derive(Debug, Clone, Serialize, Deserialize, specta::Type)] +pub struct RegistryResponse { + pub version: u32, + pub extensions: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, specta::Type)] +pub struct RegistryEntry { + pub id: String, + pub name: String, + pub version: String, + pub api_version: String, + pub description: String, + pub download_url: String, + pub checksum: String, + pub size: u64, +} + pub struct State { pub runtime: hypr_extensions_runtime::ExtensionsRuntime, } @@ -48,6 +66,9 @@ fn make_specta_builder() -> tauri_specta::Builder { commands::list_extensions::, commands::get_extensions_dir::, commands::get_extension::, + commands::fetch_registry, + commands::download_extension::, + commands::uninstall_extension::, ]) .error_handling(tauri_specta::ErrorHandlingMode::Result) }