#[cfg(feature = "cargo")]
pub mod cargo;
#[cfg(feature = "gh")]
pub mod gh;
#[cfg(feature = "git")]
pub mod git;
use std::process::{Command, Stdio};
#[derive(Debug, thiserror::Error)]
pub enum CliError {
#[error("{cli} not found. {hint}")]
NotFound { cli: String, hint: String },
#[error("{cli} failed (exit {code}): {stderr}")]
Failed {
cli: String,
code: i32,
stderr: String,
},
#[error("failed to spawn {cli}: {source}")]
Spawn { cli: String, source: std::io::Error },
#[error("{cli} produced invalid UTF-8 output")]
InvalidUtf8 { cli: String },
}
pub type Result<T> = std::result::Result<T, CliError>;
pub struct Cli {
pub name: &'static str,
pub hint: &'static str,
}
impl Cli {
pub fn available(&self) -> bool {
Command::new(self.name)
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok_and(|s| s.success())
}
pub fn require(&self) -> Result<()> {
if self.available() {
Ok(())
} else {
Err(CliError::NotFound {
cli: self.name.to_string(),
hint: self.hint.to_string(),
})
}
}
pub fn command(&self) -> Command {
Command::new(self.name)
}
pub(crate) fn run_command(&self, cmd: &mut Command) -> Result<String> {
let output = cmd.output().map_err(|e| self.spawn_error(e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
return Err(CliError::Failed {
cli: self.name.to_string(),
code: output.status.code().unwrap_or(-1),
stderr,
});
}
String::from_utf8(output.stdout)
.map(|s| s.trim_end().to_string())
.map_err(|_| CliError::InvalidUtf8 {
cli: self.name.to_string(),
})
}
pub(crate) fn run_status(&self, cmd: &mut Command) -> Result<()> {
let status = cmd.status().map_err(|e| self.spawn_error(e))?;
if !status.success() {
return Err(CliError::Failed {
cli: self.name.to_string(),
code: status.code().unwrap_or(-1),
stderr: String::new(),
});
}
Ok(())
}
fn spawn_error(&self, e: std::io::Error) -> CliError {
if e.kind() == std::io::ErrorKind::NotFound {
CliError::NotFound {
cli: self.name.to_string(),
hint: self.hint.to_string(),
}
} else {
CliError::Spawn {
cli: self.name.to_string(),
source: e,
}
}
}
}