mi6_cli/commands/
upgrade.rs

1//! Self-upgrade command for mi6.
2//!
3//! Detects installation method and uses the appropriate upgrade strategy:
4//! - Homebrew: `brew upgrade mi6`
5//! - Cargo: `cargo install mi6`
6//! - Standalone: Uses `self_update` crate to download from GitHub releases
7
8use std::process::Command;
9
10use anyhow::{Context, Result};
11
12use crate::display::confirm_stdout;
13
14/// How mi6 was installed
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum InstallMethod {
17    /// Installed via Homebrew
18    Homebrew,
19    /// Installed via `cargo install`
20    Cargo,
21    /// Standalone binary (e.g., downloaded from GitHub releases)
22    Standalone,
23}
24
25impl InstallMethod {
26    /// Detect the installation method based on the executable path
27    pub fn detect() -> Result<Self> {
28        let exe_path = std::env::current_exe().context("failed to get current executable path")?;
29        let path_str = exe_path.to_string_lossy();
30
31        if path_str.contains("/Cellar/") || path_str.contains("/homebrew/") {
32            Ok(Self::Homebrew)
33        } else if path_str.contains("/.cargo/bin/") {
34            Ok(Self::Cargo)
35        } else {
36            Ok(Self::Standalone)
37        }
38    }
39
40    /// Get a human-readable name for the install method
41    pub const fn name(self) -> &'static str {
42        match self {
43            Self::Homebrew => "Homebrew",
44            Self::Cargo => "Cargo",
45            Self::Standalone => "Standalone",
46        }
47    }
48}
49
50/// Options for the upgrade command
51pub struct UpgradeOptions {
52    /// Target version (only for Cargo installs)
53    pub version: Option<String>,
54    /// Skip confirmation prompt
55    pub yes: bool,
56    /// Check for updates without installing
57    pub dry_run: bool,
58}
59
60/// Run the upgrade command
61pub fn run_upgrade(options: UpgradeOptions) -> Result<()> {
62    let current_version = env!("CARGO_PKG_VERSION");
63    let method = InstallMethod::detect()?;
64
65    println!("mi6 v{}", current_version);
66    println!("Detected: {} install", method.name());
67    println!();
68
69    // Warn if --version is used with non-Cargo install methods
70    if options.version.is_some() && method != InstallMethod::Cargo {
71        println!(
72            "Warning: --version is only supported for Cargo installs (ignored for {})",
73            method.name()
74        );
75        println!();
76    }
77
78    // Handle dry-run mode
79    if options.dry_run {
80        return check_for_updates(current_version, method);
81    }
82
83    match method {
84        InstallMethod::Homebrew => upgrade_homebrew(options.yes)?,
85        InstallMethod::Cargo => upgrade_cargo(options.version.as_deref(), options.yes)?,
86        InstallMethod::Standalone => upgrade_standalone(current_version, options.yes)?,
87    }
88
89    println!();
90    println!("Done! Run `mi6 --version` to verify.");
91
92    Ok(())
93}
94
95/// Check for available updates without installing (dry-run mode)
96fn check_for_updates(current_version: &str, method: InstallMethod) -> Result<()> {
97    match method {
98        InstallMethod::Standalone => {
99            println!("Checking GitHub for updates...");
100            println!();
101
102            let update = self_update::backends::github::Update::configure()
103                .repo_owner("paradigmxyz")
104                .repo_name("mi6")
105                .bin_name("mi6")
106                .current_version(current_version)
107                .build()
108                .context("failed to configure self-updater")?;
109
110            let latest = update
111                .get_latest_release()
112                .context("failed to check for updates")?;
113
114            let latest_version = latest.version.trim_start_matches('v');
115
116            if latest_version == current_version {
117                println!("Already at the latest version (v{}).", current_version);
118            } else {
119                println!(
120                    "Update available: v{} -> v{}",
121                    current_version, latest_version
122                );
123                println!();
124                println!("Run `mi6 upgrade` to install.");
125            }
126        }
127        InstallMethod::Homebrew => {
128            println!("To check for Homebrew updates, run:");
129            println!("  brew outdated mi6");
130        }
131        InstallMethod::Cargo => {
132            println!("To check for Cargo updates, run:");
133            println!("  cargo search mi6");
134        }
135    }
136
137    Ok(())
138}
139
140fn upgrade_homebrew(skip_confirm: bool) -> Result<()> {
141    if !skip_confirm {
142        println!("This will run: brew upgrade mi6");
143        if !confirm_stdout("Proceed?")? {
144            println!("Aborted.");
145            return Ok(());
146        }
147        println!();
148    }
149
150    println!("Running: brew upgrade mi6");
151    println!();
152
153    let status = Command::new("brew")
154        .args(["upgrade", "mi6"])
155        .status()
156        .context("failed to run brew upgrade")?;
157
158    if !status.success() {
159        // Homebrew returns non-zero if package is already at latest version
160        println!();
161        println!("Note: brew upgrade exited with non-zero status.");
162        println!("This usually means mi6 is already at the latest version.");
163        println!("If not, try: brew reinstall mi6");
164    }
165
166    Ok(())
167}
168
169fn upgrade_cargo(version: Option<&str>, skip_confirm: bool) -> Result<()> {
170    let mut args = vec!["install", "mi6"];
171    if let Some(v) = version {
172        args.extend(["--version", v]);
173    }
174
175    if !skip_confirm {
176        println!("This will run: cargo {}", args.join(" "));
177        if !confirm_stdout("Proceed?")? {
178            println!("Aborted.");
179            return Ok(());
180        }
181        println!();
182    }
183
184    println!("Running: cargo {}", args.join(" "));
185    println!();
186
187    let status = Command::new("cargo")
188        .args(&args)
189        .status()
190        .context("failed to run cargo install")?;
191
192    if !status.success() {
193        anyhow::bail!("cargo install failed with exit code: {:?}", status.code());
194    }
195
196    Ok(())
197}
198
199fn upgrade_standalone(current_version: &str, skip_confirm: bool) -> Result<()> {
200    println!("Checking GitHub for updates...");
201    println!();
202
203    let update = self_update::backends::github::Update::configure()
204        .repo_owner("paradigmxyz")
205        .repo_name("mi6")
206        .bin_name("mi6")
207        .current_version(current_version)
208        .build()
209        .context("failed to configure self-updater")?;
210
211    // Check for update first
212    let latest = update
213        .get_latest_release()
214        .context("failed to check for updates")?;
215
216    let latest_version = latest.version.trim_start_matches('v');
217
218    if latest_version == current_version {
219        println!("Already at the latest version (v{}).", current_version);
220        return Ok(());
221    }
222
223    println!(
224        "New version available: v{} -> v{}",
225        current_version, latest_version
226    );
227
228    if !skip_confirm && !confirm_stdout("Proceed with update?")? {
229        println!("Aborted.");
230        return Ok(());
231    }
232    println!();
233
234    println!("Downloading and installing v{}...", latest_version);
235
236    // Create a new update with show_progress for the actual download
237    let status = self_update::backends::github::Update::configure()
238        .repo_owner("paradigmxyz")
239        .repo_name("mi6")
240        .bin_name("mi6")
241        .current_version(current_version)
242        .show_download_progress(true)
243        .build()
244        .context("failed to configure self-updater")?
245        .update()
246        .context("failed to perform update")?;
247
248    println!();
249    println!("Updated to v{}!", status.version());
250
251    Ok(())
252}