fix(update): never self-update the desktop app's Engine

Add InstallMethod::DesktopApp for an Engine inside a macOS
`*.app/Contents/MacOS/` bundle or the desktop app's pinned
`<CODEWHALE_HOME>/gpui/engine/` store. It does not support self-update,
so `codewhale update`, target validation and `uninstall` refuse to write
there, and the CLI, /update and the TUI notice say to use
Codewhale > Check for Updates instead of printing GitHub migration help.

Verified on a Linux Boat VM against origin/main plus this series (debug build): cargo build -p codewhale-cli and cargo check --workspace --all-targets --locked clean (0 errors, 0 warnings); cargo test -p codewhale-release 57 passed; codewhale-cli update:: 109 passed. Fixture in a temp HOME: `update` from X/Codewhale.app/Contents/MacOS/codewhale prints the desktop message and exits 1, `uninstall` names the app, bundle bytes unchanged. Not exercised: a real signed desktop bundle on macOS.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
CodeWhale BotandClaude Opus 5.5 committed 2026-10-27 18:50:08 -07:00
1 parent b2eb6f331c
commit e5283cb022
4 files changed
+35 -1

No files matched your search

+3
View File
@@ -317,6 +317,9 @@ fn managed_install_warning(method: InstallMethod) -> Option<String> {
if method.supports_self_update() {
return None;
}
if method == InstallMethod::DesktopApp {
return Some(codewhale_release::install::DESKTOP_APP_UPDATE_HELP.to_string());
}
Some(format!(
"This executable is managed by {label}; in-place self-update is disabled.\n\n\
{GITHUB_MIGRATION_HELP}\n\n\
+19
View File
@@ -41,6 +41,10 @@ Keep the chosen PATH directory in your shell profile after verifying it.
Windows: https://github.com/codewhale-hq/CodeWhale/releases/latest
PATH and migration: https://github.com/codewhale-hq/CodeWhale/blob/main/docs/INSTALL.md"#;
pub const DESKTOP_APP_UPDATE: &str = "Codewhale > Check for Updates";
pub const DESKTOP_APP_UPDATE_HELP: &str = "The Codewhale app updates this copy together with the app. Use Codewhale > Check for Updates. `codewhale update` never writes inside the app.";
/// The package manager (if any) that owns the running executable.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum InstallMethod {
@@ -56,6 +60,7 @@ pub enum InstallMethod {
Scoop,
/// A WinGet portable package under `Microsoft/WinGet/Packages`.
WinGet,
DesktopApp,
/// A release binary the user placed on disk themselves. The default, and
/// the only case where in-place self-update is correct.
Binary,
@@ -130,6 +135,16 @@ impl InstallMethod {
let has = |name: &str| components.iter().any(|c| c == name);
if components
.windows(3)
.any(|parts| parts[0].ends_with(".app") && parts[1] == "contents" && parts[2] == "macos")
|| components
.windows(2)
.any(|pair| pair[0] == "gpui" && pair[1] == "engine")
{
return Self::DesktopApp;
}
// npm is checked first: a `node_modules` install *inside* a Homebrew
// or Termux prefix is still npm's to update.
if has("node_modules") {
@@ -168,6 +183,7 @@ impl InstallMethod {
"omarchy" => Some(Self::Omarchy),
"scoop" => Some(Self::Scoop),
"winget" => Some(Self::WinGet),
"desktop" | "app" => Some(Self::DesktopApp),
"binary" | "release" => Some(Self::Binary),
_ => None,
}
@@ -188,6 +204,7 @@ impl InstallMethod {
Self::Omarchy => "omarchy update",
Self::Scoop => "scoop update codewhale",
Self::WinGet => "winget upgrade --id HunterBown.CodeWhale",
Self::DesktopApp => DESKTOP_APP_UPDATE,
Self::Binary => "codewhale update",
}
}
@@ -205,6 +222,7 @@ impl InstallMethod {
}
Self::Scoop => "scoop uninstall codewhale",
Self::WinGet => "winget uninstall --id HunterBown.CodeWhale",
Self::DesktopApp => "remove the Codewhale app; this Engine is part of it",
Self::Binary => "codewhale uninstall --yes",
}
}
@@ -228,6 +246,7 @@ impl InstallMethod {
Self::Omarchy => "Omarchy",
Self::Scoop => "Scoop",
Self::WinGet => "WinGet",
Self::DesktopApp => "the Codewhale app",
Self::Binary => "release binary",
}
}
@@ -129,6 +129,9 @@ pub(super) fn resolve_updater(
/// Instructions for an install this command must not update in place.
pub(super) fn managed_install_message(method: InstallMethod) -> String {
if method == InstallMethod::DesktopApp {
return codewhale_release::install::DESKTOP_APP_UPDATE_HELP.to_string();
}
format!(
"Codewhale was installed with {label}, which owns this binary.\n\
{migration_help}\n\n\
+10 -1
View File
@@ -1414,6 +1414,13 @@ impl UpdateNotice {
/// Short line for the transient status toast, naming the command that
/// actually updates *this* install.
fn toast_line(&self, install: InstallMethod) -> String {
if install == InstallMethod::DesktopApp {
return format!(
"v{latest} available - use {command}",
latest = self.latest,
command = install.update_command()
);
}
format!(
"v{latest} available - run `{command}` and restart",
latest = self.latest,
@@ -1437,7 +1444,9 @@ impl UpdateNotice {
/// Homebrew or npm owns leaves the manager's metadata lying about what is
/// on disk, and the next upgrade silently reverts the user.
fn notice_block(&self, install: InstallMethod) -> String {
let action = if install.supports_self_update() {
let action = if install == InstallMethod::DesktopApp {
codewhale_release::install::DESKTOP_APP_UPDATE_HELP.to_string()
} else if install.supports_self_update() {
"Run `/update install` here (preview it with a bare `/update`), or `codewhale update` in a shell, then restart Codewhale."
.to_string()
} else {