use crate::defaults;
use crate::error::{Error, Result};
use std::process::Command;
use super::constants::{CRATES_IO_API, GITHUB_RELEASES_API, VERSION};
use super::execution::execute_upgrade;
use super::planning::resolve_binary_on_path;
use super::types::*;
use super::validation::check_for_updates;
pub fn current_version() -> &'static str {
VERSION
}
pub(crate) fn fetch_latest_crates_io_version() -> Result<String> {
let client = reqwest::blocking::Client::builder()
.user_agent(format!("homeboy/{}", VERSION))
.timeout(std::time::Duration::from_secs(10))
.build()
.map_err(|e| Error::internal_io(e.to_string(), Some("create HTTP client".to_string())))?;
let response: CratesIoResponse = client
.get(CRATES_IO_API)
.send()
.map_err(|e| Error::internal_io(e.to_string(), Some("query crates.io".to_string())))?
.json()
.map_err(|e| {
Error::internal_json(e.to_string(), Some("parse crates.io response".to_string()))
})?;
Ok(response.crate_info.newest_version)
}
pub(crate) fn fetch_latest_github_version() -> Result<String> {
let client = reqwest::blocking::Client::builder()
.user_agent(format!("homeboy/{}", VERSION))
.timeout(std::time::Duration::from_secs(10))
.build()
.map_err(|e| Error::internal_io(e.to_string(), Some("create HTTP client".to_string())))?;
let response: GitHubRelease = client
.get(GITHUB_RELEASES_API)
.send()
.map_err(|e| Error::internal_io(e.to_string(), Some("query GitHub releases".to_string())))?
.json()
.map_err(|e| {
Error::internal_json(
e.to_string(),
Some("parse GitHub release response".to_string()),
)
})?;
let version = response
.tag_name
.strip_prefix('v')
.unwrap_or(&response.tag_name);
Ok(version.to_string())
}
pub fn fetch_latest_version(method: InstallMethod) -> Result<String> {
match method {
InstallMethod::Cargo => fetch_latest_crates_io_version(),
InstallMethod::Homebrew
| InstallMethod::Source
| InstallMethod::Binary
| InstallMethod::Unknown => fetch_latest_github_version(),
}
}
pub fn detect_install_method() -> InstallMethod {
let exe_path = match std::env::current_exe() {
Ok(path) => path.to_string_lossy().to_string(),
Err(_) => return InstallMethod::Unknown,
};
let defaults = defaults::load_defaults();
for pattern in &defaults.install_methods.homebrew.path_patterns {
if exe_path.contains(pattern) {
return InstallMethod::Homebrew;
}
}
if let Some(list_cmd) = &defaults.install_methods.homebrew.list_command {
let parts: Vec<&str> = list_cmd.split_whitespace().collect();
if let Some((cmd, args)) = parts.split_first() {
if Command::new(cmd)
.args(args)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
{
return InstallMethod::Homebrew;
}
}
}
for pattern in &defaults.install_methods.cargo.path_patterns {
if exe_path.contains(pattern) {
return InstallMethod::Cargo;
}
}
for pattern in &defaults.install_methods.source.path_patterns {
if exe_path.contains(pattern) {
return InstallMethod::Source;
}
}
for pattern in &defaults.install_methods.binary.path_patterns {
if exe_path.contains(pattern) {
return InstallMethod::Binary;
}
}
InstallMethod::Unknown
}
pub(crate) fn version_is_newer(latest: &str, current: &str) -> bool {
let parse = |v: &str| -> Option<(u32, u32, u32)> {
let parts: Vec<&str> = v.split('.').collect();
if parts.len() >= 3 {
Some((
parts[0].parse().ok()?,
parts[1].parse().ok()?,
parts[2].parse().ok()?,
))
} else {
None
}
};
match (parse(latest), parse(current)) {
(Some(l), Some(c)) => l > c,
_ => latest != current,
}
}
pub fn run_upgrade_with_method(
force: bool,
method_override: Option<InstallMethod>,
) -> Result<UpgradeResult> {
let install_method = method_override.unwrap_or_else(detect_install_method);
let previous_version = current_version().to_string();
if install_method == InstallMethod::Unknown {
return Err(Error::validation_invalid_argument(
"install_method",
"Could not detect installation method",
None,
None,
)
.with_hint("Try: homeboy upgrade --method binary")
.with_hint("Or reinstall using: brew install homeboy")
.with_hint("Or: cargo install homeboy"));
}
if !force {
let check = check_for_updates()?;
if !check.update_available {
return Ok(UpgradeResult {
command: "upgrade".to_string(),
install_method,
previous_version: previous_version.clone(),
new_version: Some(previous_version),
upgraded: false,
message: "Already at latest version".to_string(),
restart_required: false,
extensions_updated: vec![],
extensions_skipped: vec![],
});
}
}
let (success, new_version) = execute_upgrade(install_method)?;
let (extensions_updated, extensions_skipped) = if success {
update_all_extensions()
} else {
(vec![], vec![])
};
Ok(UpgradeResult {
command: "upgrade".to_string(),
install_method,
previous_version,
new_version: new_version.clone(),
upgraded: success,
message: if success {
format!("Upgraded to {}", new_version.as_deref().unwrap_or("latest"))
} else {
"Upgrade command completed but version unchanged".to_string()
},
restart_required: success,
extensions_updated,
extensions_skipped,
})
}
fn update_all_extensions() -> (Vec<ExtensionUpgradeEntry>, Vec<String>) {
use crate::extension;
let extension_ids = extension::available_extension_ids();
if extension_ids.is_empty() {
return (vec![], vec![]);
}
log_status!(
"upgrade",
"Updating {} installed extension(s)...",
extension_ids.len()
);
let mut updated = Vec::new();
let mut skipped = Vec::new();
for id in &extension_ids {
if extension::is_extension_linked(id) {
skipped.push(id.clone());
continue;
}
let old_version = extension::load_extension(id)
.ok()
.map(|m| m.version.clone())
.unwrap_or_default();
match extension::update(id, false) {
Ok(_) => {
let new_version = extension::load_extension(id)
.ok()
.map(|m| m.version.clone())
.unwrap_or_default();
if old_version != new_version {
log_status!("upgrade", " {} {} → {}", id, old_version, new_version);
} else {
log_status!("upgrade", " {} {} (up to date)", id, new_version);
}
updated.push(ExtensionUpgradeEntry {
extension_id: id.clone(),
old_version,
new_version,
});
}
Err(e) => {
log_status!("upgrade", " {} skipped: {}", id, e.message);
skipped.push(id.clone());
}
}
}
(updated, skipped)
}
#[cfg(not(unix))]
pub fn restart_with_new_binary() {
log_status!("upgrade", "Please restart homeboy to use the new version.");
}
#[cfg(unix)]
pub fn restart_with_new_binary() -> ! {
use std::os::unix::process::CommandExt;
let binary = resolve_binary_on_path()
.unwrap_or_else(|| std::env::current_exe().expect("Failed to get current executable path"));
let err = Command::new(&binary).arg("--version").exec();
eprintln!(
"Warning: could not restart automatically ({}). Please run `homeboy --version` to confirm the upgrade.",
err
);
std::process::exit(0);
}