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 (registry): `cargo install mi6`
6//! - Cargo (path): `cargo install --path <original-path>`
7//! - Standalone: Uses `self_update` crate to download from GitHub releases
8//!
9//! Supports switching between installation methods with automatic rollback on failure.
10
11use std::path::PathBuf;
12use std::process::Command;
13
14use anyhow::{Context, Result, bail};
15
16use crate::args::UpgradeMethod;
17use crate::display::{bold_green, bold_white, confirm_stdout};
18
19/// How mi6 was installed
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum InstallMethod {
22    /// Installed via Homebrew
23    Homebrew,
24    /// Installed via `cargo install mi6` (from crates.io registry)
25    CargoRegistry,
26    /// Installed via `cargo install --path <path>` (from local source)
27    CargoPath(PathBuf),
28    /// Standalone binary (e.g., downloaded from GitHub releases or install.sh)
29    Standalone,
30}
31
32impl InstallMethod {
33    /// Detect the installation method based on the executable path and cargo metadata
34    pub fn detect() -> Result<Self> {
35        let exe_path = std::env::current_exe().context("failed to get current executable path")?;
36        let path_str = exe_path.to_string_lossy();
37
38        if path_str.contains("/Cellar/") || path_str.contains("/homebrew/") {
39            return Ok(Self::Homebrew);
40        }
41
42        if path_str.contains("/.cargo/bin/") {
43            // Use `cargo install --list` to determine if installed from registry or path
44            if let Some(method) = Self::detect_cargo_source()? {
45                return Ok(method);
46            }
47            // Fallback to registry if we can't determine
48            return Ok(Self::CargoRegistry);
49        }
50
51        Ok(Self::Standalone)
52    }
53
54    /// Use `cargo install --list` to determine the install source
55    ///
56    /// Output format:
57    /// - Registry install: `mi6 v0.2.0:`
58    /// - Path install: `mi6 v0.2.0 (/path/to/crate):`
59    fn detect_cargo_source() -> Result<Option<Self>> {
60        let output = Command::new("cargo")
61            .args(["install", "--list"])
62            .output()
63            .context("failed to run cargo install --list")?;
64
65        if !output.status.success() {
66            return Ok(None);
67        }
68
69        let stdout = String::from_utf8_lossy(&output.stdout);
70
71        // Find the line starting with "mi6 v"
72        for line in stdout.lines() {
73            if !line.starts_with("mi6 v") {
74                continue;
75            }
76
77            // Check if there's a path in parentheses: "mi6 v0.2.0 (/path/to/crate):"
78            if let Some(start) = line.find(" (")
79                && let Some(end) = line.rfind("):")
80            {
81                let path_str = &line[start + 2..end];
82                return Ok(Some(Self::CargoPath(PathBuf::from(path_str))));
83            }
84
85            // No path means registry install: "mi6 v0.2.0:"
86            return Ok(Some(Self::CargoRegistry));
87        }
88
89        Ok(None)
90    }
91
92    /// Get a human-readable name for the install method
93    pub fn name(&self) -> &'static str {
94        match self {
95            Self::Homebrew => "Homebrew",
96            Self::CargoRegistry => "Cargo (crates.io)",
97            Self::CargoPath(_) => "Cargo (source)",
98            Self::Standalone => "Standalone",
99        }
100    }
101
102    /// Convert to UpgradeMethod (for comparison purposes)
103    pub fn to_upgrade_method(&self) -> UpgradeMethod {
104        match self {
105            Self::Homebrew => UpgradeMethod::Brew,
106            Self::CargoRegistry => UpgradeMethod::CargoCrates,
107            Self::CargoPath(_) => UpgradeMethod::CargoSource,
108            Self::Standalone => UpgradeMethod::Github,
109        }
110    }
111
112    /// Create from UpgradeMethod with optional path for CargoSource
113    pub fn from_upgrade_method(method: UpgradeMethod, path: Option<PathBuf>) -> Result<Self> {
114        match method {
115            UpgradeMethod::Brew => Ok(Self::Homebrew),
116            UpgradeMethod::CargoCrates => Ok(Self::CargoRegistry),
117            UpgradeMethod::Github => Ok(Self::Standalone),
118            UpgradeMethod::CargoSource => {
119                let source_path = path.ok_or_else(|| {
120                    anyhow::anyhow!(
121                        "--source-path is required when using --method cargo_source\n\
122                         Example: mi6 upgrade --method cargo_source --source-path /path/to/mi6"
123                    )
124                })?;
125                Ok(Self::CargoPath(source_path))
126            }
127        }
128    }
129
130    /// Check if prerequisites for this installation method are met
131    pub fn check_prerequisites(&self) -> Result<()> {
132        match self {
133            Self::Homebrew => {
134                let output = Command::new("brew")
135                    .arg("--version")
136                    .output()
137                    .context("brew is not installed or not in PATH")?;
138                if !output.status.success() {
139                    bail!("brew is not working properly");
140                }
141                Ok(())
142            }
143            Self::CargoRegistry | Self::CargoPath(_) => {
144                let output = Command::new("cargo")
145                    .arg("--version")
146                    .output()
147                    .context("cargo is not installed or not in PATH")?;
148                if !output.status.success() {
149                    bail!("cargo is not working properly");
150                }
151                // For CargoPath, check that the source directory exists and is a valid Rust project
152                if let Self::CargoPath(path) = self {
153                    if !path.exists() {
154                        bail!(
155                            "Source directory does not exist: {}\n\
156                             Please provide a valid path to the mi6 source.",
157                            path.display()
158                        );
159                    }
160                    let cargo_toml = path.join("Cargo.toml");
161                    if !cargo_toml.exists() {
162                        bail!(
163                            "Not a valid Rust project (no Cargo.toml): {}\n\
164                             Please provide a path to the mi6 source directory.",
165                            path.display()
166                        );
167                    }
168                }
169                Ok(())
170            }
171            Self::Standalone => {
172                // No prerequisites for standalone - just needs network access
173                Ok(())
174            }
175        }
176    }
177}
178
179/// Options for the upgrade command
180pub struct UpgradeOptions {
181    /// Target version (only for Cargo registry installs)
182    pub version: Option<String>,
183    /// Skip confirmation prompt
184    pub yes: bool,
185    /// Check for updates without installing
186    pub dry_run: bool,
187    /// Override installation method
188    pub method: Option<UpgradeMethod>,
189    /// Source path for cargo_source method
190    pub source_path: Option<PathBuf>,
191}
192
193/// Run the upgrade command
194pub fn run_upgrade(options: UpgradeOptions) -> Result<()> {
195    let current_version = env!("CARGO_PKG_VERSION");
196    let current_method = InstallMethod::detect()?;
197
198    println!("mi6 v{}", current_version);
199    println!();
200    println!(
201        "{} {} install",
202        bold_green("Detected:"),
203        current_method.name()
204    );
205    println!();
206
207    // Determine target method (either specified or current)
208    let target_method = if let Some(method) = options.method {
209        // If switching to cargo_source and no path provided, try to use current path
210        let path = options.source_path.clone().or_else(|| {
211            if let InstallMethod::CargoPath(p) = &current_method {
212                Some(p.clone())
213            } else {
214                None
215            }
216        });
217        // Canonicalize the path to avoid issues with relative paths
218        let path = path.map(|p| p.canonicalize().unwrap_or(p));
219        InstallMethod::from_upgrade_method(method, path)?
220    } else {
221        current_method.clone()
222    };
223
224    let is_switching = current_method.to_upgrade_method() != target_method.to_upgrade_method();
225
226    // Warn if --version is used with non-registry cargo install methods
227    if options.version.is_some() && !matches!(target_method, InstallMethod::CargoRegistry) {
228        println!(
229            "Warning: --version is only supported for Cargo registry installs (ignored for {})",
230            target_method.name()
231        );
232        println!();
233    }
234
235    // Handle dry-run mode
236    if options.dry_run {
237        if is_switching {
238            println!(
239                "Would switch from {} to {}",
240                current_method.name(),
241                target_method.name()
242            );
243            println!();
244
245            // Check prerequisites even in dry-run to warn early
246            print!("Checking prerequisites for {}... ", target_method.name());
247            match target_method.check_prerequisites() {
248                Ok(()) => println!("OK"),
249                Err(e) => {
250                    println!("FAILED");
251                    println!();
252                    println!("Warning: Switch would fail: {}", e);
253                    println!();
254                }
255            }
256        }
257        return check_for_updates(current_version, &target_method);
258    }
259
260    // If switching methods, go through the switch flow
261    if is_switching {
262        return switch_method(&current_method, &target_method, &options);
263    }
264
265    // Same method upgrade
266    let completed = match &target_method {
267        InstallMethod::Homebrew => upgrade_homebrew(options.yes)?,
268        InstallMethod::CargoRegistry => {
269            upgrade_cargo_registry(options.version.as_deref(), options.yes)?
270        }
271        InstallMethod::CargoPath(path) => upgrade_cargo_path(path, options.yes)?,
272        InstallMethod::Standalone => upgrade_standalone(current_version, options.yes)?,
273    };
274
275    if completed {
276        println!();
277        println!("Done! Run `mi6 --version` to verify.");
278    }
279
280    Ok(())
281}
282
283/// Switch from one installation method to another.
284///
285/// This function ensures the mi6 binary is always available during the switch
286/// by installing the new method BEFORE uninstalling the old one. This is critical
287/// for live sessions that have hooks calling `mi6 ingest event`.
288///
289/// For cargo-to-cargo switches (same binary location), the install atomically
290/// overwrites so no separate uninstall is needed.
291fn switch_method(from: &InstallMethod, to: &InstallMethod, options: &UpgradeOptions) -> Result<()> {
292    println!(
293        "Switching installation method: {} -> {}",
294        from.name(),
295        to.name()
296    );
297    println!();
298
299    // Step 1: Check prerequisites for the new method BEFORE any changes
300    println!("Checking prerequisites for {}...", to.name());
301    to.check_prerequisites()
302        .with_context(|| format!("Cannot switch to {}: prerequisites not met", to.name()))?;
303    println!("Prerequisites OK.");
304    println!();
305
306    // Determine if this is a cargo-to-cargo switch (same binary location, atomic overwrite)
307    let is_cargo_to_cargo = matches!(
308        (from, to),
309        (
310            InstallMethod::CargoRegistry | InstallMethod::CargoPath(_),
311            InstallMethod::CargoRegistry | InstallMethod::CargoPath(_)
312        )
313    );
314
315    // Step 2: Confirm with user
316    if !options.yes {
317        println!("This will:");
318        if is_cargo_to_cargo {
319            println!("  Install mi6 via {} (overwrites existing)", to.name());
320        } else {
321            println!("  1. Install mi6 via {}", to.name());
322            println!("  2. Uninstall mi6 via {} (binary only)", from.name());
323        }
324        println!();
325        println!("Note: Your mi6 data (database, config, themes) will be preserved.");
326        println!();
327        if !confirm_stdout("Proceed with method switch?")? {
328            println!("Aborted.");
329            return Ok(());
330        }
331        println!();
332    }
333
334    if is_cargo_to_cargo {
335        // Cargo-to-cargo: atomic overwrite, use --force to ensure replacement
336        println!("Installing via {} (atomic overwrite)...", to.name());
337        install_binary_forced(to, options.version.as_deref())
338            .with_context(|| format!("Failed to install via {}", to.name()))?;
339    } else {
340        // Different locations: install new first, then uninstall old
341        // This ensures mi6 is always available for live sessions
342
343        // Step 3: Install new method first (old binary still available)
344        println!("Installing via {}...", to.name());
345        if let Err(install_err) = install_binary(to, options.version.as_deref()) {
346            // Install failed, but old binary is still there - no rollback needed
347            bail!(
348                "Failed to install via {}: {}\n\n\
349                 Your current installation ({}) is unchanged.",
350                to.name(),
351                install_err,
352                from.name()
353            );
354        }
355        println!("Install complete.");
356        println!();
357
358        // Step 4: Now safe to uninstall old method (new binary is ready)
359        println!("Removing old installation ({})...", from.name());
360        if let Err(uninstall_err) = uninstall_binary(from) {
361            // New binary is installed, but old one couldn't be removed
362            // This is not critical - warn user but don't fail
363            eprintln!();
364            eprintln!(
365                "WARNING: Could not remove old {} installation: {}",
366                from.name(),
367                uninstall_err
368            );
369            eprintln!();
370            eprintln!("You may have two mi6 binaries installed.");
371            eprintln!("Run `which mi6` to verify you're using the correct binary.");
372            eprintln!();
373        } else {
374            println!("Old installation removed.");
375            println!();
376        }
377    }
378
379    println!(
380        "Done! Successfully switched from {} to {}.",
381        from.name(),
382        to.name()
383    );
384    println!("Run `mi6 --version` to verify.");
385
386    Ok(())
387}
388
389/// Uninstall the mi6 binary using the specified method (binary only, preserves data).
390fn uninstall_binary(method: &InstallMethod) -> Result<()> {
391    match method {
392        InstallMethod::Homebrew => {
393            let status = Command::new("brew")
394                .args(["uninstall", "mi6"])
395                .status()
396                .context("failed to run brew uninstall")?;
397            if !status.success() {
398                bail!("brew uninstall failed with exit code: {:?}", status.code());
399            }
400        }
401        InstallMethod::CargoRegistry | InstallMethod::CargoPath(_) => {
402            let status = Command::new("cargo")
403                .args(["uninstall", "mi6"])
404                .status()
405                .context("failed to run cargo uninstall")?;
406            if !status.success() {
407                bail!("cargo uninstall failed with exit code: {:?}", status.code());
408            }
409        }
410        InstallMethod::Standalone => {
411            let exe_path =
412                std::env::current_exe().context("failed to get current executable path")?;
413            std::fs::remove_file(&exe_path)
414                .with_context(|| format!("failed to delete {}", exe_path.display()))?;
415        }
416    }
417    Ok(())
418}
419
420/// Install the mi6 binary using the specified method.
421fn install_binary(method: &InstallMethod, version: Option<&str>) -> Result<()> {
422    install_binary_impl(method, version, false)
423}
424
425/// Install the mi6 binary with --force flag (for cargo-to-cargo switches).
426fn install_binary_forced(method: &InstallMethod, version: Option<&str>) -> Result<()> {
427    install_binary_impl(method, version, true)
428}
429
430/// Internal implementation of binary installation.
431fn install_binary_impl(method: &InstallMethod, version: Option<&str>, force: bool) -> Result<()> {
432    match method {
433        InstallMethod::Homebrew => {
434            let status = Command::new("brew")
435                .args(["install", "mi6"])
436                .status()
437                .context("failed to run brew install")?;
438            if !status.success() {
439                bail!("brew install failed with exit code: {:?}", status.code());
440            }
441        }
442        InstallMethod::CargoRegistry => {
443            let mut args = vec!["install", "mi6"];
444            if force {
445                args.push("--force");
446            }
447            if let Some(v) = version {
448                args.extend(["--version", v]);
449            }
450            let status = Command::new("cargo")
451                .args(&args)
452                .status()
453                .context("failed to run cargo install")?;
454            if !status.success() {
455                bail!("cargo install failed with exit code: {:?}", status.code());
456            }
457        }
458        InstallMethod::CargoPath(path) => {
459            let path_str = path.to_string_lossy();
460            let mut args = vec!["install", "--path", path_str.as_ref()];
461            if force {
462                args.push("--force");
463            }
464            let status = Command::new("cargo")
465                .args(&args)
466                .status()
467                .context("failed to run cargo install --path")?;
468            if !status.success() {
469                bail!(
470                    "cargo install --path failed with exit code: {:?}",
471                    status.code()
472                );
473            }
474        }
475        InstallMethod::Standalone => {
476            // When switching TO standalone, we must install to a DIFFERENT location
477            // than the current binary. By default, self_update replaces current_exe(),
478            // which would be the brew/cargo location - then uninstall would delete it!
479            //
480            // Install to /usr/local/bin (standard location for standalone binaries)
481            let install_dir = PathBuf::from("/usr/local/bin");
482
483            // Ensure the directory exists and is writable
484            if !install_dir.exists() {
485                std::fs::create_dir_all(&install_dir).with_context(|| {
486                    format!(
487                        "Cannot create install directory: {}\n\
488                         You may need to run with sudo or choose a different installation method.",
489                        install_dir.display()
490                    )
491                })?;
492            }
493
494            // Use self_update to download from GitHub releases
495            // Use "0.0.0" as current version to force download even if versions match.
496            let status = self_update::backends::github::Update::configure()
497                .repo_owner("paradigmxyz")
498                .repo_name("mi6")
499                .bin_name("mi6")
500                .bin_install_path(&install_dir)
501                .current_version("0.0.0") // Force download
502                .show_download_progress(true)
503                .build()
504                .context("failed to configure self-updater")?
505                .update()
506                .context("failed to download from GitHub releases")?;
507
508            // Verify the download actually happened
509            if !status.updated() {
510                bail!(
511                    "GitHub download did not produce a binary. \
512                     This may indicate a problem with the release assets."
513                );
514            }
515
516            // Remind user about PATH if /usr/local/bin might not be in it
517            let installed_path = install_dir.join("mi6");
518            println!();
519            println!("Installed to: {}", installed_path.display());
520            println!("Ensure /usr/local/bin is in your PATH.");
521        }
522    }
523    Ok(())
524}
525
526/// Check for available updates without installing (dry-run mode)
527fn check_for_updates(current_version: &str, method: &InstallMethod) -> Result<()> {
528    match method {
529        InstallMethod::Standalone => {
530            println!("Checking GitHub for updates...");
531            println!();
532
533            let update = self_update::backends::github::Update::configure()
534                .repo_owner("paradigmxyz")
535                .repo_name("mi6")
536                .bin_name("mi6")
537                .current_version(current_version)
538                .build()
539                .context("failed to configure self-updater")?;
540
541            let latest = update
542                .get_latest_release()
543                .context("failed to check for updates")?;
544
545            let latest_version = latest.version.trim_start_matches('v');
546
547            if latest_version == current_version {
548                println!("Already at the latest version (v{}).", current_version);
549            } else {
550                println!(
551                    "Update available: v{} -> v{}",
552                    current_version, latest_version
553                );
554                println!();
555                println!("Run `mi6 upgrade` to install.");
556            }
557        }
558        InstallMethod::Homebrew => {
559            println!("To check for Homebrew updates, run:");
560            println!("  brew outdated mi6");
561        }
562        InstallMethod::CargoRegistry => {
563            println!("To check for Cargo updates, run:");
564            println!("  cargo search mi6");
565        }
566        InstallMethod::CargoPath(path) => {
567            println!("Installed from source at: {}", path.display());
568            println!();
569            println!("To check for updates, pull latest changes and reinstall:");
570            println!("  mi6 upgrade");
571        }
572    }
573
574    Ok(())
575}
576
577fn upgrade_homebrew(skip_confirm: bool) -> Result<bool> {
578    if !skip_confirm {
579        println!("This will run: brew upgrade mi6");
580        if !confirm_stdout("Proceed?")? {
581            println!("Aborted.");
582            return Ok(false);
583        }
584        println!();
585    }
586
587    println!("Running: brew upgrade mi6");
588    println!();
589
590    let status = Command::new("brew")
591        .args(["upgrade", "mi6"])
592        .status()
593        .context("failed to run brew upgrade")?;
594
595    if !status.success() {
596        // Homebrew returns non-zero if package is already at latest version
597        println!();
598        println!("Note: brew upgrade exited with non-zero status.");
599        println!("This usually means mi6 is already at the latest version.");
600        println!("If not, try: brew reinstall mi6");
601    }
602
603    Ok(true)
604}
605
606fn upgrade_cargo_registry(version: Option<&str>, skip_confirm: bool) -> Result<bool> {
607    let mut args = vec!["install", "mi6"];
608    if let Some(v) = version {
609        args.extend(["--version", v]);
610    }
611
612    if !skip_confirm {
613        println!("This will run: cargo {}", args.join(" "));
614        if !confirm_stdout("Proceed?")? {
615            println!("Aborted.");
616            return Ok(false);
617        }
618        println!();
619    }
620
621    println!("Running: cargo {}", args.join(" "));
622    println!();
623
624    let status = Command::new("cargo")
625        .args(&args)
626        .status()
627        .context("failed to run cargo install")?;
628
629    if !status.success() {
630        anyhow::bail!("cargo install failed with exit code: {:?}", status.code());
631    }
632
633    Ok(true)
634}
635
636fn upgrade_cargo_path(path: &std::path::Path, skip_confirm: bool) -> Result<bool> {
637    // Verify the path still exists
638    if !path.exists() {
639        anyhow::bail!(
640            "Source directory no longer exists: {}\n\
641             You may need to reinstall mi6 or update the source location.",
642            path.display()
643        );
644    }
645
646    // Find the workspace root (look for Cargo.toml with [workspace])
647    let workspace_root = find_workspace_root(path)?;
648
649    // Determine the git pull target - if we're in a worktree, pull from main worktree instead
650    let pull_target = workspace_root.as_ref().and_then(|root| {
651        let git_dir = root.join(".git");
652        if git_dir.exists() {
653            Some(find_main_worktree(root).unwrap_or_else(|| root.clone()))
654        } else {
655            None
656        }
657    });
658
659    let path_str = path.to_string_lossy();
660    let args = vec!["install", "--path", path_str.as_ref()];
661
662    if !skip_confirm {
663        println!("{}", bold_green("Upgrade process:"));
664        if let Some(ref target) = pull_target {
665            println!(
666                "{} Pull latest changes ({})",
667                bold_green("1."),
668                bold_white(&format!("git pull -C {}", target.display()))
669            );
670            println!(
671                "{} Run cargo install ({})",
672                bold_green("2."),
673                bold_white(&format!("cargo {}", args.join(" ")))
674            );
675        } else {
676            println!(
677                "Run cargo install ({})",
678                bold_white(&format!("cargo {}", args.join(" ")))
679            );
680        }
681        println!();
682        if !confirm_stdout("Proceed?")? {
683            println!("Aborted.");
684            return Ok(false);
685        }
686        println!();
687    }
688
689    // Pull latest changes if we have a git directory
690    if let Some(ref target) = pull_target {
691        println!("Pulling latest changes...");
692        let target_str = target.to_string_lossy();
693
694        let status = Command::new("git")
695            .args(["-C", target_str.as_ref(), "pull"])
696            .status()
697            .context("failed to run git pull")?;
698
699        if !status.success() {
700            anyhow::bail!("git pull failed. Please resolve any conflicts or issues and try again.");
701        }
702        println!();
703    }
704
705    println!("Running: cargo {}", args.join(" "));
706    println!();
707
708    let status = Command::new("cargo")
709        .args(&args)
710        .status()
711        .context("failed to run cargo install --path")?;
712
713    if !status.success() {
714        anyhow::bail!("cargo install failed with exit code: {:?}", status.code());
715    }
716
717    Ok(true)
718}
719
720/// Find the workspace root by looking for Cargo.toml with [workspace] section
721fn find_workspace_root(crate_path: &std::path::Path) -> Result<Option<PathBuf>> {
722    let mut current = crate_path.to_path_buf();
723
724    // Walk up the directory tree looking for workspace Cargo.toml
725    for _ in 0..10 {
726        let cargo_toml = current.join("Cargo.toml");
727        if cargo_toml.exists() {
728            let content = std::fs::read_to_string(&cargo_toml)?;
729            if content.contains("[workspace]") {
730                return Ok(Some(current));
731            }
732        }
733
734        if !current.pop() {
735            break;
736        }
737    }
738
739    Ok(None)
740}
741
742/// Find the main worktree directory from any git directory (including worktrees).
743///
744/// If the given path is within a git worktree, this returns the main worktree
745/// (the original repo directory). If it's already in the main worktree, returns None.
746fn find_main_worktree(git_path: &std::path::Path) -> Option<PathBuf> {
747    let path_str = git_path.to_string_lossy();
748
749    // Get the common git directory (shared across all worktrees)
750    let output = Command::new("git")
751        .args([
752            "-C",
753            &path_str,
754            "rev-parse",
755            "--path-format=absolute",
756            "--git-common-dir",
757        ])
758        .output()
759        .ok()?;
760
761    if !output.status.success() {
762        return None;
763    }
764
765    let common_dir = String::from_utf8_lossy(&output.stdout).trim().to_string();
766    let common_path = PathBuf::from(&common_dir);
767
768    // The main worktree is the parent of the common .git directory
769    // (e.g., /path/to/repo/.git -> /path/to/repo)
770    let main_worktree = common_path.parent()?.to_path_buf();
771
772    // Check if we're already in the main worktree by comparing to current path
773    // Get the git dir for the given path
774    let git_dir_output = Command::new("git")
775        .args([
776            "-C",
777            &path_str,
778            "rev-parse",
779            "--path-format=absolute",
780            "--git-dir",
781        ])
782        .output()
783        .ok()?;
784
785    if !git_dir_output.status.success() {
786        return None;
787    }
788
789    let git_dir = String::from_utf8_lossy(&git_dir_output.stdout)
790        .trim()
791        .to_string();
792
793    // If git-dir equals git-common-dir, we're in the main worktree
794    if git_dir == common_dir {
795        return None;
796    }
797
798    Some(main_worktree)
799}
800
801fn upgrade_standalone(current_version: &str, skip_confirm: bool) -> Result<bool> {
802    println!("Checking GitHub for updates...");
803    println!();
804
805    let update = self_update::backends::github::Update::configure()
806        .repo_owner("paradigmxyz")
807        .repo_name("mi6")
808        .bin_name("mi6")
809        .current_version(current_version)
810        .build()
811        .context("failed to configure self-updater")?;
812
813    // Check for update first
814    let latest = update
815        .get_latest_release()
816        .context("failed to check for updates")?;
817
818    let latest_version = latest.version.trim_start_matches('v');
819
820    if latest_version == current_version {
821        println!("Already at the latest version (v{}).", current_version);
822        return Ok(true);
823    }
824
825    println!(
826        "New version available: v{} -> v{}",
827        current_version, latest_version
828    );
829
830    if !skip_confirm && !confirm_stdout("Proceed with update?")? {
831        println!("Aborted.");
832        return Ok(false);
833    }
834    println!();
835
836    println!("Downloading and installing v{}...", latest_version);
837
838    // Create a new update with show_progress for the actual download
839    let status = self_update::backends::github::Update::configure()
840        .repo_owner("paradigmxyz")
841        .repo_name("mi6")
842        .bin_name("mi6")
843        .current_version(current_version)
844        .show_download_progress(true)
845        .build()
846        .context("failed to configure self-updater")?
847        .update()
848        .context("failed to perform update")?;
849
850    println!();
851    println!("Updated to v{}!", status.version());
852
853    Ok(true)
854}