diff --git a/Cargo.lock b/Cargo.lock index 10c865d9..04a26590 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2611,7 +2611,7 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "mcpmux" -version = "0.2.0" +version = "0.2.2" dependencies = [ "anyhow", "async-trait", @@ -2635,6 +2635,7 @@ dependencies = [ "tauri-plugin-deep-link", "tauri-plugin-dialog", "tauri-plugin-opener", + "tauri-plugin-process", "tauri-plugin-single-instance", "tauri-plugin-updater", "tokio", @@ -2649,7 +2650,7 @@ dependencies = [ [[package]] name = "mcpmux-core" -version = "0.2.0" +version = "0.2.2" dependencies = [ "anyhow", "async-trait", @@ -2674,7 +2675,7 @@ dependencies = [ [[package]] name = "mcpmux-gateway" -version = "0.2.0" +version = "0.2.2" dependencies = [ "anyhow", "async-stream", @@ -2714,7 +2715,7 @@ dependencies = [ [[package]] name = "mcpmux-mcp" -version = "0.2.0" +version = "0.2.2" dependencies = [ "anyhow", "async-trait", @@ -2733,7 +2734,7 @@ dependencies = [ [[package]] name = "mcpmux-storage" -version = "0.2.0" +version = "0.2.2" dependencies = [ "anyhow", "async-trait", @@ -5307,6 +5308,16 @@ dependencies = [ "zbus", ] +[[package]] +name = "tauri-plugin-process" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55511a7bf6cd70c8767b02c97bf8134fa434daf3926cfc1be0a0f94132d165a" +dependencies = [ + "tauri", + "tauri-plugin", +] + [[package]] name = "tauri-plugin-single-instance" version = "2.3.7" diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index d1834969..3d86555c 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -22,6 +22,7 @@ tauri-plugin-deep-link = "2" tauri-plugin-updater = "2" tauri-plugin-autostart = "2" tauri-plugin-dialog = "2" +tauri-plugin-process = "2" image = { version = "0.25", default-features = false, features = ["png"] } serde.workspace = true serde_json.workspace = true diff --git a/apps/desktop/src-tauri/capabilities/default.json b/apps/desktop/src-tauri/capabilities/default.json index 54a377ca..d5621f46 100644 --- a/apps/desktop/src-tauri/capabilities/default.json +++ b/apps/desktop/src-tauri/capabilities/default.json @@ -17,6 +17,7 @@ "opener:default", "deep-link:default", "updater:default", + "process:allow-restart", "dialog:default" ] } diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 008ee446..ad5b178b 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -122,12 +122,48 @@ fn init_tracing() -> tracing_appender::non_blocking::WorkerGuard { guard } -/// Get app version +/// Get app version (compiled into the binary) #[tauri::command] fn get_version() -> String { env!("CARGO_PKG_VERSION").to_string() } +/// Get the on-disk bundle version (macOS only). +/// +/// After a Homebrew Cask upgrade, the `.app` bundle on disk has the new version +/// but the running process still has the old compiled-in version. Comparing +/// `get_version()` with `get_bundle_version()` lets the frontend detect this +/// mismatch and prompt the user to restart. +#[tauri::command] +fn get_bundle_version() -> Option { + #[cfg(target_os = "macos")] + { + // Read CFBundleShortVersionString from the running app's Info.plist + let exe = std::env::current_exe().ok()?; + // exe is typically: Foo.app/Contents/MacOS/Foo + let contents_dir = exe.parent()?.parent()?; + let plist_path = contents_dir.join("Info.plist"); + let plist = std::fs::read_to_string(&plist_path).ok()?; + + // Simple extraction — avoids adding a plist parsing dependency. + // Looks for CFBundleShortVersionString\nX.Y.Z + let key = "CFBundleShortVersionString"; + let key_pos = plist.find(key)?; + let after_key = &plist[key_pos + key.len()..]; + let string_start = after_key.find("")? + "".len(); + let string_end = after_key[string_start..].find("")?; + Some( + after_key[string_start..string_start + string_end] + .trim() + .to_string(), + ) + } + #[cfg(not(target_os = "macos"))] + { + None + } +} + /// Get the path to the logs directory #[tauri::command] fn get_logs_path() -> String { @@ -195,6 +231,7 @@ pub fn run() { Some(vec!["--hidden"]), // Start minimized to tray )) .plugin(tauri_plugin_updater::Builder::new().build()) + .plugin(tauri_plugin_process::init()) .plugin(tauri_plugin_single_instance::init(|app, args, cwd| { // This callback is called when a second instance is launched info!("Second instance detected, focusing existing window"); @@ -691,6 +728,7 @@ pub fn run() { }) .invoke_handler(tauri::generate_handler![ get_version, + get_bundle_version, // Space commands commands::list_spaces, commands::get_space, diff --git a/apps/desktop/src/features/settings/UpdateChecker.tsx b/apps/desktop/src/features/settings/UpdateChecker.tsx index 6b9f2b94..dbf068f9 100644 --- a/apps/desktop/src/features/settings/UpdateChecker.tsx +++ b/apps/desktop/src/features/settings/UpdateChecker.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { check, Update } from '@tauri-apps/plugin-updater'; import { relaunch } from '@tauri-apps/plugin-process'; import { @@ -9,7 +9,7 @@ import { CardDescription, CardContent, } from '@mcpmux/ui'; -import { Download, Loader2, CheckCircle, AlertCircle, RefreshCw } from 'lucide-react'; +import { Download, Loader2, CheckCircle, AlertCircle, RefreshCw, RotateCcw } from 'lucide-react'; import { invoke } from '@tauri-apps/api/core'; interface DownloadEvent { @@ -27,6 +27,7 @@ export function UpdateChecker() { const [downloadProgress, setDownloadProgress] = useState({ downloaded: 0, total: 0 }); const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); const [currentVersion, setCurrentVersion] = useState(''); + const [bundleVersionMismatch, setBundleVersionMismatch] = useState(null); // Load current version on mount useState(() => { @@ -35,6 +36,23 @@ export function UpdateChecker() { .catch((err) => console.error('Failed to get version:', err)); }); + // Check if the on-disk bundle version differs from the running version (Homebrew Cask upgrades) + useEffect(() => { + if (!currentVersion) return; + invoke('get_bundle_version') + .then((bundleVersion) => { + if (bundleVersion && bundleVersion !== currentVersion) { + console.log( + `[Updater] Bundle version mismatch: running=${currentVersion}, on-disk=${bundleVersion}` + ); + setBundleVersionMismatch(bundleVersion); + } + }) + .catch(() => { + // Expected to return null on non-macOS platforms + }); + }, [currentVersion]); + const checkForUpdates = async () => { setChecking(true); setMessage(null); @@ -149,8 +167,32 @@ export function UpdateChecker() {

+ {/* Bundle version mismatch (e.g., after brew upgrade) */} + {bundleVersionMismatch && ( +
+
+

Restart Required

+

+ Version v{bundleVersionMismatch} has been installed on disk, but you are still + running v{currentVersion}. Restart to apply the update. +

+
+ +
+ )} + {/* Check Button */} - {!updateInfo && ( + {!updateInfo && !bundleVersionMismatch && (