Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:32:22.503Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

main.rs

1560 lines · 55.4 KB · rust
1#![allow(
2    clippy::disallowed_methods,
3    reason = "We are not in an async environment, so std::process::Command is fine"
4)]
5#![cfg_attr(
6    any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
7    allow(dead_code)
8)]
9
10mod completions;
11
12use crate::completions::Shell;
13
14use anyhow::{Context as _, Result};
15use clap::{CommandFactory, Parser};
16use cli::{CliRequest, CliResponse, IpcHandshake, ipc::IpcOneShotServer};
17use parking_lot::Mutex;
18use std::{
19    collections::{BTreeMap, BTreeSet},
20    env,
21    ffi::OsStr,
22    fs, io,
23    path::{Path, PathBuf},
24    process::ExitStatus,
25    sync::Arc,
26    thread::{self, JoinHandle},
27};
28use tempfile::{NamedTempFile, TempDir};
29use util::paths::PathWithPosition;
30use walkdir::WalkDir;
31
32use std::io::IsTerminal;
33
34const URL_PREFIX: [&'static str; 5] = ["zed://", "http://", "https://", "file://", "ssh://"];
35
36struct Detect;
37
38trait InstalledApp {
39    fn app_version_string(&self) -> String;
40    fn launch(&self, ipc_url: String, user_data_dir: Option<&str>) -> anyhow::Result<()>;
41    fn run_foreground(
42        &self,
43        ipc_url: String,
44        user_data_dir: Option<&str>,
45    ) -> io::Result<ExitStatus>;
46    /// The executable to run — one file.
47    fn path(&self) -> PathBuf;
48    /// The whole installation — the directory `--uninstall` has to remove.
49    ///
50    /// Distinct from `path` because on macOS they are never the same thing:
51    /// `path` is `Omega.app/Contents/MacOS/omega`, and the installation is
52    /// `Omega.app`, which also holds `cli`, `omega-identity-proof`, and a
53    /// bundled Node runtime. `0.2.0-rc16` passed `path` to the uninstaller, so
54    /// a full uninstall left 130.9 MB and five executables in `/Applications`,
55    /// among them a `cli` that still carried `--uninstall` (omega#92).
56    fn installation_root(&self) -> PathBuf;
57}
58
59#[derive(Parser, Debug)]
60#[command(
61    name = "omega",
62    disable_version_flag = true,
63    before_help = "The Omega CLI binary.
64This CLI is a separate binary that invokes Omega.
65
66Examples:
67    `omega`
68          Simply opens Omega
69    `omega --foreground`
70          Runs in foreground (shows all logs)
71    `omega path-to-your-project`
72          Open your project in Omega
73    `omega -n path-to-file `
74          Open file/folder in a new window",
75    after_help = "To read from stdin, append '-', e.g. 'ps axf | omega -'"
76)]
77struct Args {
78    /// Wait for all of the given paths to be opened/closed before exiting.
79    ///
80    /// When opening a directory, waits until the created window is closed.
81    #[arg(short, long)]
82    wait: bool,
83    /// Add files to the currently open workspace
84    #[arg(short, long, overrides_with_all = ["new", "reuse", "existing", "classic"])]
85    add: bool,
86    /// Create a new workspace
87    #[arg(short, long, overrides_with_all = ["add", "reuse", "existing", "classic"])]
88    new: bool,
89    /// Reuse an existing window, replacing its workspace
90    #[arg(short, long, overrides_with_all = ["add", "new", "existing", "classic"], hide = true)]
91    reuse: bool,
92    /// Open in existing Omega window
93    #[arg(short = 'e', long = "existing", overrides_with_all = ["add", "new", "reuse", "classic"])]
94    existing: bool,
95    /// Use the classic open behavior: new window for directories, reuse for files
96    #[arg(long, hide = true, overrides_with_all = ["add", "new", "reuse", "existing"])]
97    classic: bool,
98    /// Sets a custom directory for all user data (e.g., database, extensions, logs).
99    /// This overrides the default platform-specific data directory location:
100    #[cfg_attr(
101        target_os = "macos",
102        doc = "`~/Library/Application Support/<Omega channel>`."
103    )]
104    #[cfg_attr(target_os = "windows", doc = "`%LOCALAPPDATA%\\<Omega channel>`.")]
105    #[cfg_attr(
106        not(any(target_os = "windows", target_os = "macos")),
107        doc = "`$XDG_DATA_HOME/<omega-channel>`."
108    )]
109    #[arg(long, value_name = "DIR", value_hint = clap::ValueHint::DirPath)]
110    user_data_dir: Option<String>,
111    /// The paths to open in Omega (space-separated).
112    ///
113    /// Use `path:line:column` syntax to open a file at the given line and column.
114    #[arg(trailing_var_arg = true, value_hint = clap::ValueHint::AnyPath)]
115    paths_with_position: Vec<String>,
116    /// Print Omega's version and the app path.
117    #[arg(short, long)]
118    version: bool,
119    /// Run Omega in the foreground (useful for debugging)
120    #[arg(long)]
121    foreground: bool,
122    /// Custom path to Omega.app or the omega binary
123    #[arg(long = "omega", alias = "zed")]
124    omega: Option<PathBuf>,
125    /// Run Omega in dev-server mode
126    #[arg(long)]
127    dev_server_token: Option<String>,
128    /// The username and WSL distribution to use when opening paths. If not specified,
129    /// Omega will attempt to open the paths directly.
130    ///
131    /// The username is optional, and if not specified, the default user for the distribution
132    /// will be used.
133    ///
134    /// Example: `me@Ubuntu` or `Ubuntu`.
135    ///
136    /// WARN: You should not fill in this field by hand.
137    #[cfg(target_os = "windows")]
138    #[arg(long, value_name = "USER@DISTRO")]
139    wsl: Option<String>,
140    /// Not supported in Omega CLI, only supported on Omega binary
141    /// Will attempt to give the correct command to run
142    #[arg(long)]
143    system_specs: bool,
144    /// Open the project in a dev container.
145    ///
146    /// Automatically triggers "Reopen in Dev Container" if a `.devcontainer/`
147    /// configuration is found in the project directory.
148    #[arg(long)]
149    dev_container: bool,
150    /// Pairs of file paths to diff. Can be specified multiple times.
151    /// When directories are provided, recurses into them and shows all changed files in a single multi-diff view.
152    #[arg(long, action = clap::ArgAction::Append, num_args = 2, value_names = ["OLD_PATH", "NEW_PATH"], value_hint = clap::ValueHint::AnyPath)]
153    diff: Vec<String>,
154    /// Generate shell completions for Omega
155    #[arg(long, value_names = ["SHELL"])]
156    completions: Option<Shell>,
157    /// Uninstall Omega from user system
158    #[cfg(all(
159        any(target_os = "linux", target_os = "macos"),
160        not(feature = "no-bundled-uninstall")
161    ))]
162    #[arg(long)]
163    uninstall: bool,
164
165    /// Used for SSH/Git password authentication, to remove the need for netcat as a dependency,
166    /// by having Omega act like netcat communicating over a Unix socket.
167    #[arg(long, hide = true)]
168    askpass: Option<String>,
169}
170
171/// Parses a path containing a position (e.g. `path:line:column`)
172/// and returns its canonicalized string representation.
173///
174/// If a part of path doesn't exist, it will canonicalize the
175/// existing part and append the non-existing part.
176///
177/// This method must return an absolute path, as many Omega
178/// crates assume absolute paths.
179fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
180    match Path::new(argument_str).canonicalize() {
181        Ok(existing_path) => Ok(PathWithPosition::from_path(existing_path)),
182        Err(_) => PathWithPosition::parse_str(argument_str).map_path(|mut path| {
183            let curdir = env::current_dir().context("retrieving current directory")?;
184            let mut children = Vec::new();
185            let root;
186            loop {
187                // canonicalize handles './', and '/'.
188                if let Ok(canonicalized) = fs::canonicalize(&path) {
189                    root = canonicalized;
190                    break;
191                }
192                // The comparison to `curdir` is just a shortcut
193                // since we know it is canonical. The other one
194                // is if `argument_str` is a string that starts
195                // with a name (e.g. "foo/bar").
196                if path == curdir || path == Path::new("") {
197                    root = curdir;
198                    break;
199                }
200                children.push(
201                    path.file_name()
202                        .with_context(|| format!("parsing as path with position {argument_str}"))?
203                        .to_owned(),
204                );
205                if !path.pop() {
206                    unreachable!("parsing as path with position {argument_str}");
207                }
208            }
209            Ok(children.iter().rev().fold(root, |mut path, child| {
210                path.push(child);
211                path
212            }))
213        }),
214    }
215    .map(|path_with_pos| path_with_pos.to_string(&|path| path.to_string_lossy().into_owned()))
216}
217
218/// Returns whether a `--diff` argument refers to an existing path, allowing a
219/// trailing `:line:column` suffix (parsed later by the Omega side, matching how
220/// regular `omega path:line:column` arguments are handled).
221fn diff_path_exists(diff_path: &str) -> bool {
222    Path::new(diff_path).exists() || PathWithPosition::parse_str(diff_path).path.exists()
223}
224
225fn expand_directory_diff_pairs(
226    diff_pairs: Vec<[String; 2]>,
227) -> anyhow::Result<(Vec<[String; 2]>, Vec<TempDir>)> {
228    let mut expanded = Vec::new();
229    let mut temp_dirs = Vec::new();
230
231    for pair in diff_pairs {
232        let left = PathBuf::from(&pair[0]);
233        let right = PathBuf::from(&pair[1]);
234
235        if left.is_dir() && right.is_dir() {
236            let (mut pairs, temp_dir) = expand_directory_pair(&left, &right)?;
237            expanded.append(&mut pairs);
238            if let Some(temp_dir) = temp_dir {
239                temp_dirs.push(temp_dir);
240            }
241        } else {
242            expanded.push(pair);
243        }
244    }
245
246    Ok((expanded, temp_dirs))
247}
248
249fn expand_directory_pair(
250    left: &Path,
251    right: &Path,
252) -> anyhow::Result<(Vec<[String; 2]>, Option<TempDir>)> {
253    let left_files = collect_files(left)?;
254    let right_files = collect_files(right)?;
255
256    let mut rel_paths = BTreeSet::new();
257    rel_paths.extend(left_files.keys().cloned());
258    rel_paths.extend(right_files.keys().cloned());
259
260    let mut temp_dir = TempDir::new()?;
261    let mut temp_dir_used = false;
262    let mut pairs = Vec::new();
263
264    for rel in rel_paths {
265        match (left_files.get(&rel), right_files.get(&rel)) {
266            (Some(left_path), Some(right_path)) => {
267                pairs.push([
268                    left_path.to_string_lossy().into_owned(),
269                    right_path.to_string_lossy().into_owned(),
270                ]);
271            }
272            (Some(left_path), None) => {
273                let stub = create_empty_stub(&mut temp_dir, &rel)?;
274                temp_dir_used = true;
275                pairs.push([
276                    left_path.to_string_lossy().into_owned(),
277                    stub.to_string_lossy().into_owned(),
278                ]);
279            }
280            (None, Some(right_path)) => {
281                let stub = create_empty_stub(&mut temp_dir, &rel)?;
282                temp_dir_used = true;
283                pairs.push([
284                    stub.to_string_lossy().into_owned(),
285                    right_path.to_string_lossy().into_owned(),
286                ]);
287            }
288            (None, None) => {}
289        }
290    }
291
292    let temp_dir = if temp_dir_used { Some(temp_dir) } else { None };
293    Ok((pairs, temp_dir))
294}
295
296fn collect_files(root: &Path) -> anyhow::Result<BTreeMap<PathBuf, PathBuf>> {
297    let mut files = BTreeMap::new();
298
299    for entry in WalkDir::new(root) {
300        let entry = entry?;
301        if entry.file_type().is_file() {
302            let rel = entry
303                .path()
304                .strip_prefix(root)
305                .context("stripping directory prefix")?
306                .to_path_buf();
307            files.insert(rel, entry.into_path());
308        }
309    }
310
311    Ok(files)
312}
313
314fn create_empty_stub(temp_dir: &mut TempDir, rel: &Path) -> anyhow::Result<PathBuf> {
315    let stub_path = temp_dir.path().join(rel);
316    if let Some(parent) = stub_path.parent() {
317        fs::create_dir_all(parent)?;
318    }
319    fs::File::create(&stub_path)?;
320    Ok(stub_path)
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326    use serde_json::json;
327    use util::path;
328    use util::paths::SanitizedPath;
329    use util::test::TempTree;
330
331    macro_rules! assert_path_eq {
332        ($left:expr, $right:expr) => {
333            assert_eq!(
334                SanitizedPath::new(Path::new(&$left)),
335                SanitizedPath::new(Path::new(&$right))
336            )
337        };
338    }
339
340    fn cwd() -> PathBuf {
341        env::current_dir().unwrap()
342    }
343
344    static CWD_LOCK: Mutex<()> = Mutex::new(());
345
346    fn with_cwd<T>(path: &Path, f: impl FnOnce() -> anyhow::Result<T>) -> anyhow::Result<T> {
347        let _lock = CWD_LOCK.lock();
348        let old_cwd = cwd();
349        env::set_current_dir(path)?;
350        let result = f();
351        env::set_current_dir(old_cwd)?;
352        result
353    }
354
355    #[test]
356    fn test_parse_non_existing_path() {
357        // Absolute path
358        let result = parse_path_with_position(path!("/non/existing/path.txt")).unwrap();
359        assert_path_eq!(result, path!("/non/existing/path.txt"));
360
361        // Absolute path in cwd
362        let path = cwd().join(path!("non/existing/path.txt"));
363        let expected = path.to_string_lossy().to_string();
364        let result = parse_path_with_position(&expected).unwrap();
365        assert_path_eq!(result, expected);
366
367        // Relative path
368        let result = parse_path_with_position(path!("non/existing/path.txt")).unwrap();
369        assert_path_eq!(result, expected)
370    }
371
372    #[test]
373    fn test_parse_existing_path() {
374        let temp_tree = TempTree::new(json!({
375            "file.txt": "",
376        }));
377        let file_path = temp_tree.path().join("file.txt");
378        let expected = file_path.to_string_lossy().to_string();
379
380        // Absolute path
381        let result = parse_path_with_position(file_path.to_str().unwrap()).unwrap();
382        assert_path_eq!(result, expected);
383
384        // Relative path
385        let result = with_cwd(temp_tree.path(), || parse_path_with_position("file.txt")).unwrap();
386        assert_path_eq!(result, expected);
387    }
388
389    // NOTE:
390    // While POSIX symbolic links are somewhat supported on Windows, they are an opt in by the user, and thus
391    // we assume that they are not supported out of the box.
392    #[cfg(not(windows))]
393    #[test]
394    fn test_parse_symlink_file() {
395        let temp_tree = TempTree::new(json!({
396            "target.txt": "",
397        }));
398        let target_path = temp_tree.path().join("target.txt");
399        let symlink_path = temp_tree.path().join("symlink.txt");
400        std::os::unix::fs::symlink(&target_path, &symlink_path).unwrap();
401
402        // Absolute path
403        let result = parse_path_with_position(symlink_path.to_str().unwrap()).unwrap();
404        assert_eq!(result, target_path.to_string_lossy());
405
406        // Relative path
407        let result =
408            with_cwd(temp_tree.path(), || parse_path_with_position("symlink.txt")).unwrap();
409        assert_eq!(result, target_path.to_string_lossy());
410    }
411
412    #[cfg(not(windows))]
413    #[test]
414    fn test_parse_symlink_dir() {
415        let temp_tree = TempTree::new(json!({
416            "some": {
417                "dir": { // symlink target
418                    "ec": {
419                        "tory": {
420                            "file.txt": "",
421        }}}}}));
422
423        let target_file_path = temp_tree.path().join("some/dir/ec/tory/file.txt");
424        let expected = target_file_path.to_string_lossy();
425
426        let dir_path = temp_tree.path().join("some/dir");
427        let symlink_path = temp_tree.path().join("symlink");
428        std::os::unix::fs::symlink(&dir_path, &symlink_path).unwrap();
429
430        // Absolute path
431        let result =
432            parse_path_with_position(symlink_path.join("ec/tory/file.txt").to_str().unwrap())
433                .unwrap();
434        assert_eq!(result, expected);
435
436        // Relative path
437        let result = with_cwd(temp_tree.path(), || {
438            parse_path_with_position("symlink/ec/tory/file.txt")
439        })
440        .unwrap();
441        assert_eq!(result, expected);
442    }
443}
444
445fn parse_path_in_wsl(source: &str, wsl: &str) -> Result<String> {
446    let mut source = PathWithPosition::parse_str(source);
447
448    let (user, distro_name) = if let Some((user, distro)) = wsl.split_once('@') {
449        if user.is_empty() {
450            anyhow::bail!("user is empty in wsl argument");
451        }
452        (Some(user), distro)
453    } else {
454        (None, wsl)
455    };
456
457    let mut args = vec!["--distribution", distro_name];
458    if let Some(user) = user {
459        args.push("--user");
460        args.push(user);
461    }
462
463    let command = [
464        OsStr::new("realpath"),
465        OsStr::new("-s"),
466        source.path.as_ref(),
467    ];
468
469    let output = util::command::new_std_command("wsl.exe")
470        .args(&args)
471        .arg("--exec")
472        .args(&command)
473        .output()?;
474    let result = if output.status.success() {
475        String::from_utf8_lossy(&output.stdout).to_string()
476    } else {
477        let fallback = util::command::new_std_command("wsl.exe")
478            .args(&args)
479            .arg("--")
480            .args(&command)
481            .output()?;
482        String::from_utf8_lossy(&fallback.stdout).to_string()
483    };
484
485    source.path = Path::new(result.trim()).to_owned();
486
487    Ok(source.to_string(&|path| path.to_string_lossy().into_owned()))
488}
489
490fn main() {
491    if let Err(error) = run() {
492        eprintln!("error: {error:#}");
493        std::process::exit(1);
494    }
495}
496
497fn run() -> Result<()> {
498    #[cfg(unix)]
499    util::prevent_root_execution();
500
501    // Exit flatpak sandbox if needed
502    #[cfg(target_os = "linux")]
503    {
504        flatpak::try_restart_to_host();
505        flatpak::ld_extra_libs();
506    }
507
508    // Intercept version designators
509    #[cfg(target_os = "macos")]
510    if let Some(channel) = std::env::args().nth(1).filter(|arg| arg.starts_with("--")) {
511        // When the first argument is a name of a release channel, we're going to spawn off the CLI of that version, with trailing args passed along.
512        use std::str::FromStr as _;
513
514        if let Ok(channel) = release_channel::ReleaseChannel::from_str(&channel[2..]) {
515            return mac_os::spawn_channel_cli(channel, std::env::args().skip(2).collect());
516        }
517    }
518
519    // Must happen before clap — SSH invokes cli.exe directly as SSH_ASKPASS
520    // and passes the socket path via env var to avoid argument parsing.
521    if let Ok(socket) = std::env::var("ZED_ASKPASS_SOCKET") {
522        askpass::main_from_args(&socket, std::env::args().skip(1));
523        return Ok(());
524    }
525
526    let args = Args::parse();
527
528    // `zed --askpass` Makes zed operate in nc/netcat mode for use with askpass
529    if let Some(socket) = &args.askpass {
530        askpass::main(socket);
531        return Ok(());
532    }
533
534    // Set custom data directory before any path operations
535    let user_data_dir = args.user_data_dir.clone();
536    if let Some(dir) = &user_data_dir {
537        paths::set_custom_data_dir(dir);
538    }
539
540    #[cfg(target_os = "linux")]
541    let args = flatpak::set_bin_if_no_escape(args);
542
543    let app = Detect::detect(args.omega.as_deref()).context("Bundle detection")?;
544
545    if let Some(shell) = &args.completions {
546        let file_path = std::env::current_exe()?;
547        let file_name = file_path
548            .file_name()
549            .and_then(OsStr::to_str)
550            .ok_or("--completions expects a UTF-8 name for cli bin")
551            .map_err(anyhow::Error::msg)?;
552        let mut cmd = Args::command();
553        cmd.set_bin_name(file_name);
554        cmd.build();
555        crate::completions::main(&cmd, shell);
556        return Ok(());
557    }
558
559    if args.version {
560        println!("{}", app.app_version_string());
561        return Ok(());
562    }
563
564    if args.system_specs {
565        let path = app.path();
566        let msg = [
567            "The `--system-specs` argument is not supported in the Omega CLI, only on Omega binary.",
568            "To retrieve the system specs on the command line, run the following command:",
569            &format!("{} --system-specs", path.display()),
570        ];
571        anyhow::bail!(msg.join("\n"));
572    }
573
574    #[cfg(all(
575        any(target_os = "linux", target_os = "macos"),
576        not(feature = "no-bundled-uninstall")
577    ))]
578    if args.uninstall {
579        // OMEGA-DELTA-0036. The plan is built here, from the same `paths::`
580        // functions the running application writes those directories with, and
581        // handed to the script. The script has no paths of its own, because a
582        // hand-written path table disconnected from the code that creates the
583        // directories is exactly how `0.2.0-rc14` shipped an "Uninstall Omega"
584        // that kept Omega and deleted another editor instead.
585        static UNINSTALL_SCRIPT: &[u8] = include_bytes!("../../../script/uninstall.sh");
586
587        let tmp_dir = tempfile::tempdir()?;
588        let script_path = tmp_dir.path().join("uninstall.sh");
589        fs::write(&script_path, UNINSTALL_SCRIPT)?;
590
591        use std::os::unix::fs::PermissionsExt as _;
592        fs::set_permissions(&script_path, fs::Permissions::from_mode(0o755))?;
593
594        // `installation_root()`, never `path()`: on macOS the executable is one
595        // file inside `Omega.app`, and removing it leaves the bundle, four more
596        // executables and a bundled Node runtime behind (omega#92).
597        let plan =
598            cli::uninstall::UninstallRoots::from_installed_paths(Some(app.installation_root()))
599                .plan(paths::app_name());
600
601        let status = std::process::Command::new("sh")
602            .arg(&script_path)
603            .env("OMEGA_UNINSTALL_PRODUCT", &plan.product)
604            .env("OMEGA_UNINSTALL_PATHS", plan.paths_env())
605            .env("OMEGA_UNINSTALL_CONFIG_DIR", &plan.config_dir)
606            .status()
607            .context("Failed to execute uninstall script")?;
608
609        std::process::exit(status.code().unwrap_or(1));
610    }
611
612    let (server, server_name) =
613        IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Omega spawn")?;
614    let url = format!("zed-cli://{server_name}");
615
616    let open_behavior = if args.new {
617        cli::OpenBehavior::AlwaysNew
618    } else if args.add {
619        cli::OpenBehavior::Add
620    } else if args.existing {
621        cli::OpenBehavior::ExistingWindow
622    } else if args.classic {
623        cli::OpenBehavior::Classic
624    } else if args.reuse {
625        cli::OpenBehavior::Reuse
626    } else {
627        cli::OpenBehavior::Default
628    };
629
630    let env = {
631        #[cfg(any(target_os = "linux", target_os = "freebsd"))]
632        {
633            use collections::HashMap;
634
635            // On Linux, the desktop entry uses `cli` to spawn `zed`.
636            // We need to handle env vars correctly since std::env::vars() may not contain
637            // project-specific vars (e.g. those set by direnv).
638            // By setting env to None here, the LSP will use worktree env vars instead,
639            // which is what we want.
640            if !std::io::stdout().is_terminal() {
641                None
642            } else {
643                Some(std::env::vars().collect::<HashMap<_, _>>())
644            }
645        }
646
647        #[cfg(target_os = "windows")]
648        {
649            // On Windows, by default, a child process inherits a copy of the environment block of the parent process.
650            // So we don't need to pass env vars explicitly.
651            None
652        }
653
654        #[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "windows")))]
655        {
656            use collections::HashMap;
657
658            Some(std::env::vars().collect::<HashMap<_, _>>())
659        }
660    };
661
662    let exit_status = Arc::new(Mutex::new(None));
663    let mut paths = vec![];
664    let mut urls = vec![];
665    let mut diff_paths = vec![];
666    let mut stdin_tmp_file: Option<fs::File> = None;
667    let mut anonymous_fd_tmp_files = vec![];
668
669    // Check if any diff paths are directories to determine diff_all mode
670    let diff_all_mode = args
671        .diff
672        .chunks(2)
673        .any(|pair| Path::new(&pair[0]).is_dir() || Path::new(&pair[1]).is_dir());
674
675    for path in args.diff.chunks(2) {
676        let left = parse_path_with_position(&path[0])?;
677        let right = parse_path_with_position(&path[1])?;
678        for diff_path in [&left, &right] {
679            anyhow::ensure!(
680                diff_path_exists(diff_path),
681                "--diff path does not exist: {diff_path}"
682            );
683        }
684        diff_paths.push([left, right]);
685    }
686
687    let (expanded_diff_paths, temp_dirs) = expand_directory_diff_pairs(diff_paths)?;
688    diff_paths = expanded_diff_paths;
689    // Prevent automatic cleanup of temp directories containing empty stub files
690    // for directory diffs. The CLI process may exit before Zed has read these
691    // files (e.g., when RPC-ing into an already-running instance). The files
692    // live in the OS temp directory and will be cleaned up on reboot.
693    for temp_dir in temp_dirs {
694        let _ = temp_dir.keep();
695    }
696
697    #[cfg(target_os = "windows")]
698    let wsl = args.wsl.as_ref();
699    #[cfg(not(target_os = "windows"))]
700    let wsl = None;
701
702    for path in args.paths_with_position.iter() {
703        if URL_PREFIX.iter().any(|&prefix| path.starts_with(prefix)) {
704            urls.push(path.to_string());
705        } else if path == "-" && args.paths_with_position.len() == 1 {
706            let file = NamedTempFile::new()?;
707            paths.push(file.path().to_string_lossy().into_owned());
708            let (file, _) = file.keep()?;
709            stdin_tmp_file = Some(file);
710        } else if let Some(file) = anonymous_fd(path) {
711            let tmp_file = NamedTempFile::new()?;
712            paths.push(tmp_file.path().to_string_lossy().into_owned());
713            let (tmp_file, _) = tmp_file.keep()?;
714            anonymous_fd_tmp_files.push((file, tmp_file));
715        } else if let Some(wsl) = wsl {
716            urls.push(format!("file://{}", parse_path_in_wsl(path, wsl)?));
717        } else {
718            paths.push(parse_path_with_position(path)?);
719        }
720    }
721
722    anyhow::ensure!(
723        args.dev_server_token.is_none(),
724        "Dev servers were removed in v0.157.x please upgrade to SSH remoting: https://zed.dev/docs/remote-development"
725    );
726
727    rayon::ThreadPoolBuilder::new()
728        .num_threads(4)
729        .stack_size(10 * 1024 * 1024)
730        .thread_name(|ix| format!("RayonWorker{}", ix))
731        .build_global()
732        .unwrap();
733
734    let sender: JoinHandle<anyhow::Result<()>> = thread::Builder::new()
735        .name("CliReceiver".to_string())
736        .spawn({
737            let exit_status = exit_status.clone();
738            let user_data_dir_for_thread = user_data_dir.clone();
739            move || {
740                let (_, handshake) = server.accept().context("Handshake after Omega spawn")?;
741                let (tx, rx) = (handshake.requests, handshake.responses);
742
743                #[cfg(target_os = "windows")]
744                let wsl = args.wsl;
745                #[cfg(not(target_os = "windows"))]
746                let wsl = None;
747
748                let open_request = CliRequest::Open {
749                    paths,
750                    urls,
751                    diff_paths,
752                    diff_all: diff_all_mode,
753                    wsl,
754                    wait: args.wait,
755                    open_behavior,
756                    env,
757                    user_data_dir: user_data_dir_for_thread,
758                    dev_container: args.dev_container,
759                    cwd: env::current_dir().ok(),
760                };
761
762                tx.send(open_request)?;
763
764                while let Ok(response) = rx.recv() {
765                    match response {
766                        CliResponse::Ping => {}
767                        CliResponse::Stdout { message } => println!("{message}"),
768                        CliResponse::Stderr { message } => eprintln!("{message}"),
769                        CliResponse::Exit { status } => {
770                            exit_status.lock().replace(status);
771                            return Ok(());
772                        }
773                        CliResponse::PromptOpenBehavior => {
774                            let behavior = prompt_open_behavior()
775                                .unwrap_or(cli::CliBehaviorSetting::ExistingWindow);
776                            tx.send(CliRequest::SetOpenBehavior { behavior })?;
777                        }
778                    }
779                }
780
781                Ok(())
782            }
783        })
784        .unwrap();
785
786    let stdin_pipe_handle: Option<JoinHandle<anyhow::Result<()>>> =
787        stdin_tmp_file.map(|mut tmp_file| {
788            thread::Builder::new()
789                .name("CliStdin".to_string())
790                .spawn(move || {
791                    let mut stdin = std::io::stdin().lock();
792                    if !io::IsTerminal::is_terminal(&stdin) {
793                        io::copy(&mut stdin, &mut tmp_file)?;
794                    }
795                    Ok(())
796                })
797                .unwrap()
798        });
799
800    let anonymous_fd_pipe_handles: Vec<_> = anonymous_fd_tmp_files
801        .into_iter()
802        .map(|(mut file, mut tmp_file)| {
803            thread::Builder::new()
804                .name("CliAnonymousFd".to_string())
805                .spawn(move || io::copy(&mut file, &mut tmp_file))
806                .unwrap()
807        })
808        .collect();
809
810    if args.foreground {
811        app.run_foreground(url, user_data_dir.as_deref())?;
812    } else {
813        app.launch(url, user_data_dir.as_deref())?;
814        sender.join().unwrap()?;
815        if let Some(handle) = stdin_pipe_handle {
816            handle.join().unwrap()?;
817        }
818        for handle in anonymous_fd_pipe_handles {
819            handle.join().unwrap()?;
820        }
821    }
822
823    if let Some(exit_status) = exit_status.lock().take() {
824        std::process::exit(exit_status);
825    }
826    Ok(())
827}
828
829fn anonymous_fd(path: &str) -> Option<fs::File> {
830    #[cfg(target_os = "linux")]
831    {
832        use std::os::fd::{self, FromRawFd};
833
834        let fd_str = path.strip_prefix("/proc/self/fd/")?;
835
836        let link = fs::read_link(path).ok()?;
837        if !link.starts_with("memfd:") {
838            return None;
839        }
840
841        let fd: fd::RawFd = fd_str.parse().ok()?;
842        let file = unsafe { fs::File::from_raw_fd(fd) };
843        Some(file)
844    }
845    #[cfg(any(target_os = "macos", target_os = "freebsd"))]
846    {
847        use std::os::{
848            fd::{self, FromRawFd},
849            unix::fs::FileTypeExt,
850        };
851
852        let fd_str = path.strip_prefix("/dev/fd/")?;
853
854        let metadata = fs::metadata(path).ok()?;
855        let file_type = metadata.file_type();
856        if !file_type.is_fifo() && !file_type.is_socket() {
857            return None;
858        }
859        let fd: fd::RawFd = fd_str.parse().ok()?;
860        let file = unsafe { fs::File::from_raw_fd(fd) };
861        Some(file)
862    }
863    #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "freebsd")))]
864    {
865        _ = path;
866        // not implemented for bsd, windows. Could be, but isn't yet
867        None
868    }
869}
870
871/// Shows an interactive prompt asking the user to choose the default open
872/// behavior for `omega <path>`. Returns `None` if the prompt cannot be shown
873/// (e.g. stdin is not a terminal) or the user cancels.
874fn prompt_open_behavior() -> Option<cli::CliBehaviorSetting> {
875    if !std::io::stdin().is_terminal() {
876        return None;
877    }
878
879    let blue = console::Style::new().blue();
880    // Every command form here is built from `paths::BINARY_NAME`, never
881    // written out. The literals `zed --existing`, `zed --classic` and
882    // `zed <path>` shipped in the signed `cli` of `0.2.0-rc16`, in a panel
883    // whose surrounding copy said "Omega window" and "Omega settings", so it
884    // named our product twice and somebody else's binary three times
885    // (omega#93). A command form cannot drift from the binary it describes if
886    // it is spelled by the binary's own name.
887    let items = [
888        format!(
889            "Add to existing Omega window ({})",
890            blue.apply_to(format!("{} --existing", paths::BINARY_NAME))
891        ),
892        format!(
893            "Open a new window ({})",
894            blue.apply_to(format!("{} --classic", paths::BINARY_NAME))
895        ),
896    ];
897
898    let prompt = format!(
899        "Configure default behavior for {}\n{}",
900        blue.apply_to(format!("{} <path>", paths::BINARY_NAME)),
901        console::style("You can change this later in Omega settings"),
902    );
903
904    let selection = dialoguer::Select::new()
905        .with_prompt(&prompt)
906        .items(&items)
907        .default(0)
908        .interact()
909        .ok()?;
910
911    Some(if selection == 0 {
912        cli::CliBehaviorSetting::ExistingWindow
913    } else {
914        cli::CliBehaviorSetting::NewWindow
915    })
916}
917
918#[cfg(any(target_os = "linux", target_os = "freebsd"))]
919mod linux {
920    use std::{
921        env,
922        ffi::OsString,
923        io,
924        os::unix::net::{SocketAddr, UnixDatagram},
925        path::{Path, PathBuf},
926        process::{self, ExitStatus},
927        thread,
928        time::Duration,
929    };
930
931    use anyhow::{Context as _, anyhow};
932    use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
933    use fork::Fork;
934
935    use crate::{Detect, InstalledApp};
936
937    struct App(PathBuf);
938
939    impl Detect {
940        pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
941            let path = if let Some(path) = path {
942                path.to_path_buf().canonicalize()?
943            } else {
944                let cli = env::current_exe()?;
945                let dir = cli.parent().context("no parent path for cli")?;
946
947                // libexec is the standard, lib/omega is for distributions that avoid libexec,
948                // and ./omega is for the target directory in development builds.
949                let possible_locations = [
950                    "../libexec/omega-editor",
951                    "../lib/omega/omega-editor",
952                    "./omega",
953                ];
954                possible_locations
955                    .iter()
956                    .find_map(|p| dir.join(p).canonicalize().ok().filter(|path| path != &cli))
957                    .with_context(|| {
958                        format!("could not find any of: {}", possible_locations.join(", "))
959                    })?
960            };
961
962            Ok(App(path))
963        }
964    }
965
966    impl InstalledApp for App {
967        fn app_version_string(&self) -> String {
968            format!(
969                "Omega {}{}{} – {}",
970                if *release_channel::RELEASE_CHANNEL_NAME == "stable" {
971                    "".to_string()
972                } else {
973                    format!("{} ", *release_channel::RELEASE_CHANNEL_NAME)
974                },
975                option_env!("RELEASE_VERSION").unwrap_or_default(),
976                match option_env!("ZED_COMMIT_SHA") {
977                    Some(commit_sha) => format!(" {commit_sha} "),
978                    None => "".to_string(),
979                },
980                self.0.display(),
981            )
982        }
983
984        fn launch(&self, ipc_url: String, user_data_dir: Option<&str>) -> anyhow::Result<()> {
985            let data_dir = user_data_dir
986                .map(PathBuf::from)
987                .unwrap_or_else(|| paths::data_dir().clone());
988
989            let sock_path = data_dir.join(format!(
990                "zed-{}.sock",
991                *release_channel::RELEASE_CHANNEL_NAME
992            ));
993            let sock = UnixDatagram::unbound()?;
994            if sock.connect(&sock_path).is_err() {
995                self.boot_background(ipc_url, user_data_dir)?;
996            } else {
997                sock.send(ipc_url.as_bytes())?;
998            }
999            Ok(())
1000        }
1001
1002        fn run_foreground(
1003            &self,
1004            ipc_url: String,
1005            user_data_dir: Option<&str>,
1006        ) -> io::Result<ExitStatus> {
1007            let mut cmd = std::process::Command::new(self.0.clone());
1008            cmd.arg(ipc_url);
1009            if let Some(dir) = user_data_dir {
1010                cmd.arg("--user-data-dir").arg(dir);
1011            }
1012            cmd.status()
1013        }
1014
1015        fn path(&self) -> PathBuf {
1016            self.0.clone()
1017        }
1018
1019        fn installation_root(&self) -> PathBuf {
1020            // A Linux install is loose files under a prefix — `libexec`,
1021            // `bin`, `share` — with no single directory that is only Omega, so
1022            // the editor binary is the honest answer here. macOS is where the
1023            // installation really is one directory.
1024            self.0.clone()
1025        }
1026    }
1027
1028    impl App {
1029        fn boot_background(
1030            &self,
1031            ipc_url: String,
1032            user_data_dir: Option<&str>,
1033        ) -> anyhow::Result<()> {
1034            let path = &self.0;
1035
1036            match fork::fork() {
1037                Ok(Fork::Parent(_)) => Ok(()),
1038                Ok(Fork::Child) => {
1039                    unsafe { std::env::set_var(FORCE_CLI_MODE_ENV_VAR_NAME, "") };
1040                    if fork::setsid().is_err() {
1041                        eprintln!("failed to setsid: {}", std::io::Error::last_os_error());
1042                        process::exit(1);
1043                    }
1044                    if fork::close_fd().is_err() {
1045                        eprintln!("failed to close_fd: {}", std::io::Error::last_os_error());
1046                    }
1047                    let mut args: Vec<OsString> =
1048                        vec![path.as_os_str().to_owned(), OsString::from(ipc_url)];
1049                    if let Some(dir) = user_data_dir {
1050                        args.push(OsString::from("--user-data-dir"));
1051                        args.push(OsString::from(dir));
1052                    }
1053                    let error = exec::execvp(path.clone(), &args);
1054                    // if exec succeeded, we never get here.
1055                    eprintln!("failed to exec {:?}: {}", path, error);
1056                    process::exit(1)
1057                }
1058                Err(_) => Err(anyhow!(io::Error::last_os_error())),
1059            }
1060        }
1061
1062        fn wait_for_socket(
1063            &self,
1064            sock_addr: &SocketAddr,
1065            sock: &mut UnixDatagram,
1066        ) -> Result<(), std::io::Error> {
1067            for _ in 0..100 {
1068                thread::sleep(Duration::from_millis(10));
1069                if sock.connect_addr(sock_addr).is_ok() {
1070                    return Ok(());
1071                }
1072            }
1073            sock.connect_addr(sock_addr)
1074        }
1075    }
1076}
1077
1078#[cfg(target_os = "linux")]
1079mod flatpak {
1080    use std::ffi::OsString;
1081    use std::path::PathBuf;
1082    use std::process::Command;
1083    use std::{env, process};
1084
1085    const EXTRA_LIB_ENV_NAME: &str = "ZED_FLATPAK_LIB_PATH";
1086    const NO_ESCAPE_ENV_NAME: &str = "ZED_FLATPAK_NO_ESCAPE";
1087
1088    /// Adds bundled libraries to LD_LIBRARY_PATH if running under flatpak
1089    pub fn ld_extra_libs() {
1090        let mut paths = if let Ok(paths) = env::var("LD_LIBRARY_PATH") {
1091            env::split_paths(&paths).collect()
1092        } else {
1093            Vec::new()
1094        };
1095
1096        if let Ok(extra_path) = env::var(EXTRA_LIB_ENV_NAME) {
1097            paths.push(extra_path.into());
1098        }
1099
1100        unsafe { env::set_var("LD_LIBRARY_PATH", env::join_paths(paths).unwrap()) };
1101    }
1102
1103    /// Restarts outside of the sandbox if currently running within it
1104    pub fn try_restart_to_host() {
1105        if let Some(flatpak_dir) = get_flatpak_dir() {
1106            let mut args = vec!["/usr/bin/flatpak-spawn".into(), "--host".into()];
1107            args.append(&mut get_xdg_env_args());
1108            args.push("--env=ZED_UPDATE_EXPLANATION=Please use flatpak to update omega".into());
1109            args.push(
1110                format!(
1111                    "--env={EXTRA_LIB_ENV_NAME}={}",
1112                    flatpak_dir.join("lib").to_str().unwrap()
1113                )
1114                .into(),
1115            );
1116            args.push(flatpak_dir.join("bin").join("zed").into());
1117
1118            let mut is_app_location_set = false;
1119            for arg in &env::args_os().collect::<Vec<_>>()[1..] {
1120                args.push(arg.clone());
1121                is_app_location_set |= arg == "--zed";
1122            }
1123
1124            if !is_app_location_set {
1125                args.push("--zed".into());
1126                args.push(flatpak_dir.join("libexec").join("omega-editor").into());
1127            }
1128
1129            let error = exec::execvp("/usr/bin/flatpak-spawn", args);
1130            eprintln!("failed restart cli on host: {:?}", error);
1131            process::exit(1);
1132        }
1133    }
1134
1135    pub fn set_bin_if_no_escape(mut args: super::Args) -> super::Args {
1136        if env::var(NO_ESCAPE_ENV_NAME).is_ok()
1137            && env::var("FLATPAK_ID").is_ok_and(|id| id.starts_with("com.openagents.omega"))
1138            && args.omega.is_none()
1139        {
1140            args.omega = Some("/app/libexec/omega-editor".into());
1141            unsafe {
1142                env::set_var(
1143                    "ZED_UPDATE_EXPLANATION",
1144                    "Please use flatpak to update Omega",
1145                )
1146            };
1147        }
1148        args
1149    }
1150
1151    fn get_flatpak_dir() -> Option<PathBuf> {
1152        if env::var(NO_ESCAPE_ENV_NAME).is_ok() {
1153            return None;
1154        }
1155
1156        if let Ok(flatpak_id) = env::var("FLATPAK_ID") {
1157            if !flatpak_id.starts_with("com.openagents.omega") {
1158                return None;
1159            }
1160
1161            let install_dir = Command::new("/usr/bin/flatpak-spawn")
1162                .arg("--host")
1163                .arg("flatpak")
1164                .arg("info")
1165                .arg("--show-location")
1166                .arg(flatpak_id)
1167                .output()
1168                .unwrap();
1169            let install_dir = PathBuf::from(String::from_utf8(install_dir.stdout).unwrap().trim());
1170            Some(install_dir.join("files"))
1171        } else {
1172            None
1173        }
1174    }
1175
1176    fn get_xdg_env_args() -> Vec<OsString> {
1177        let xdg_keys = [
1178            "XDG_DATA_HOME",
1179            "XDG_CONFIG_HOME",
1180            "XDG_CACHE_HOME",
1181            "XDG_STATE_HOME",
1182        ];
1183        env::vars()
1184            .filter(|(key, _)| xdg_keys.contains(&key.as_str()))
1185            .map(|(key, val)| format!("--env=FLATPAK_{}={}", key, val).into())
1186            .collect()
1187    }
1188}
1189
1190#[cfg(target_os = "windows")]
1191mod windows {
1192    use anyhow::Context;
1193    use release_channel::app_identifier;
1194    use windows::{
1195        Win32::{
1196            Foundation::{CloseHandle, ERROR_ALREADY_EXISTS, GENERIC_WRITE, GetLastError},
1197            Storage::FileSystem::{
1198                CreateFileW, FILE_FLAGS_AND_ATTRIBUTES, FILE_SHARE_MODE, OPEN_EXISTING, WriteFile,
1199            },
1200            System::Threading::CreateMutexW,
1201        },
1202        core::HSTRING,
1203    };
1204
1205    use crate::{Detect, InstalledApp};
1206    use std::io;
1207    use std::path::{Path, PathBuf};
1208    use std::process::{ExitStatus, Stdio};
1209
1210    fn check_single_instance() -> bool {
1211        let mutex = unsafe {
1212            CreateMutexW(
1213                None,
1214                false,
1215                &HSTRING::from(format!("{}-Instance-Mutex", app_identifier())),
1216            )
1217            .expect("Unable to create instance sync event")
1218        };
1219        let last_err = unsafe { GetLastError() };
1220        let _ = unsafe { CloseHandle(mutex) };
1221        last_err != ERROR_ALREADY_EXISTS
1222    }
1223
1224    struct App(PathBuf);
1225
1226    impl InstalledApp for App {
1227        fn app_version_string(&self) -> String {
1228            format!(
1229                "Omega {}{}{} – {}",
1230                if *release_channel::RELEASE_CHANNEL_NAME == "stable" {
1231                    "".to_string()
1232                } else {
1233                    format!("{} ", *release_channel::RELEASE_CHANNEL_NAME)
1234                },
1235                option_env!("RELEASE_VERSION").unwrap_or_default(),
1236                match option_env!("ZED_COMMIT_SHA") {
1237                    Some(commit_sha) => format!(" {commit_sha} "),
1238                    None => "".to_string(),
1239                },
1240                self.0.display(),
1241            )
1242        }
1243
1244        fn launch(&self, ipc_url: String, user_data_dir: Option<&str>) -> anyhow::Result<()> {
1245            if check_single_instance() {
1246                let mut cmd = std::process::Command::new(self.0.clone());
1247                cmd.arg(ipc_url);
1248                if let Some(dir) = user_data_dir {
1249                    cmd.arg("--user-data-dir").arg(dir);
1250                }
1251                cmd.stdin(Stdio::null())
1252                    .stdout(Stdio::null())
1253                    .stderr(Stdio::null());
1254                cmd.spawn()?;
1255            } else {
1256                unsafe {
1257                    let pipe = CreateFileW(
1258                        &HSTRING::from(format!("\\\\.\\pipe\\{}-Named-Pipe", app_identifier())),
1259                        GENERIC_WRITE.0,
1260                        FILE_SHARE_MODE::default(),
1261                        None,
1262                        OPEN_EXISTING,
1263                        FILE_FLAGS_AND_ATTRIBUTES::default(),
1264                        None,
1265                    )?;
1266                    let message = ipc_url.as_bytes();
1267                    let mut bytes_written = 0;
1268                    WriteFile(pipe, Some(message), Some(&mut bytes_written), None)?;
1269                    CloseHandle(pipe)?;
1270                }
1271            }
1272            Ok(())
1273        }
1274
1275        fn run_foreground(
1276            &self,
1277            ipc_url: String,
1278            user_data_dir: Option<&str>,
1279        ) -> io::Result<ExitStatus> {
1280            let mut cmd = std::process::Command::new(self.0.clone());
1281            cmd.arg(ipc_url).arg("--foreground");
1282            if let Some(dir) = user_data_dir {
1283                cmd.arg("--user-data-dir").arg(dir);
1284            }
1285            cmd.spawn()?.wait()
1286        }
1287
1288        fn path(&self) -> PathBuf {
1289            self.0.clone()
1290        }
1291
1292        fn installation_root(&self) -> PathBuf {
1293            // Windows has no `--uninstall`; the installer owns removal there.
1294            self.0.clone()
1295        }
1296    }
1297
1298    impl Detect {
1299        pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
1300            let path = if let Some(path) = path {
1301                path.to_path_buf().canonicalize()?
1302            } else {
1303                let cli = std::env::current_exe()?;
1304                let dir = cli.parent().context("no parent path for cli")?;
1305
1306                // ../Omega.exe is the standard, lib/omega is for MSYS2, ./omega.exe is for the target
1307                // directory in development builds.
1308                let possible_locations = [
1309                    "../Omega.exe",
1310                    "../lib/omega/omega-editor.exe",
1311                    "./omega.exe",
1312                ];
1313                possible_locations
1314                    .iter()
1315                    .find_map(|p| dir.join(p).canonicalize().ok().filter(|path| path != &cli))
1316                    .context(format!(
1317                        "could not find any of: {}",
1318                        possible_locations.join(", ")
1319                    ))?
1320            };
1321
1322            Ok(App(path))
1323        }
1324    }
1325}
1326
1327#[cfg(target_os = "macos")]
1328mod mac_os {
1329    use anyhow::{Context as _, Result};
1330    use core_foundation::{
1331        array::{CFArray, CFIndex},
1332        base::TCFType as _,
1333        string::kCFStringEncodingUTF8,
1334        url::{CFURL, CFURLCreateWithBytes},
1335    };
1336    use core_services::{
1337        LSLaunchURLSpec, LSOpenFromURLSpec, kLSLaunchDefaults, kLSLaunchDontSwitch,
1338    };
1339    use serde::Deserialize;
1340    use std::{
1341        ffi::OsStr,
1342        fs, io,
1343        path::{Path, PathBuf},
1344        process::{Command, ExitStatus},
1345        ptr,
1346    };
1347
1348    use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
1349
1350    use crate::{Detect, InstalledApp};
1351
1352    #[derive(Debug, Deserialize)]
1353    struct InfoPlist {
1354        #[serde(rename = "CFBundleShortVersionString")]
1355        bundle_short_version_string: String,
1356    }
1357
1358    enum Bundle {
1359        App {
1360            app_bundle: PathBuf,
1361            plist: InfoPlist,
1362        },
1363        LocalPath {
1364            executable: PathBuf,
1365        },
1366    }
1367
1368    fn locate_bundle() -> Result<PathBuf> {
1369        let cli_path = std::env::current_exe()?.canonicalize()?;
1370        let mut app_path = cli_path.clone();
1371        while app_path.extension() != Some(OsStr::new("app")) {
1372            anyhow::ensure!(
1373                app_path.pop(),
1374                "cannot find app bundle containing {cli_path:?}"
1375            );
1376        }
1377        Ok(app_path)
1378    }
1379
1380    impl Detect {
1381        pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
1382            let bundle_path = if let Some(bundle_path) = path {
1383                bundle_path
1384                    .canonicalize()
1385                    .with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))?
1386            } else {
1387                locate_bundle().context("bundle autodiscovery")?
1388            };
1389
1390            match bundle_path.extension().and_then(|ext| ext.to_str()) {
1391                Some("app") => {
1392                    let plist_path = bundle_path.join("Contents/Info.plist");
1393                    let plist =
1394                        plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
1395                            format!("Reading *.app bundle plist file at {plist_path:?}")
1396                        })?;
1397                    Ok(Bundle::App {
1398                        app_bundle: bundle_path,
1399                        plist,
1400                    })
1401                }
1402                _ => Ok(Bundle::LocalPath {
1403                    executable: bundle_path,
1404                }),
1405            }
1406        }
1407    }
1408
1409    impl InstalledApp for Bundle {
1410        fn app_version_string(&self) -> String {
1411            // The installation, which is what a person can point at in Finder.
1412            // This used to read `self.path()`, resolved to an inherent method
1413            // that returned the bundle while the trait method of the same name
1414            // returned the executable. That ambiguity is what omega#92 was.
1415            format!(
1416                "Omega {} – {}",
1417                self.version(),
1418                self.installation_root().display(),
1419            )
1420        }
1421
1422        fn launch(&self, url: String, user_data_dir: Option<&str>) -> anyhow::Result<()> {
1423            match self {
1424                Self::App { app_bundle, .. } => {
1425                    let app_path = app_bundle;
1426
1427                    let status = unsafe {
1428                        let app_url = CFURL::from_path(app_path, true)
1429                            .with_context(|| format!("invalid app path {app_path:?}"))?;
1430                        let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
1431                            ptr::null(),
1432                            url.as_ptr(),
1433                            url.len() as CFIndex,
1434                            kCFStringEncodingUTF8,
1435                            ptr::null(),
1436                        ));
1437                        // equivalent to: open zed-cli:... -a /Applications/Zed\ Preview.app
1438                        let urls_to_open =
1439                            CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
1440                        LSOpenFromURLSpec(
1441                            &LSLaunchURLSpec {
1442                                appURL: app_url.as_concrete_TypeRef(),
1443                                itemURLs: urls_to_open.as_concrete_TypeRef(),
1444                                passThruParams: ptr::null(),
1445                                launchFlags: kLSLaunchDefaults | kLSLaunchDontSwitch,
1446                                asyncRefCon: ptr::null_mut(),
1447                            },
1448                            ptr::null_mut(),
1449                        )
1450                    };
1451
1452                    anyhow::ensure!(
1453                        status == 0,
1454                        "cannot start app bundle {}",
1455                        self.app_version_string()
1456                    );
1457                }
1458
1459                Self::LocalPath { executable, .. } => {
1460                    let executable_parent = executable
1461                        .parent()
1462                        .with_context(|| format!("Executable {executable:?} path has no parent"))?;
1463                    let subprocess_stdout_file = fs::File::create(
1464                        executable_parent.join("zed_dev.log"),
1465                    )
1466                    .with_context(|| format!("Log file creation in {executable_parent:?}"))?;
1467                    let subprocess_stdin_file =
1468                        subprocess_stdout_file.try_clone().with_context(|| {
1469                            format!("Cloning descriptor for file {subprocess_stdout_file:?}")
1470                        })?;
1471                    let mut command = std::process::Command::new(executable);
1472                    command.env(FORCE_CLI_MODE_ENV_VAR_NAME, "");
1473                    if let Some(dir) = user_data_dir {
1474                        command.arg("--user-data-dir").arg(dir);
1475                    }
1476                    command
1477                        .stderr(subprocess_stdout_file)
1478                        .stdout(subprocess_stdin_file)
1479                        .arg(url);
1480
1481                    command
1482                        .spawn()
1483                        .with_context(|| format!("Spawning {command:?}"))?;
1484                }
1485            }
1486
1487            Ok(())
1488        }
1489
1490        fn run_foreground(
1491            &self,
1492            ipc_url: String,
1493            user_data_dir: Option<&str>,
1494        ) -> io::Result<ExitStatus> {
1495            let path = match self {
1496                Bundle::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/omega"),
1497                Bundle::LocalPath { executable, .. } => executable.clone(),
1498            };
1499
1500            let mut cmd = std::process::Command::new(path);
1501            cmd.arg(ipc_url);
1502            if let Some(dir) = user_data_dir {
1503                cmd.arg("--user-data-dir").arg(dir);
1504            }
1505            cmd.status()
1506        }
1507
1508        fn path(&self) -> PathBuf {
1509            match self {
1510                Bundle::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/omega"),
1511                Bundle::LocalPath { executable, .. } => executable.clone(),
1512            }
1513        }
1514
1515        fn installation_root(&self) -> PathBuf {
1516            match self {
1517                // The bundle, not the executable inside it. Everything Omega
1518                // installs on macOS lives under this directory.
1519                Bundle::App { app_bundle, .. } => app_bundle.clone(),
1520                Bundle::LocalPath { executable, .. } => executable.clone(),
1521            }
1522        }
1523    }
1524
1525    impl Bundle {
1526        fn version(&self) -> String {
1527            match self {
1528                Self::App { plist, .. } => plist.bundle_short_version_string.clone(),
1529                Self::LocalPath { .. } => "<development>".to_string(),
1530            }
1531        }
1532    }
1533
1534    pub(super) fn spawn_channel_cli(
1535        channel: release_channel::ReleaseChannel,
1536        leftover_args: Vec<String>,
1537    ) -> Result<()> {
1538        use anyhow::bail;
1539
1540        let app_path_prompt = format!(
1541            "POSIX path of (path to application \"{}\")",
1542            channel.display_name()
1543        );
1544        let app_path_output = Command::new("osascript")
1545            .arg("-e")
1546            .arg(&app_path_prompt)
1547            .output()?;
1548        if !app_path_output.status.success() {
1549            bail!(
1550                "Could not determine app path for {}",
1551                channel.display_name()
1552            );
1553        }
1554        let app_path = String::from_utf8(app_path_output.stdout)?.trim().to_owned();
1555        let cli_path = format!("{app_path}/Contents/MacOS/cli");
1556        Command::new(cli_path).args(leftover_args).spawn()?;
1557        Ok(())
1558    }
1559}
1560
Served at tenant.openagents/omega Member data and write actions are omitted.