use crate::constants::MULTISHELL_PATH_VAR;
use crate::{fs, network};
use anyhow::Result;
use clap::Parser;
use colored::Colorize;
#[derive(Parser, Debug)]
pub struct Install {
pub version: Option<String>,
}
pub async fn execute_install(version: &str) -> Result<()> {
execute_install_with(version, true).await.map(|_| ())
}
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 {
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
);
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))?;
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
}
}
}
}