use crate::config::UpdateInfo;
use anyhow::{Context, Result};
use std::env;
use std::fs;
use std::path::Path;
pub fn get_binary_path(binary_name: &str) -> Result<String> {
if !cfg!(unix) {
anyhow::bail!("Windows support is not yet implemented. Please use Linux or macOS.");
}
let system_path = format!("/usr/local/bin/{}", binary_name);
let user_bin_dir = env::var("HOME")
.map(|home| format!("{}/.local/bin", home))
.unwrap_or_else(|_| "/home/user/.local/bin".to_string());
let user_path = format!("{}/{}", user_bin_dir, binary_name);
if check_write_permissions(Path::new("/usr/local/bin")) {
return Ok(system_path);
}
tracing::debug!(
"Cannot write to /usr/local/bin, falling back to user directory: {}",
user_path
);
if let Err(e) = fs::create_dir_all(&user_bin_dir) {
tracing::warn!("Could not create user bin directory: {}", e);
}
Ok(user_path)
}
pub fn get_config_dir() -> Result<String> {
if !cfg!(unix) {
anyhow::bail!("Windows support is not yet implemented. Please use Linux or macOS.");
}
let home = env::var("HOME").context("Could not determine HOME directory")?;
Ok(format!("{}/.config/terraphim", home))
}
pub fn check_write_permissions(path: &Path) -> bool {
let check_path = if path.exists() {
if path.is_file() {
match path.parent() {
Some(parent) if parent.to_str() != Some("") => parent,
_ => return false,
}
} else {
path
}
} else {
match path.parent() {
Some(parent) if parent.to_str() != Some("") => parent,
_ => return false,
}
};
let test_file = check_path.join(".write_test_12345");
match fs::write(&test_file, b"test") {
Ok(_) => {
let _ = fs::remove_file(&test_file);
true
}
Err(_) => false,
}
}
pub fn show_manual_update_instructions(update_info: &UpdateInfo) {
println!("\n========================================");
println!(" MANUAL UPDATE INSTRUCTIONS");
println!("========================================\n");
println!("Version {} is available!\n", update_info.version);
println!("Release notes:\n{}\n", update_info.notes);
println!("Release date: {}\n", update_info.release_date);
println!("To update manually, follow these steps:\n");
println!("1. Download the new binary:");
println!(" curl -L {} -o /tmp/terraphim", update_info.download_url);
println!();
println!("2. Verify the signature:");
println!(
" curl -L {} -o /tmp/terraphim.sig",
update_info.signature_url
);
println!(" gpg --verify /tmp/terraphim.sig /tmp/terraphim");
println!();
println!("3. Make it executable:");
println!(" chmod +x /tmp/terraphim");
println!();
println!("4. Replace the old binary (you may need sudo):");
println!(" sudo mv /tmp/terraphim /usr/local/bin/");
println!();
println!(" Or if you prefer user-local installation:");
println!(" mkdir -p ~/.local/bin");
println!(" mv /tmp/terraphim ~/.local/bin/");
println!();
println!(" Make sure ~/.local/bin is in your PATH:");
println!(" export PATH=\"$HOME/.local/bin:$PATH\"");
println!();
println!("5. Verify the installation:");
println!(" terraphim --version");
println!();
println!("========================================\n");
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn test_get_binary_path_system_writable() {
let binary_name = "test_binary";
let path = get_binary_path(binary_name).expect("Should get binary path");
assert!(
path.contains(binary_name),
"Path should contain binary name"
);
println!("Binary path: {}", path);
}
#[test]
fn test_get_binary_path_custom_name() {
let path = get_binary_path("my_custom_tool").expect("Should get binary path");
assert!(path.contains("my_custom_tool"));
}
#[test]
fn test_get_config_dir() {
let config_dir = get_config_dir().expect("Should get config directory");
assert!(
config_dir.contains(".config/terraphim"),
"Should contain .config/terraphim"
);
let path = Path::new(&config_dir);
assert!(path.is_absolute(), "Should be absolute path");
}
#[test]
fn test_check_write_permissions_writable_dir() {
let temp_dir = std::env::temp_dir();
let can_write = check_write_permissions(&temp_dir);
assert!(can_write, "Should be able to write to temp directory");
}
#[test]
fn test_check_write_permissions_readonly_dir() {
let temp_dir = tempfile::TempDir::new().expect("Failed to create temp dir");
let readonly_path = temp_dir.path().join("readonly");
fs::create_dir(&readonly_path).expect("Failed to create readonly dir");
#[cfg(unix)]
{
let mut perms = fs::metadata(&readonly_path)
.expect("Failed to get metadata")
.permissions();
perms.set_readonly(true);
fs::set_permissions(&readonly_path, perms).expect("Failed to set permissions");
let can_write = check_write_permissions(&readonly_path);
assert!(
!can_write,
"Should not be able to write to readonly directory"
);
}
#[cfg(not(unix))]
{
let can_write = check_write_permissions(&readonly_path);
assert!(
!can_write,
"Should not be able to write to readonly directory"
);
}
}
#[test]
fn test_check_write_permissions_file() {
let temp_file = tempfile::NamedTempFile::new().expect("Failed to create temp file");
let can_write = check_write_permissions(temp_file.path());
assert!(
can_write,
"Should be able to write to file's parent directory"
);
}
#[test]
fn test_check_write_permissions_nonexistent_path() {
let nonexistent = Path::new("/this/path/does/not/exist/test");
let can_write = check_write_permissions(nonexistent);
println!("Can write to nonexistent path: {}", can_write);
}
#[test]
fn test_show_manual_update_instructions() {
let info = UpdateInfo {
version: "1.1.0".to_string(),
release_date: jiff::Timestamp::now(),
notes: "Bug fixes and improvements".to_string(),
download_url:
"https://github.com/terraphim/terraphim-ai/releases/download/v1.1.0/terraphim"
.to_string(),
signature_url:
"https://github.com/terraphim/terraphim-ai/releases/download/v1.1.0/terraphim.sig"
.to_string(),
arch: "x86_64".to_string(),
};
show_manual_update_instructions(&info);
}
#[test]
fn test_show_manual_update_instructions_with_long_notes() {
let long_notes = "This is a very long release note that contains multiple lines and detailed information about what has changed in this release. It should be formatted properly in the output.";
let info = UpdateInfo {
version: "2.0.0".to_string(),
release_date: jiff::Timestamp::now(),
notes: long_notes.to_string(),
download_url: "https://example.com/binary".to_string(),
signature_url: "https://example.com/binary.sig".to_string(),
arch: "aarch64".to_string(),
};
show_manual_update_instructions(&info);
}
#[test]
fn test_get_config_dir_creates_parent() {
let config_dir = get_config_dir().expect("Should get config directory");
assert!(config_dir.starts_with('/'), "Should be absolute path");
}
#[test]
fn test_binary_path_consistency() {
let path1 = get_binary_path("test").expect("Should get path");
let path2 = get_binary_path("test").expect("Should get path");
assert_eq!(path1, path2, "Should return consistent paths");
}
}