use crate::doctor;
use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
use std::process::Command;
pub async fn update_executable(
target_dir: Option<PathBuf>,
repo_url: Option<String>,
branch: Option<String>,
) -> Result<()> {
let repo_url = repo_url.unwrap_or_else(|| {
env!("CARGO_PKG_REPOSITORY").to_string()
});
let branch = branch.unwrap_or_else(|| "main".to_string());
let target_dir = match target_dir {
Some(dir) => dir,
None => {
let home = dirs::home_dir()
.ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?;
home.join(".local").join("bin")
}
};
println!("🦀 rust-docs-mcp Updater");
println!("=========================");
check_command_exists("git")?;
check_command_exists("cargo")?;
check_nightly_toolchain()?;
let temp_dir = tempfile::TempDir::new().context("Failed to create temporary directory")?;
let temp_path = temp_dir.path();
println!("📦 Cloning rust-docs-mcp repository...");
let clone_output = Command::new("git")
.args(["clone", "--depth", "1", "--branch", &branch, &repo_url])
.arg(temp_path.join("rust-docs-mcp"))
.output()
.context("Failed to run git clone")?;
if !clone_output.status.success() {
let stderr = String::from_utf8_lossy(&clone_output.stderr);
anyhow::bail!("Failed to clone repository: {}", stderr);
}
println!("🔨 Building rust-docs-mcp in release mode (this may take a few minutes)...");
let build_output = Command::new("cargo")
.args(["build", "--release", "-p", "rust-docs-mcp"])
.current_dir(temp_path.join("rust-docs-mcp"))
.output()
.context("Failed to run cargo build")?;
if !build_output.status.success() {
let stderr = String::from_utf8_lossy(&build_output.stderr);
anyhow::bail!("Failed to build rust-docs-mcp: {}", stderr);
}
println!("📋 Installing rust-docs-mcp to {}...", target_dir.display());
let built_binary = temp_path.join("rust-docs-mcp/target/release/rust-docs-mcp");
let install_output = Command::new(&built_binary)
.args([
"install",
"--target-dir",
&target_dir.to_string_lossy(),
"--force",
])
.output()
.context("Failed to run install command")?;
if !install_output.status.success() {
let stderr = String::from_utf8_lossy(&install_output.stderr);
anyhow::bail!("Failed to install rust-docs-mcp: {}", stderr);
}
handle_macos_signing(&target_dir)?;
println!("✅ rust-docs-mcp updated successfully!");
check_path_and_advise(&target_dir)?;
println!("\n📖 Usage:");
println!(" rust-docs-mcp # Start MCP server");
println!(" rust-docs-mcp install # Install/update to PATH");
println!(" rust-docs-mcp update # Update to latest version");
println!(" rust-docs-mcp doctor # Verify system environment");
println!(" rust-docs-mcp --help # Show help");
doctor::run_and_print_diagnostics().await?;
Ok(())
}
fn check_command_exists(command: &str) -> Result<()> {
let output = Command::new("which")
.arg(command)
.output()
.context("Failed to check command existence")?;
if !output.status.success() {
anyhow::bail!("{} is required but not installed", command);
}
Ok(())
}
fn check_nightly_toolchain() -> Result<()> {
let output = Command::new("rustup")
.args(["toolchain", "list"])
.output()
.context("Failed to check rustup toolchains")?;
if !output.status.success() {
anyhow::bail!("Failed to check available toolchains");
}
let toolchains = String::from_utf8_lossy(&output.stdout);
if !toolchains.contains("nightly") {
println!("🔧 Installing Rust nightly toolchain...");
let install_output = Command::new("rustup")
.args(["toolchain", "install", "nightly"])
.output()
.context("Failed to install nightly toolchain")?;
if !install_output.status.success() {
let stderr = String::from_utf8_lossy(&install_output.stderr);
anyhow::bail!("Failed to install Rust nightly toolchain: {}", stderr);
}
println!("✅ Rust nightly toolchain installed");
}
Ok(())
}
#[cfg(target_os = "macos")]
fn handle_macos_signing(target_dir: &Path) -> Result<()> {
let binary_path = target_dir.join("rust-docs-mcp");
println!("🔐 Signing binary for macOS...");
let _ = Command::new("xattr")
.args(["-cr", &binary_path.to_string_lossy()])
.output();
let sign_output = Command::new("codesign")
.args([
"--force",
"--deep",
"-s",
"-",
&binary_path.to_string_lossy(),
])
.output()
.context("Failed to run codesign")?;
if sign_output.status.success() {
println!("✅ Binary signed successfully");
} else {
println!("⚠️ Could not sign binary - you may need to run:");
println!(" codesign --force --deep -s - {}", binary_path.display());
}
Ok(())
}
#[cfg(not(target_os = "macos"))]
fn handle_macos_signing(_target_dir: &Path) -> Result<()> {
Ok(())
}
fn check_path_and_advise(target_dir: &Path) -> Result<()> {
use std::env;
if let Ok(path_var) = env::var("PATH") {
let path_separator = if cfg!(windows) { ';' } else { ':' };
let paths: Vec<&str> = path_var.split(path_separator).collect();
let target_dir_str = target_dir.to_string_lossy();
if !paths.iter().any(|&p| p == target_dir_str) {
println!("\n⚠️ {} is not in your PATH.", target_dir.display());
println!("Add this line to your shell configuration file (.bashrc, .zshrc, etc.):");
println!("export PATH=\"{}:$PATH\"", target_dir.display());
println!("\nThen reload your shell or run:");
println!("source ~/.bashrc # or ~/.zshrc");
} else {
println!("\n✅ You can now run 'rust-docs-mcp' from anywhere!");
}
}
Ok(())
}