cli: Treat first argument as name of release channel to use for the cli (#10856)

Piotr Osiewicz created

With this commit, it is now possible to invoke cli with a release
channel of bundle as an argument. E.g: `zed stable some_arguments` will
find CLI binary of Stable channel installed on your machine and invoke
it with `some_arguments` (so the first argument is essentially omitted).

Fixes #10851

Release Notes:

- CLI now accepts an optional name of release channel as it's first
argument. For example, `zed stable` will always use your Stable
installation's CLI. Trailing args are passed along.

Change summary

Cargo.lock                        |  1 
crates/cli/Cargo.toml             |  1 
crates/cli/src/main.rs            | 43 +++++++++++++++++++++++++++++++-
crates/release_channel/src/lib.rs | 27 ++++++++++++++++----
4 files changed, 64 insertions(+), 8 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -2047,6 +2047,7 @@ dependencies = [
  "core-services",
  "ipc-channel",
  "plist",
+ "release_channel",
  "serde",
  "util",
 ]

crates/cli/Cargo.toml πŸ”—

@@ -20,6 +20,7 @@ path = "src/main.rs"
 anyhow.workspace = true
 clap.workspace = true
 ipc-channel = "0.18"
+release_channel.workspace = true
 serde.workspace = true
 util.workspace = true
 

crates/cli/src/main.rs πŸ”—

@@ -7,7 +7,7 @@ use serde::Deserialize;
 use std::{
     env,
     ffi::OsStr,
-    fs::{self},
+    fs,
     path::{Path, PathBuf},
 };
 use util::paths::PathLikeWithPosition;
@@ -53,6 +53,16 @@ struct InfoPlist {
 }
 
 fn main() -> Result<()> {
+    // Intercept version designators
+    #[cfg(target_os = "macos")]
+    if let Some(channel) = std::env::args().nth(1) {
+        //Β When the first argument is a name of a release channel, we're gonna spawn off a cli of that version, with trailing args passed along.
+        use std::str::FromStr as _;
+
+        if let Ok(channel) = release_channel::ReleaseChannel::from_str(&channel) {
+            return mac_os::spawn_channel_cli(channel, std::env::args().skip(2).collect());
+        }
+    }
     let args = Args::parse();
 
     let bundle = Bundle::detect(args.bundle_path.as_deref()).context("Bundle detection")?;
@@ -200,7 +210,7 @@ mod windows {
 
 #[cfg(target_os = "macos")]
 mod mac_os {
-    use anyhow::Context;
+    use anyhow::{Context, Result};
     use core_foundation::{
         array::{CFArray, CFIndex},
         string::kCFStringEncodingUTF8,
@@ -348,4 +358,33 @@ mod mac_os {
             )
         }
     }
+    pub(super) fn spawn_channel_cli(
+        channel: release_channel::ReleaseChannel,
+        leftover_args: Vec<String>,
+    ) -> Result<()> {
+        use anyhow::bail;
+        use std::process::Command;
+
+        let app_id_prompt = format!("id of app \"{}\"", channel.display_name());
+        let app_id_output = Command::new("osascript")
+            .arg("-e")
+            .arg(&app_id_prompt)
+            .output()?;
+        if !app_id_output.status.success() {
+            bail!("Could not determine app id for {}", channel.display_name());
+        }
+        let app_name = String::from_utf8(app_id_output.stdout)?.trim().to_owned();
+        let app_path_prompt = format!("kMDItemCFBundleIdentifier == '{app_name}'");
+        let app_path_output = Command::new("mdfind").arg(app_path_prompt).output()?;
+        if !app_path_output.status.success() {
+            bail!(
+                "Could not determine app path for {}",
+                channel.display_name()
+            );
+        }
+        let app_path = String::from_utf8(app_path_output.stdout)?.trim().to_owned();
+        let cli_path = format!("{app_path}/Contents/MacOS/cli");
+        Command::new(cli_path).args(leftover_args).spawn()?;
+        Ok(())
+    }
 }

crates/release_channel/src/lib.rs πŸ”—

@@ -2,7 +2,7 @@
 
 #![deny(missing_docs)]
 
-use std::env;
+use std::{env, str::FromStr};
 
 use gpui::{AppContext, Global, SemanticVersion};
 use once_cell::sync::Lazy;
@@ -18,11 +18,8 @@ static RELEASE_CHANNEL_NAME: Lazy<String> = if cfg!(debug_assertions) {
 
 #[doc(hidden)]
 pub static RELEASE_CHANNEL: Lazy<ReleaseChannel> =
-    Lazy::new(|| match RELEASE_CHANNEL_NAME.as_str() {
-        "dev" => ReleaseChannel::Dev,
-        "nightly" => ReleaseChannel::Nightly,
-        "preview" => ReleaseChannel::Preview,
-        "stable" => ReleaseChannel::Stable,
+    Lazy::new(|| match ReleaseChannel::from_str(&RELEASE_CHANNEL_NAME) {
+        Ok(channel) => channel,
         _ => panic!("invalid release channel {}", *RELEASE_CHANNEL_NAME),
     });
 
@@ -149,3 +146,21 @@ impl ReleaseChannel {
         }
     }
 }
+
+/// Error indicating that release channel string does not match any known release channel names.
+#[derive(Copy, Clone, Debug, Hash, PartialEq)]
+pub struct InvalidReleaseChannel;
+
+impl FromStr for ReleaseChannel {
+    type Err = InvalidReleaseChannel;
+
+    fn from_str(channel: &str) -> Result<Self, Self::Err> {
+        Ok(match channel {
+            "dev" => ReleaseChannel::Dev,
+            "nightly" => ReleaseChannel::Nightly,
+            "preview" => ReleaseChannel::Preview,
+            "stable" => ReleaseChannel::Stable,
+            _ => return Err(InvalidReleaseChannel),
+        })
+    }
+}