zilliz 1.4.2

TUI and CLI tool for managing Zilliz Cloud clusters and Milvus operations
Documentation
use anyhow::{anyhow, bail, Context, Result};
use std::fs;
use std::io::{IsTerminal, Write};
use std::path::{Path, PathBuf};
#[cfg(windows)]
use std::process::{Command, Stdio};

struct Layout {
    /// The currently running binary, canonicalized. On Windows this is the
    /// path we cannot delete synchronously; on Unix it's deleted last.
    self_path: PathBuf,
    /// All binary / alias files that should be removed (zilliz, zz, .exe
    /// variants). `self_path` is part of this list.
    targets: Vec<PathBuf>,
    #[cfg_attr(not(windows), allow(dead_code))]
    install_dir: PathBuf,
    config_dir: Option<PathBuf>,
}

pub async fn run(yes: bool, purge: bool) -> Result<()> {
    let layout = resolve_install_layout()?;

    print_plan(&layout, purge);

    if !yes {
        if !std::io::stdin().is_terminal() {
            bail!("Refusing to uninstall non-interactively. Pass --yes to proceed.");
        }
        if !prompt_confirm("Proceed with uninstall? [y/N]: ")? {
            println!("Uninstall cancelled.");
            return Ok(());
        }
    }

    // Delete every sibling first (synchronously), leave the running binary
    // for last (matters on Windows where the running .exe is locked).
    for target in &layout.targets {
        if target == &layout.self_path {
            continue;
        }
        remove_path(target);
    }

    #[cfg(unix)]
    remove_self_unix(&layout.self_path)?;

    #[cfg(windows)]
    schedule_self_delete_windows(&layout.self_path)?;

    #[cfg(windows)]
    cleanup_windows_user_path(&layout.install_dir);

    if purge {
        if let Some(cfg) = &layout.config_dir {
            if cfg.exists() {
                fs::remove_dir_all(cfg)
                    .with_context(|| format!("remove config dir {}", cfg.display()))?;
                println!("Removed {}", cfg.display());
            }
        }
    }

    print_summary(&layout, purge);
    Ok(())
}

fn resolve_install_layout() -> Result<Layout> {
    let raw = std::env::current_exe().context("locate running binary via current_exe()")?;
    let self_path = raw.canonicalize().unwrap_or(raw);
    let install_dir = self_path
        .parent()
        .ok_or_else(|| anyhow!("install dir has no parent: {}", self_path.display()))?
        .to_path_buf();

    let names: &[&str] = if cfg!(windows) {
        &["zilliz.exe", "zz.exe"]
    } else {
        &["zilliz", "zz"]
    };

    // Scan every well-known install location, not just the directory the
    // running binary lives in. Different installers use different defaults:
    //   - our install.sh / install.ps1: ~/.zilliz/bin
    //   - upstream zilliz-cli install.sh: ~/.local/bin
    //   - `cargo install zilliz`: ~/.cargo/bin
    // Plus current_exe()'s directory in case it's none of the above.
    let mut search_dirs: Vec<PathBuf> = vec![install_dir.clone()];
    if let Some(home) = dirs::home_dir() {
        for sub in [".zilliz/bin", ".local/bin", ".cargo/bin"] {
            let d = home.join(sub);
            if !search_dirs.contains(&d) {
                search_dirs.push(d);
            }
        }
    }

    let mut targets: Vec<PathBuf> = Vec::new();
    for dir in &search_dirs {
        for name in names {
            let p = dir.join(name);
            if (p.exists() || fs::symlink_metadata(&p).is_ok()) && !targets.contains(&p) {
                targets.push(p);
            }
        }
    }
    if !targets.contains(&self_path) {
        targets.push(self_path.clone());
    }

    let config_dir = dirs::home_dir().map(|h| h.join(".zilliz"));

    Ok(Layout {
        self_path,
        targets,
        install_dir,
        config_dir,
    })
}

fn print_plan(layout: &Layout, purge: bool) {
    println!("This will remove:");
    for target in &layout.targets {
        println!("  - {}", target.display());
    }
    if purge {
        if let Some(cfg) = &layout.config_dir {
            println!("  - {} (user config: credentials, config)", cfg.display());
        }
    } else if let Some(cfg) = &layout.config_dir {
        println!(
            "\nConfig at {} will be preserved (pass --purge to remove it).",
            cfg.display()
        );
    }
    println!();
}

fn prompt_confirm(prompt: &str) -> Result<bool> {
    print!("{}", prompt);
    std::io::stdout().flush().ok();
    let mut buf = String::new();
    std::io::stdin().read_line(&mut buf).context("read stdin")?;
    let trimmed = buf.trim();
    Ok(trimmed.eq_ignore_ascii_case("y") || trimmed.eq_ignore_ascii_case("yes"))
}

fn remove_path(p: &Path) {
    if !p.exists() && fs::symlink_metadata(p).is_err() {
        return;
    }
    match fs::remove_file(p) {
        Ok(()) => println!("Removed {}", p.display()),
        Err(e) => eprintln!("Warning: failed to remove {}: {}", p.display(), e),
    }
}

#[cfg(unix)]
fn remove_self_unix(binary: &Path) -> Result<()> {
    if !binary.exists() && fs::symlink_metadata(binary).is_err() {
        return Ok(());
    }
    fs::remove_file(binary).with_context(|| {
        format!(
            "remove {} — if your install is system-managed, use your package manager instead",
            binary.display()
        )
    })?;
    println!("Removed {}", binary.display());
    Ok(())
}

#[cfg(windows)]
fn schedule_self_delete_windows(binary: &Path) -> Result<()> {
    use std::os::windows::process::CommandExt;
    const CREATE_NO_WINDOW: u32 = 0x0800_0000;
    const DETACHED_PROCESS: u32 = 0x0000_0008;

    let script = format!(
        "ping -n 3 127.0.0.1 >nul & del /f /q \"{}\"",
        binary.display()
    );
    Command::new("cmd")
        .args(["/c", &script])
        .creation_flags(CREATE_NO_WINDOW | DETACHED_PROCESS)
        .spawn()
        .with_context(|| format!("schedule deferred delete of {}", binary.display()))?;
    println!(
        "Scheduled deletion of {} (will complete after this process exits).",
        binary.display()
    );
    Ok(())
}

#[cfg(windows)]
fn cleanup_windows_user_path(install_dir: &Path) {
    let install_str = install_dir.display().to_string();
    let ps = format!(
        "$p=[Environment]::GetEnvironmentVariable('PATH','User'); \
         if ($p) {{ \
           $new=($p -split ';' | Where-Object {{ $_ -and ($_ -ne '{0}') }}) -join ';'; \
           if ($new -ne $p) {{ [Environment]::SetEnvironmentVariable('PATH', $new, 'User') }} \
         }}",
        install_str.replace('\'', "''")
    );
    let _ = Command::new("powershell")
        .args(["-NoProfile", "-Command", &ps])
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status();
    println!("Cleaned {} from user PATH (if present).", install_str);
}

fn print_summary(layout: &Layout, purge: bool) {
    println!();
    println!("Uninstalled zilliz.");
    if !purge {
        if let Some(cfg) = &layout.config_dir {
            println!(
                "Config preserved at {} (credentials, config).",
                cfg.display()
            );
            #[cfg(unix)]
            println!("Remove it manually with: rm -rf {}", cfg.display());
            #[cfg(windows)]
            println!(
                "Remove it manually with: Remove-Item -Recurse -Force {}",
                cfg.display()
            );
        }
    }
    println!(
        "If you installed via `cargo install zilliz`, also run `cargo uninstall zilliz` \
         to clear Cargo's registry record."
    );
}