Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
59 changes: 59 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7968,8 +7968,67 @@ 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.
#[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 mut folders = vec![PathBuf::from("/Applications")];
folders.extend(std::env::home_dir().map(|home| home.join("Applications")));
for folder in folders {
let _ = fs::create_dir_all(&folder);
let staged = folder.join(format!(".{}.new", name.to_string_lossy()));
let _ = fs::remove_dir_all(&staged);
if !Command::new("/usr/bin/ditto")
.arg(bundle)
.arg(&staged)
.status()
.is_ok_and(|status| status.success())
{
continue;
}
// An older Lite already there is the one this copy replaces.
let target = folder.join(name);
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.

if fs::rename(&staged, &target).is_err() {
let _ = fs::remove_dir_all(&staged);
continue;
}
let _ = Command::new("/usr/bin/xattr")
.args(["-dr", "com.apple.quarantine"])
.arg(&target)
.status();
return Ok(target);
}
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> {
// The moved copy opens to the same update waiting for it.
#[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/"))
{
let moved = tauri::async_runtime::spawn_blocking(move || move_to_applications(&bundle))
.await
.map_err(|error| error.to_string())??;
Command::new("/usr/bin/open")
.arg("-n")
.arg(&moved)
.spawn()
.map_err(|error| {
format!(
"Moved Lite to {}, but could not open it: {error}",
path_text(&moved)
)
})?;
app.exit(0);
return Ok(());
}
let cleanup_app = app.clone();
let update = app
.updater_builder()
Expand Down
Loading