diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-03 18:35:56 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-03 18:35:56 +0300 |
| commit | 03f68b7275f07c98769bf699d2aa14805b921f8d (patch) | |
| tree | 67fa6239de5dbba7abf53118c9495e2e07db37da | |
| parent | 903662fcd822e50d875066ecc20811e3c9945e66 (diff) | |
fix: handle mise-managed updates
| -rw-r--r-- | docs/next/README.md | 12 | ||||
| -rw-r--r-- | docs/next/website/src/content/docs/configuration.mdx | 2 | ||||
| -rw-r--r-- | docs/next/website/src/content/docs/install.mdx | 14 | ||||
| -rw-r--r-- | docs/next/website/src/content/docs/persistence-remote.mdx | 2 | ||||
| -rw-r--r-- | docs/next/website/src/content/docs/session-state.mdx | 2 | ||||
| -rw-r--r-- | src/update.rs | 197 |
6 files changed, 213 insertions, 16 deletions
diff --git a/docs/next/README.md b/docs/next/README.md index 2f436723..828cbed1 100644 --- a/docs/next/README.md +++ b/docs/next/README.md @@ -31,6 +31,12 @@ or install with homebrew: brew install herdr ``` +or install with mise: + +```bash +mise use -g herdr +``` + or download the binary from [releases](https://github.com/ogulcancelik/herdr/releases). requires linux or macos. ## quick start @@ -67,7 +73,7 @@ Herdr notifies you when a new version is available. Run manually: herdr update ``` -`herdr update` is for installs managed by Herdr's own installer. Homebrew and Nix installs update through `brew upgrade herdr` or your Nix workflow, then use the same stop-and-run-again flow if a session is still running the old server. Direct installs can opt into development preview builds with `herdr channel set preview` and return to stable with `herdr channel set stable`. See [install docs](https://herdr.dev/docs/install/) and [session state docs](https://herdr.dev/docs/session-state/) for the full update, restart, restore, and handoff matrix. +`herdr update` is for installs managed by Herdr's own installer. Homebrew, mise, and Nix installs update through `brew upgrade herdr`, `mise upgrade herdr`, or your Nix workflow, then use the same stop-and-run-again flow if a session is still running the old server. Direct installs can opt into development preview builds with `herdr channel set preview` and return to stable with `herdr channel set stable`. See [install docs](https://herdr.dev/docs/install/) and [session state docs](https://herdr.dev/docs/session-state/) for the full update, restart, restore, and handoff matrix. Herdr uses the stable update channel by default. To test preview builds from `master` before the next stable release: @@ -83,7 +89,7 @@ herdr channel set stable For direct installs, changing channels also checks that channel and installs its latest binary. If that update fails, run `herdr update` to retry from the configured channel. -Preview is only for direct installs managed by Herdr's updater. Homebrew and Nix stay on stable and update through their package managers. +Preview is only for direct installs managed by Herdr's updater. Homebrew, mise, and Nix stay on stable and update through their package managers. ## how it compares @@ -239,7 +245,7 @@ In-app settings cover theme, sound, and toast preferences. Herdr writes logs und ## docs - [quick start](https://herdr.dev/docs/quick-start/) — first session, panes, copy, and named sessions -- [install](https://herdr.dev/docs/install/) — install, update, Homebrew, and Nix +- [install](https://herdr.dev/docs/install/) — install, update, Homebrew, mise, and Nix - [session state](https://herdr.dev/docs/session-state/) — detach, restart restore, agent restore, and live handoff - [configuration](https://herdr.dev/docs/configuration/) — keybindings, themes, notifications, environment variables - [integrations](https://herdr.dev/docs/integrations/) — pi, omp, claude code, codex, github copilot cli, opencode, hermes, qodercli integrations diff --git a/docs/next/website/src/content/docs/configuration.mdx b/docs/next/website/src/content/docs/configuration.mdx index f9bf47f1..d4a4c90e 100644 --- a/docs/next/website/src/content/docs/configuration.mdx +++ b/docs/next/website/src/content/docs/configuration.mdx @@ -42,7 +42,7 @@ Direct installs use the stable update channel by default. channel = "stable" ``` -Set `channel = "preview"` to make `herdr update` install preview builds from the current development branch. Homebrew and Nix installs ignore the preview channel and update through their package managers. +Set `channel = "preview"` to make `herdr update` install preview builds from the current development branch. Homebrew, mise, and Nix installs ignore the preview channel and update through their package managers. ## Reload config diff --git a/docs/next/website/src/content/docs/install.mdx b/docs/next/website/src/content/docs/install.mdx index 52b3e056..6322c8e0 100644 --- a/docs/next/website/src/content/docs/install.mdx +++ b/docs/next/website/src/content/docs/install.mdx @@ -23,6 +23,14 @@ If you already use Homebrew: brew install herdr ``` +## Install with mise + +If you already use mise: + +```bash +mise use -g herdr +``` + ## Verify Start Herdr: @@ -41,7 +49,7 @@ Herdr checks for new releases and notifies you in the app. You can update manual herdr update ``` -`herdr update` is for installs managed by Herdr's own installer. Homebrew and Nix installs are updated through those package managers instead. +`herdr update` is for installs managed by Herdr's own installer. Homebrew, mise, and Nix installs are updated through those package managers instead. Herdr uses the stable update channel by default. To opt into preview builds from `master`, set the channel: @@ -57,7 +65,7 @@ herdr channel set stable For direct installs, changing channels also checks that channel and installs its latest binary. If that update fails, run `herdr update` to retry from the configured channel. -Preview builds are GitHub prereleases published from the current development branch. They are useful when you want fixes before the next stable release, but they can regress. Homebrew and Nix installs do not use the preview channel. +Preview builds are GitHub prereleases published from the current development branch. They are useful when you want fixes before the next stable release, but they can regress. Homebrew, mise, and Nix installs do not use the preview channel. By default, `herdr update` installs the new binary and leaves compatible running sessions alone. If an update changes Herdr's client/server protocol, Herdr asks whether to stop the old server after installing. Stop the old server to use the new version. Stopping exits pane processes. For the default session, run `herdr server stop`, then run `herdr` again. For a named session, run `herdr session stop <name>`, then run `herdr session attach <name>` again. @@ -67,7 +75,7 @@ To opt into experimental live server handoff for supported running sessions, run herdr update --handoff ``` -Live handoff does not apply to Homebrew or Nix package-manager updates. For those installs, update with the package manager, then restart that Herdr session when you are ready to use the new server. If a running session still uses the old server, stop it with `herdr server stop` or `herdr session stop <name>`, then run Herdr again. +Live handoff does not apply to Homebrew, mise, or Nix package-manager updates. For those installs, update with the package manager, then restart that Herdr session when you are ready to use the new server. If a running session still uses the old server, stop it with `herdr server stop` or `herdr session stop <name>`, then run Herdr again. ## Install with Nix diff --git a/docs/next/website/src/content/docs/persistence-remote.mdx b/docs/next/website/src/content/docs/persistence-remote.mdx index 7ffffb7f..3e25dd69 100644 --- a/docs/next/website/src/content/docs/persistence-remote.mdx +++ b/docs/next/website/src/content/docs/persistence-remote.mdx @@ -102,7 +102,7 @@ herdr --remote workbox --handoff If you SSH into the server first and run `herdr` there, Herdr runs entirely on the server. That mode is useful and simple, but it cannot access your local desktop clipboard beyond normal terminal text paste. -When your local and remote platforms match, Herdr can copy the current local binary for direct installs. For Homebrew and Nix installs, or when the platforms differ, it downloads the matching release asset for the current client version from `https://herdr.dev/latest.json`. +When your local and remote platforms match, Herdr can copy the current local binary for direct installs. For Homebrew, mise, and Nix installs, or when the platforms differ, it downloads the matching release asset for the current client version from `https://herdr.dev/latest.json`. For local builds or custom binaries, set `HERDR_REMOTE_BINARY` to a local file path before running remote attach. diff --git a/docs/next/website/src/content/docs/session-state.mdx b/docs/next/website/src/content/docs/session-state.mdx index d1568675..c7301730 100644 --- a/docs/next/website/src/content/docs/session-state.mdx +++ b/docs/next/website/src/content/docs/session-state.mdx @@ -86,7 +86,7 @@ herdr --remote workbox --handoff Plain `herdr update` and plain `herdr --remote workbox` use the normal restart/stop flow by default. -`herdr update --handoff` only applies to installs managed by Herdr's own updater. Homebrew and Nix installs are updated through their package managers, so `herdr update` is disabled there and cannot perform live handoff. +`herdr update --handoff` only applies to installs managed by Herdr's own updater. Homebrew, mise, and Nix installs are updated through their package managers, so `herdr update` is disabled there and cannot perform live handoff. ## What Survives diff --git a/src/update.rs b/src/update.rs index 6d1ad49b..c5dab475 100644 --- a/src/update.rs +++ b/src/update.rs @@ -22,7 +22,9 @@ const PREVIEW_UPDATE_MANIFEST_URL: &str = "https://herdr.dev/preview.json"; const HOMEBREW_FORMULA_API_URL: &str = "https://formulae.brew.sh/api/formula/herdr.json"; const HERDR_UPDATE_COMMAND: &str = "herdr update"; const HOMEBREW_UPDATE_COMMAND: &str = "brew update && brew upgrade herdr"; +const MISE_UPDATE_COMMAND: &str = "mise upgrade herdr"; const NIX_UPDATE_COMMAND: &str = "update through Nix"; +const MISE_INSTALLS_DIR_ENV: &str = "MISE_INSTALLS_DIR"; const FAKE_UPDATE_VERSION_ENV: &str = "HERDR_FAKE_UPDATE_VERSION"; const FAKE_UPDATE_NOTES_VERSION_ENV: &str = "HERDR_FAKE_UPDATE_NOTES_VERSION"; const DEFAULT_FAKE_UPDATE_NOTES_VERSION: &str = "0.3.0"; @@ -1624,6 +1626,8 @@ fn print_running_session_update_outcomes( pub(crate) fn update_install_command() -> &'static str { if is_homebrew_managed_install() { HOMEBREW_UPDATE_COMMAND + } else if is_mise_managed_install() { + MISE_UPDATE_COMMAND } else if is_nix_managed_install() { NIX_UPDATE_COMMAND } else { @@ -1639,6 +1643,10 @@ pub(crate) fn update_install_instruction(install_command: &str) -> String { HOMEBREW_UPDATE_COMMAND => { "detach, run `brew update && brew upgrade herdr`, then restart this Herdr session when ready".to_string() } + MISE_UPDATE_COMMAND => { + "detach, run `mise upgrade herdr`, then restart this Herdr session when ready" + .to_string() + } NIX_UPDATE_COMMAND => { "detach, update through Nix, then restart this Herdr session when ready".to_string() } @@ -1662,6 +1670,14 @@ fn is_nix_managed_install() -> bool { is_nix_store_exe_path_following_links(¤t_exe) } +fn is_mise_managed_install() -> bool { + let Ok(current_exe) = env::current_exe() else { + return false; + }; + + is_mise_managed_exe_path_following_links(¤t_exe) +} + pub(crate) fn preview_channel_rejection_for_current_install() -> Option<&'static str> { let Ok(current_exe) = env::current_exe() else { return None; @@ -1674,6 +1690,8 @@ pub(crate) fn package_manager_channel_update_guidance_for_current_install() -> O { if is_homebrew_managed_install() { Some("Use `brew update && brew upgrade herdr` to update Homebrew installs.") + } else if is_mise_managed_install() { + Some("Use `mise upgrade herdr` to update mise installs.") } else if is_nix_managed_install() { Some("Update through Nix to update Nix-managed Herdr installs.") } else { @@ -1686,6 +1704,10 @@ fn preview_channel_rejection_for_exe_path(path: &Path) -> Option<&'static str> { Some( "preview channel is only available for direct Herdr installs; Homebrew installs update through `brew update && brew upgrade herdr`", ) + } else if is_mise_managed_exe_path_following_links(path) { + Some( + "preview channel is only available for direct Herdr installs; mise installs update through `mise upgrade herdr`", + ) } else if is_nix_store_exe_path_following_links(path) { Some("preview channel is only available for direct Herdr installs; Nix installs update through Nix") } else { @@ -1695,6 +1717,7 @@ fn preview_channel_rejection_for_exe_path(path: &Path) -> Option<&'static str> { pub(crate) fn is_package_manager_managed_exe_path(path: &Path) -> bool { is_homebrew_managed_exe_path_following_links(path) + || is_mise_managed_exe_path_following_links(path) || is_nix_store_exe_path_following_links(path) } @@ -1716,10 +1739,80 @@ fn is_nix_store_exe_path_following_links(path: &Path) -> bool { .is_ok_and(|path| is_nix_store_exe_path(&path)) } +fn is_mise_managed_exe_path_following_links(path: &Path) -> bool { + if is_mise_managed_exe_path(path) { + return true; + } + + path.canonicalize() + .is_ok_and(|path| is_mise_managed_exe_path(&path)) +} + fn is_nix_store_exe_path(path: &Path) -> bool { path.starts_with("/nix/store") } +fn is_mise_managed_exe_path(path: &Path) -> bool { + mise_install_root(path).is_some() +} + +fn mise_install_root(path: &Path) -> Option<PathBuf> { + if let Some(root) = mise_install_root_under_configured_installs_dir(path) { + return Some(root); + } + + mise_install_root_under_named_installs_dir(path) +} + +fn mise_install_root_under_configured_installs_dir(path: &Path) -> Option<PathBuf> { + let installs_dir = env::var_os(MISE_INSTALLS_DIR_ENV) + .map(PathBuf::from) + .filter(|path| !path.as_os_str().is_empty())?; + let version_dir = mise_tool_version_dir(path)?; + let tool_dir = version_dir.parent()?; + paths_match(tool_dir.parent()?, &installs_dir).then_some(version_dir.to_path_buf()) +} + +fn mise_install_root_under_named_installs_dir(path: &Path) -> Option<PathBuf> { + let version_dir = mise_tool_version_dir(path)?; + let tool_dir = version_dir.parent()?; + let installs_dir = tool_dir.parent()?; + if installs_dir.file_name()? != "installs" { + return None; + } + Some(version_dir.to_path_buf()) +} + +fn mise_tool_version_dir(path: &Path) -> Option<&Path> { + if path.file_name()? != "herdr" { + return None; + } + let bin_dir = path.parent()?; + if bin_dir.file_name()? != "bin" { + return None; + } + let version_dir = bin_dir.parent()?; + let tool_dir = version_dir.parent()?; + if tool_dir.file_name()? != "herdr" { + return None; + } + Some(version_dir) +} + +fn paths_match(left: &Path, right: &Path) -> bool { + if left == right { + return true; + } + + let Ok(left) = left.canonicalize() else { + return false; + }; + let Ok(right) = right.canonicalize() else { + return false; + }; + left == right +} + fn is_homebrew_managed_exe_path(path: &Path) -> bool { homebrew_cellar_keg_root(path).is_some() } @@ -1762,6 +1855,17 @@ pub fn self_update(options: SelfUpdateOptions) -> Result<Version, String> { )); } + if is_mise_managed_install() { + if channel == UpdateChannel::Preview { + return Err( + "self-update is disabled for mise installs; preview is only available for direct Herdr installs".into(), + ); + } + return Err(format!( + "self-update is disabled for mise installs; run `{MISE_UPDATE_COMMAND}`" + )); + } + if is_nix_managed_install() { if channel == UpdateChannel::Preview { return Err( @@ -1873,6 +1977,11 @@ pub fn auto_update(events: tokio::sync::mpsc::Sender<crate::events::AppEvent>) { return; } + if is_mise_managed_install() && configured_channel == UpdateChannel::Preview { + crate::logging::update_check_failed("preview channel is not available for mise installs"); + return; + } + let nix_managed_install = is_nix_managed_install(); if nix_managed_install && configured_channel == UpdateChannel::Preview { crate::logging::update_check_failed("preview channel is not available for Nix installs"); @@ -1905,15 +2014,9 @@ pub fn auto_update(events: tokio::sync::mpsc::Sender<crate::events::AppEvent>) { ); // Notify the TUI — blocking_send is safe from a std::thread - let install_command = if nix_managed_install { - NIX_UPDATE_COMMAND - } else { - HERDR_UPDATE_COMMAND - }; - let _ = events.blocking_send(crate::events::AppEvent::UpdateReady { version: release.label().to_string(), - install_command: install_command.to_string(), + install_command: update_install_command().to_string(), }); } @@ -2155,6 +2258,58 @@ mod tests { } #[test] + fn mise_install_path_is_detected() { + let path = Path::new("/home/user/.local/share/mise/installs/herdr/0.6.6/bin/herdr"); + + assert!(is_mise_managed_exe_path(path)); + assert_eq!( + mise_install_root(path).unwrap(), + PathBuf::from("/home/user/.local/share/mise/installs/herdr/0.6.6") + ); + } + + #[test] + fn mise_alias_install_path_is_detected() { + let path = Path::new("/home/user/.local/share/mise/installs/herdr/latest/bin/herdr"); + + assert!(is_mise_managed_exe_path(path)); + } + + #[test] + fn mise_custom_installs_dir_path_is_detected() { + let path = Path::new("/opt/mise-tools/installs/herdr/0.6.6/bin/herdr"); + + assert!(is_mise_managed_exe_path(path)); + } + + #[test] + fn mise_configured_installs_dir_path_is_detected() { + let _guard = env_lock().lock().unwrap(); + let previous = std::env::var_os(MISE_INSTALLS_DIR_ENV); + std::env::set_var(MISE_INSTALLS_DIR_ENV, "/opt/mise-tools"); + let path = Path::new("/opt/mise-tools/herdr/0.6.6/bin/herdr"); + + assert!(is_mise_managed_exe_path(path)); + assert_eq!( + mise_install_root(path).unwrap(), + PathBuf::from("/opt/mise-tools/herdr/0.6.6") + ); + + if let Some(previous) = previous { + std::env::set_var(MISE_INSTALLS_DIR_ENV, previous); + } else { + std::env::remove_var(MISE_INSTALLS_DIR_ENV); + } + } + + #[test] + fn non_mise_install_path_is_not_detected() { + let path = Path::new("/home/user/.local/bin/herdr"); + + assert!(!is_mise_managed_exe_path(path)); + } + + #[test] fn package_manager_path_detection_follows_homebrew_symlink() { #[cfg(unix)] { @@ -2178,6 +2333,27 @@ mod tests { } #[test] + fn package_manager_path_detection_follows_mise_symlink() { + #[cfg(unix)] + { + let root = std::env::temp_dir() + .join(format!("herdr-mise-symlink-test-{}", std::process::id())); + let version_bin = root.join("installs/herdr/0.6.2/bin"); + let latest_bin = root.join("installs/herdr/latest/bin"); + fs::create_dir_all(&version_bin).unwrap(); + fs::create_dir_all(&latest_bin).unwrap(); + let version_binary = version_bin.join("herdr"); + let latest_binary = latest_bin.join("herdr"); + fs::write(&version_binary, b"").unwrap(); + std::os::unix::fs::symlink(&version_binary, &latest_binary).unwrap(); + + assert!(is_package_manager_managed_exe_path(&latest_binary)); + + let _ = fs::remove_dir_all(root); + } + } + + #[test] fn nix_store_path_is_detected() { let path = Path::new("/nix/store/abc123-herdr-0.6.1/bin/herdr"); @@ -2188,11 +2364,14 @@ mod tests { #[test] fn preview_channel_is_rejected_for_package_manager_paths() { let homebrew = Path::new("/opt/homebrew/Cellar/herdr/0.6.6/bin/herdr"); + let mise = Path::new("/home/user/.local/share/mise/installs/herdr/0.6.6/bin/herdr"); let nix = Path::new("/nix/store/abc123-herdr-0.6.6/bin/herdr"); let direct = Path::new("/home/user/.local/bin/herdr"); assert!(preview_channel_rejection_for_exe_path(homebrew) .is_some_and(|message| message.contains("Homebrew"))); + assert!(preview_channel_rejection_for_exe_path(mise) + .is_some_and(|message| message.contains("mise"))); assert!(preview_channel_rejection_for_exe_path(nix) .is_some_and(|message| message.contains("Nix"))); assert!(preview_channel_rejection_for_exe_path(direct).is_none()); @@ -2278,6 +2457,10 @@ mod tests { update_install_instruction(HOMEBREW_UPDATE_COMMAND), "detach, run `brew update && brew upgrade herdr`, then restart this Herdr session when ready" ); + assert_eq!( + update_install_instruction(MISE_UPDATE_COMMAND), + "detach, run `mise upgrade herdr`, then restart this Herdr session when ready" + ); } #[test] |
