use crate::config::WtpConfig;
use crate::errors::{Result, WtpMcpError};
use std::path::{Path, PathBuf};
use std::process::Command;
const BINARY_NAME: &str = if cfg!(windows) { "wtp.exe" } else { "wtp" };
#[derive(Debug, Clone)]
pub struct WtpBinary {
pub path: PathBuf,
pub version: Option<String>,
}
impl WtpBinary {
pub fn resolve(config: &WtpConfig) -> Result<WtpBinary> {
if let Some(ref path) = config.path
&& is_executable(path)
{
return Ok(WtpBinary {
path: path.clone(),
version: None,
});
}
if let Some(cache_path) = cache_dir() {
let binary_path = cache_path.join(BINARY_NAME);
if is_executable(&binary_path) {
return Ok(WtpBinary {
path: binary_path,
version: None,
});
}
}
if let Ok(path) = which::which(BINARY_NAME) {
return Ok(WtpBinary {
path,
version: None,
});
}
Err(WtpMcpError::BinaryNotFound {
message: "wtp binary not found in config path, cache directory, or PATH".to_string(),
})
}
pub fn version(&self) -> Result<String> {
let output = Command::new(&self.path)
.arg("--version")
.output()
.map_err(|e| WtpMcpError::CommandFailed {
exit_code: -1,
message: format!("failed to execute wtp --version: {}", e),
stderr: String::new(),
})?;
if !output.status.success() {
return Err(WtpMcpError::CommandFailed {
exit_code: output.status.code().unwrap_or(-1),
message: "wtp --version failed".to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
});
}
let stdout = String::from_utf8_lossy(&output.stdout);
parse_version(&stdout).ok_or_else(|| WtpMcpError::ParseError {
message: "failed to parse version output".to_string(),
raw_output: stdout.to_string(),
})
}
}
pub fn cache_dir() -> Option<PathBuf> {
dirs::cache_dir().map(|p| p.join("wtp-mcp").join("bin"))
}
fn is_executable(path: &Path) -> bool {
if !path.is_file() {
return false;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(metadata) = path.metadata() {
return metadata.permissions().mode() & 0o111 != 0;
}
false
}
#[cfg(not(unix))]
{
path.exists()
}
}
fn parse_version(output: &str) -> Option<String> {
let trimmed = output.trim();
if trimmed.starts_with("wtp ") {
Some(trimmed.strip_prefix("wtp ")?.trim().to_string())
} else {
Some(trimmed.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_dir_returns_path() {
let cache = cache_dir();
assert!(cache.is_some());
let path = cache.unwrap();
assert!(path.ends_with("wtp-mcp/bin") || path.to_string_lossy().contains("wtp-mcp"));
}
#[test]
fn test_parse_version_with_prefix() {
assert_eq!(parse_version("wtp 1.2.3"), Some("1.2.3".to_string()));
assert_eq!(parse_version("wtp 1.2.3\n"), Some("1.2.3".to_string()));
}
#[test]
fn test_parse_version_without_prefix() {
assert_eq!(parse_version("1.2.3"), Some("1.2.3".to_string()));
assert_eq!(parse_version(" 1.2.3 \n"), Some("1.2.3".to_string()));
}
#[test]
fn test_resolve_with_invalid_config_path_falls_back() {
let config = WtpConfig {
path: Some(PathBuf::from("/nonexistent/path/wtp")),
};
let result = WtpBinary::resolve(&config);
if let Ok(binary) = result {
assert_ne!(binary.path, PathBuf::from("/nonexistent/path/wtp"));
}
}
}