From 39bba3c7ecd55ea1d439a33fd048576e44fdb63c Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 22 Jun 2026 12:20:57 -0600 Subject: [PATCH] install_cli: Show notification instead of failing CLI install (#59575) Closes #10065 Closes #42293 Release Notes: - Improved error handling and documentation when Zed cannot install the CLI on macOS --- Cargo.lock | 1 + crates/install_cli/Cargo.toml | 1 + crates/install_cli/src/install_cli_binary.rs | 77 +++++++++++++++----- docs/src/macos.md | 26 +++++++ 4 files changed, 88 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b38b1ffe783..f4fdccb084f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9098,6 +9098,7 @@ dependencies = [ "anyhow", "client", "gpui", + "log", "release_channel", "smol", "util", diff --git a/crates/install_cli/Cargo.toml b/crates/install_cli/Cargo.toml index 1eede025e50..cc849d11c0e 100644 --- a/crates/install_cli/Cargo.toml +++ b/crates/install_cli/Cargo.toml @@ -18,6 +18,7 @@ test-support = [] anyhow.workspace = true client.workspace = true gpui.workspace = true +log.workspace = true release_channel.workspace = true smol.workspace = true util.workspace = true diff --git a/crates/install_cli/src/install_cli_binary.rs b/crates/install_cli/src/install_cli_binary.rs index ee8b8b2b669..a88816ace13 100644 --- a/crates/install_cli/src/install_cli_binary.rs +++ b/crates/install_cli/src/install_cli_binary.rs @@ -1,10 +1,11 @@ use super::register_zed_scheme; -use anyhow::{Context as _, Result}; +use anyhow::Result; use gpui::{AppContext as _, AsyncApp, Context, PromptLevel, Window, actions}; use release_channel::ReleaseChannel; use std::ops::Deref; use std::path::{Path, PathBuf}; use util::ResultExt; +use workspace::notifications::simple_message_notification::MessageNotification; use workspace::notifications::{DetachAndPromptErr, NotificationId}; use workspace::{Toast, Workspace}; @@ -16,14 +17,20 @@ actions!( ] ); -async fn install_script(cx: &AsyncApp) -> Result { +const CANT_INSTALL_DOCS_URL: &str = "https://zed.dev/docs/macos#cant-install-cli"; + +/// Attempts to install the CLI symlink. Returns the installed path on success, +/// or `None` if the user dismissed the macOS administrator authentication +/// prompt. Returns an error if the install could not be completed, most +/// commonly because the user is not an admin. +async fn install_script(cx: &AsyncApp) -> Result> { let cli_path = cx.update(|cx| cx.path_for_auxiliary_executable("cli"))?; let link_path = Path::new("/usr/local/bin/zed"); let bin_dir_path = link_path.parent().unwrap(); // Don't re-create symlink if it points to the same CLI binary. if smol::fs::read_link(link_path).await.ok().as_ref() == Some(&cli_path) { - return Ok(link_path.into()); + return Ok(Some(link_path.into())); } // If the symlink is not there or is outdated, first try replacing it @@ -34,12 +41,12 @@ async fn install_script(cx: &AsyncApp) -> Result { .log_err() .is_some() { - return Ok(link_path.into()); + return Ok(Some(link_path.into())); } - // The symlink could not be created, so use osascript with admin privileges - // to create it. - let status = smol::process::Command::new("/usr/bin/osascript") + // The symlink could not be created without escalating, so use osascript + // with admin privileges to create it. + let output = smol::process::Command::new("/usr/bin/osascript") .args([ "-e", &format!( @@ -52,13 +59,24 @@ async fn install_script(cx: &AsyncApp) -> Result { link_path.to_string_lossy(), ), ]) - .stdout(smol::process::Stdio::inherit()) - .stderr(smol::process::Stdio::inherit()) .output() - .await? - .status; - anyhow::ensure!(status.success(), "error running osascript"); - Ok(link_path.into()) + .await?; + + if output.status.success() { + return Ok(Some(link_path.into())); + } + + // osascript reports "User canceled." (error -128) when the administrator + // prompt is dismissed. Treat that as a cancellation rather than a failure + // so we don't show an error the user already chose to avoid. + let stderr = String::from_utf8_lossy(&output.stderr); + if stderr.contains("User canceled") || stderr.contains("-128") { + return Ok(None); + } + + // The privileged write failed, most commonly because the user is not an + // admin. + anyhow::bail!("error running osascript: {}", stderr.trim()); } pub fn install_cli_binary(window: &mut Window, cx: &mut Context) { @@ -75,9 +93,34 @@ pub fn install_cli_binary(window: &mut Window, cx: &mut Context) { cx.background_spawn(prompt).detach(); return Ok(()); } - let path = install_script(cx.deref()) - .await - .context("error creating CLI symlink")?; + let path = match install_script(cx.deref()).await { + Ok(Some(path)) => path, + // The user dismissed the administrator prompt; nothing to do. + Ok(None) => return Ok(()), + Err(error) => { + log::error!("failed to install zed CLI: {error:#}"); + workspace.update(cx, |workspace, cx| { + struct CliInstallFailed; + + workspace.show_notification( + NotificationId::unique::(), + cx, + |cx| { + cx.new(|cx| { + MessageNotification::new( + "You can add `zed` to your PATH manually.", + cx, + ) + .with_title("Couldn't install the Zed CLI") + .more_info_message("Show me how") + .more_info_url(CANT_INSTALL_DOCS_URL) + }) + }, + ); + })?; + return Ok(()); + } + }; workspace.update_in(cx, |workspace, _, cx| { struct InstalledZedCli; @@ -97,5 +140,5 @@ pub fn install_cli_binary(window: &mut Window, cx: &mut Context) { register_zed_scheme(cx).await.log_err(); Ok(()) }) - .detach_and_prompt_err("Error installing zed cli", window, cx, |_, _, _| None); + .detach_and_prompt_err("Cannot install the Zed CLI", window, cx, |_, _, _| None); } diff --git a/docs/src/macos.md b/docs/src/macos.md index b9438185c67..63cff6e9b3d 100644 --- a/docs/src/macos.md +++ b/docs/src/macos.md @@ -104,6 +104,32 @@ If the `zed` command isn't available after installation: 2. Try reinstalling the CLI via {#action cli::InstallCliBinary} in the command palette 3. Open a new terminal window to reload your PATH +### Can't install CLI {#cant-install-cli} + +{#action cli::InstallCliBinary} writes a `zed` symlink to `/usr/local/bin`, which requires administrator privileges. If your macOS account isn't in the `admin` group, Zed can't create that symlink and will report that it can't install the CLI automatically. + +Instead, you can add an alias pointing to the `cli` binary bundled inside the app. The path depends on where Zed is installed: + +```sh +# Default install (Zed in /Applications) +alias zed="/Applications/Zed.app/Contents/MacOS/cli" + +# User install (Zed in ~/Applications) +alias zed="$HOME/Applications/Zed.app/Contents/MacOS/cli" + +# Preview build (Zed Preview in ~/Applications) +alias zed="$HOME/Applications/Zed Preview.app/Contents/MacOS/cli" +``` + +Add the line that matches your install to your shell configuration file. Use `~/.zshrc` for Zsh (the default on modern macOS) or `~/.bashrc` for Bash. + +After you restart your shell, you will be able to use `zed` from your terminal: + +```sh +zed . # Open current folder +zed file.txt # Open a file +``` + ### GPU or rendering issues Zed uses Metal for rendering. If you experience graphical glitches: