use anyhow::{Context, Result};
use async_trait::async_trait;
use super::{PackageManager, PackageManagerType};
use crate::utils::platform::{is_binary_available, Platform};
pub struct HomebrewManager;
impl HomebrewManager {
pub fn new() -> Self {
Self
}
fn parse_tap_and_package<'a>(&self, package: &'a str) -> (Option<&'a str>, &'a str) {
if package.contains('/') {
let parts: Vec<&str> = package.split('/').collect();
if parts.len() == 3 {
let tap = format!("{}/{}", parts[0], parts[1]);
(Some(package.split_at(tap.len()).0), parts[2])
} else if parts.len() == 2 {
(None, package)
} else {
(None, package)
}
} else {
(None, package)
}
}
async fn is_tap_installed(&self, tap: &str) -> Result<bool> {
let output = tokio::process::Command::new("brew")
.args(["tap"])
.output()
.await
.context("Failed to list Homebrew taps")?;
let taps = String::from_utf8_lossy(&output.stdout);
Ok(taps.lines().any(|line| line.trim() == tap))
}
async fn install_tap(&self, tap: &str) -> Result<()> {
println!("📥 Adding Homebrew tap: {tap}");
let output = tokio::process::Command::new("brew")
.args(["tap", tap])
.output()
.await
.context("Failed to add Homebrew tap")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("Failed to add tap {}: {}", tap, stderr);
}
Ok(())
}
}
#[async_trait]
impl PackageManager for HomebrewManager {
fn manager_type(&self) -> PackageManagerType {
PackageManagerType::Homebrew
}
async fn is_available(&self) -> bool {
match Platform::current() {
Platform::MacOS | Platform::Linux => is_binary_available("brew"),
_ => false,
}
}
async fn check_installed(&self, package: &str) -> Result<bool> {
let (_tap, package_name) = self.parse_tap_and_package(package);
let formula_output = tokio::process::Command::new("brew")
.args(["list", "--formula", package_name])
.output()
.await
.context("Failed to check Homebrew formula")?;
if formula_output.status.success() {
return Ok(true);
}
let cask_output = tokio::process::Command::new("brew")
.args(["list", "--cask", package_name])
.output()
.await
.context("Failed to check Homebrew cask")?;
Ok(cask_output.status.success())
}
async fn install(&self, package: &str, args: &[String]) -> Result<()> {
let (tap, package_name) = self.parse_tap_and_package(package);
if let Some(tap_name) = tap {
if !self.is_tap_installed(tap_name).await? {
self.install_tap(tap_name).await?;
}
}
let mut cmd_args = vec!["install"];
for arg in args {
cmd_args.push(arg);
}
cmd_args.push(package);
println!("📦 Installing {} via Homebrew...", package_name);
let output = tokio::process::Command::new("brew")
.args(&cmd_args)
.output()
.await
.context("Failed to install package via Homebrew")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("Failed to install {}: {}", package, stderr);
}
println!("✅ Successfully installed {}", package_name);
Ok(())
}
async fn get_version(&self, package: &str) -> Result<Option<String>> {
let (_tap, package_name) = self.parse_tap_and_package(package);
let formula_output = tokio::process::Command::new("brew")
.args(["list", "--versions", "--formula", package_name])
.output()
.await
.context("Failed to get formula version")?;
if formula_output.status.success() && !formula_output.stdout.is_empty() {
let version_line = String::from_utf8_lossy(&formula_output.stdout);
let parts: Vec<&str> = version_line.split_whitespace().collect();
if parts.len() > 1 {
return Ok(Some(parts.last().unwrap().to_string()));
}
}
let cask_output = tokio::process::Command::new("brew")
.args(["list", "--versions", "--cask", package_name])
.output()
.await
.context("Failed to get cask version")?;
if cask_output.status.success() && !cask_output.stdout.is_empty() {
let version_line = String::from_utf8_lossy(&cask_output.stdout);
let parts: Vec<&str> = version_line.split_whitespace().collect();
if parts.len() > 1 {
return Ok(Some(parts.last().unwrap().to_string()));
}
}
Ok(None)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_tap_and_package() {
let manager = HomebrewManager::new();
let (tap, pkg) = manager.parse_tap_and_package("git");
assert_eq!(tap, None);
assert_eq!(pkg, "git");
let (tap, pkg) = manager.parse_tap_and_package("alexcaza/weztermocil/weztermocil");
assert_eq!(tap, Some("alexcaza/weztermocil"));
assert_eq!(pkg, "weztermocil");
let (tap, pkg) = manager.parse_tap_and_package("homebrew/cask");
assert_eq!(tap, None);
assert_eq!(pkg, "homebrew/cask");
}
}