php-version-manager 1.3.1

A blazing fast, zero-configuration PHP version manager
use crate::constants::MULTISHELL_PATH_VAR;
use crate::{fs, network};
use anyhow::Result;
use clap::Parser;
use colored::Colorize;

/// Install a specific PHP version
#[derive(Parser, Debug)]
pub struct Install {
    /// The version to install, or "latest"
    pub version: Option<String>,
}

pub async fn execute_install(version: &str) -> Result<()> {
    execute_install_with(version, true).await.map(|_| ())
}

/// `prompt_activation` controls the trailing "Do you want to use PHP X now?" prompt and the
/// resulting env-file write. Callers like `pvm use` set it to `false` because they will fall
/// through to their own activation path with the returned resolved version.
pub async fn execute_install_with(
    version: &str,
    prompt_activation: bool,
) -> Result<Option<String>> {
    let versions_dir = fs::get_versions_dir()?;
    std::fs::create_dir_all(&versions_dir)?;

    println!(
        "{} Resolving latest patch for PHP {}...",
        "".blue(),
        version
    );
    let resolved_version = network::resolve_version(version).await?;

    let available_versions = network::get_available_versions().await?;
    let available_packages = available_versions
        .iter()
        .find(|(v, _)| v == &resolved_version)
        .map(|(_, pkgs)| pkgs.clone())
        .unwrap_or_default();

    if available_packages.is_empty() {
        anyhow::bail!("No packages found for PHP {}", resolved_version);
    }

    let theme = dialoguer::theme::ColorfulTheme::default();
    let selections = dialoguer::MultiSelect::with_theme(&theme)
        .with_prompt(format!(
            "Select packages to install for PHP {}",
            resolved_version
        ))
        .items(&available_packages)
        .defaults(
            &available_packages
                .iter()
                .map(|p| p == "cli")
                .collect::<Vec<_>>(),
        )
        .interact()?;

    if selections.is_empty() {
        println!("{} No packages selected. Operation cancelled.", "".red());
        return Ok(None);
    }

    let selected_packages: Vec<String> = selections
        .into_iter()
        .map(|i| available_packages[i].clone())
        .collect();

    let dest = versions_dir.join(&resolved_version);
    let dest_existed = dest.exists();
    std::fs::create_dir_all(&dest)?;

    for package in &selected_packages {
        println!(
            "{} Fetching PHP {} ({}) package...",
            "".blue(),
            resolved_version,
            package
        );
        if let Err(e) = network::download_and_extract(&resolved_version, package, &dest).await {
            // Only wipe the dest if it didn't exist before this install attempt;
            // a pre-existing install must not be destroyed by a follow-up failure.
            if !dest_existed {
                std::fs::remove_dir_all(&dest).ok();
            }
            anyhow::bail!(
                "Failed to install PHP {} (package {}): {}",
                resolved_version,
                package,
                e
            );
        }
    }

    println!(
        "{} Successfully installed PHP {} [{}] as {}",
        "".green(),
        version,
        selected_packages.join(", "),
        resolved_version
    );

    // Only the cli package places a `php` binary on PATH; without it, switching is meaningless.
    let cli_selected = selected_packages.iter().any(|p| p == "cli");

    if !prompt_activation {
        if !cli_selected {
            println!(
                "{} The 'cli' package was not selected; this version cannot be activated via PATH.",
                "💡".yellow()
            );
            return Ok(None);
        }
        return Ok(Some(resolved_version));
    }

    let use_now = cli_selected
        && dialoguer::Confirm::with_theme(&theme)
            .with_prompt(
                format!("Do you want to use PHP {} now?", resolved_version)
                    .bold()
                    .to_string(),
            )
            .default(true)
            .interact_opt()
            .unwrap_or(Some(false))
            .unwrap_or(false);

    if use_now {
        let v = crate::fs::resolve_local_version(&resolved_version)?;
        let bin_dir = crate::fs::get_version_bin_dir(&v)?;
        let s = crate::shell::detect_shell();
        let export_str1 = s.set_env_var(MULTISHELL_PATH_VAR, &bin_dir.to_string_lossy());
        let export_str2 = s.path(&bin_dir);

        let env_file = crate::fs::get_env_update_path(None)?;
        crate::fs::write_env_file_locked(&env_file, &format!("{}\n{}", export_str1, export_str2))?;

        // Note: process-global env is intentionally NOT mutated here. std::env::set_var
        // is unsound in a multi-threaded tokio runtime, and the wrapper sources env_file
        // into the parent shell on exit, so subsequent pvm invocations see the new PATH.
        println!("{} Switched to PHP {}", "".green(), v.bold());
        Ok(Some(resolved_version))
    } else if !cli_selected {
        println!(
            "{} The 'cli' package was not selected; this version cannot be activated via PATH.",
            "💡".yellow()
        );
        Ok(None)
    } else {
        println!(
            "{} To use this version later, run `{}`",
            "💡".yellow(),
            format!("pvm use {}", version).bold()
        );
        Ok(Some(resolved_version))
    }
}

impl Install {
    pub async fn call(self) -> Result<()> {
        match self.version {
            Some(v) => execute_install(&v).await,
            None => {
                let ls_cmd = crate::commands::ls_remote::LsRemote {
                    version_prefix: None,
                };
                ls_cmd.call().await
            }
        }
    }
}