Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[package]
name = "lite"
version = "0.0.95"
version = "0.0.96"
description = "A fast, local workspace for AI coding agents."
authors = ["Ultralytics"]
edition = "2024"
Expand Down
64 changes: 64 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7968,8 +7968,72 @@ async fn check_update(app: AppHandle) -> Result<Option<ReleaseInfo>, String> {
.map_err(|error| error.to_string())
}

// macOS runs a downloaded app that Finder never moved from a read-only copy, which no update can replace.
// Lite copies itself where dragging it would have put it and lifts the download mark, so the copy opens in
// place, as apps that offer to move themselves to Applications do. A Lite already there is opened instead,
// so nothing the user has is replaced; it can update itself.
#[cfg(target_os = "macos")]
fn move_to_applications(bundle: &Path) -> Result<PathBuf, String> {
let name = bundle.file_name().ok_or("Could not read Lite's location")?;
let unquarantine = |target: &Path| {
Command::new("/usr/bin/xattr")
.args(["-dr", "com.apple.quarantine"])
.arg(target)
.status()
.is_ok_and(|status| status.success())
};
let mut folders = vec![PathBuf::from("/Applications")];
folders.extend(std::env::home_dir().map(|home| home.join("Applications")));
for folder in folders {
let target = folder.join(name);
if target.exists() {
unquarantine(&target);
return Ok(target);
}
let _ = fs::create_dir_all(&folder);
if Command::new("/usr/bin/ditto")
.arg(bundle)
.arg(&target)
.status()
.is_ok_and(|status| status.success())
&& unquarantine(&target)
{
return Ok(target);
}
let _ = fs::remove_dir_all(&target);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 MEDIUM: If removing the existing app succeeds but the following rename fails, this deletes the working installation; the error path then removes the staged copy and continues. Preserve the old bundle until the replacement succeeds, for example by renaming it to a backup and restoring it if the staged rename fails.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in 5d54c6a: an existing Lite.app is never removed or replaced any more. It is opened instead, and the only cleanup deletes a copy this call just created.

}
Err("Drag Lite into your Applications folder, open it from there, and update again".into())
}

#[tauri::command]
async fn install_update(app: AppHandle) -> Result<(), String> {
// Lite reopens from where it can be updated, and the update is offered there again.
#[cfg(target_os = "macos")]
if let Some(bundle) = std::env::current_exe()
.ok()
.and_then(|executable| executable.ancestors().nth(3).map(Path::to_path_buf))
.filter(|bundle| bundle.to_string_lossy().contains("/AppTranslocation/"))
{
tauri::async_runtime::spawn_blocking(move || {
let moved = move_to_applications(&bundle)?;
if Command::new("/usr/bin/open")
.arg("-n")
.arg(&moved)
.status()
.is_ok_and(|status| status.success())
{
return Ok(());
}
Err(format!(
"Lite is in {}, but could not open it there. Open it from that folder and update again",
path_text(&moved)
))
})
.await
.map_err(|error| error.to_string())??;
app.exit(0);
return Ok(());
}
let cleanup_app = app.clone();
let update = app
.updater_builder()
Expand Down
Loading