Skip to main content

zenith_cli/
selfupdate.rs

1//! `zenith update` — self-update by piping the published install script to `sh`.
2//!
3//! This is the system-facing edge of the CLI: it spawns processes and mutates
4//! the install directory. It therefore lives outside `commands/`, which is pure
5//! and never touches the environment. `lib.rs` owns the stdout/stderr edge; this
6//! module owns the process plumbing and returns a plain `Result`.
7//!
8//! Zenith has no project domain, so the install script is fetched straight from
9//! GitHub's raw endpoint on the default branch — the same script the
10//! `curl … | sh` one-liner uses.
11
12use std::process::{Command, Stdio};
13
14/// Raw URL of the install script on the default branch.
15const INSTALL_SCRIPT_URL: &str =
16    "https://raw.githubusercontent.com/zenitheditor/zenith/main/scripts/install.sh";
17
18/// Download and run the install script, forwarding the channel/version flags.
19///
20/// Mirrors the flags accepted by `scripts/install.sh`: `--pre` for the latest
21/// prerelease, `--version <TAG>` for an exact version. With neither, the latest
22/// stable release is installed.
23pub fn run(pre: bool, version: Option<&str>) -> Result<(), String> {
24    let mut sh_args: Vec<String> = vec!["-s".to_string(), "--".to_string()];
25    if pre {
26        sh_args.push("--pre".to_string());
27    }
28    if let Some(v) = version {
29        let tag = if v.starts_with('v') {
30            v.to_string()
31        } else {
32            format!("v{v}")
33        };
34        sh_args.push("--version".to_string());
35        sh_args.push(tag);
36    }
37
38    let (dl_cmd, dl_args): (&str, &[&str]) = if which("curl") {
39        ("curl", &["-fsSL", INSTALL_SCRIPT_URL])
40    } else if which("wget") {
41        ("wget", &["-qO-", INSTALL_SCRIPT_URL])
42    } else {
43        return Err("curl or wget is required for self-update".to_string());
44    };
45
46    let mut downloader = Command::new(dl_cmd)
47        .args(dl_args)
48        .stdout(Stdio::piped())
49        .spawn()
50        .map_err(|e| format!("failed to start {dl_cmd}: {e}"))?;
51
52    let pipe = downloader
53        .stdout
54        .take()
55        .ok_or_else(|| "failed to capture the download stream".to_string())?;
56
57    let status = Command::new("sh")
58        .args(&sh_args)
59        .stdin(pipe)
60        .status()
61        .map_err(|e| format!("failed to run the install script: {e}"))?;
62
63    // Reap the downloader so it does not linger as a zombie.
64    let _ = downloader.wait();
65
66    if status.success() {
67        Ok(())
68    } else {
69        Err("the install script failed".to_string())
70    }
71}
72
73/// Return `true` if `cmd` is resolvable on `PATH` (via the POSIX `command -v`).
74fn which(cmd: &str) -> bool {
75    Command::new("sh")
76        .arg("-c")
77        .arg(format!("command -v {cmd}"))
78        .stdout(Stdio::null())
79        .stderr(Stdio::null())
80        .status()
81        .map(|s| s.success())
82        .unwrap_or(false)
83}