use crate::cmd::{CliError, Result, UI};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
pub struct Utils;
impl Utils {
pub fn timestamp() -> String {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
.to_string()
}
pub fn generate_id() -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
SystemTime::now().hash(&mut hasher);
format!("{:x}", hasher.finish())
}
pub fn command_exists(command: &str) -> bool {
which::which(command).is_ok()
}
pub fn run_command(command: &str, args: &[&str]) -> Result<String> {
let output = Command::new(command)
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.map_err(|e| CliError::CommandFailed(format!("Failed to execute {}: {}", command, e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(CliError::CommandFailed(format!(
"Command {} failed: {}",
command, stderr
)));
}
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
pub fn is_git_repo(path: &Path) -> bool {
path.join(".git").exists() ||
path.ancestors().any(|p| p.join(".git").exists())
}
pub fn git_root(path: &Path) -> Option<PathBuf> {
for ancestor in path.ancestors() {
if ancestor.join(".git").exists() {
return Some(ancestor.to_path_buf());
}
}
None
}
pub fn has_extension(path: &Path, ext: &str) -> bool {
path.extension()
.and_then(|s| s.to_str())
.map(|s| s.eq_ignore_ascii_case(ext))
.unwrap_or(false)
}
pub fn file_size(path: &Path) -> Result<u64> {
fs::metadata(path)
.map(|m| m.len())
.map_err(|e| CliError::FileSystem(format!("Failed to get file size: {}", e)))
}
pub fn format_size(size: u64) -> String {
const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
let mut size = size as f64;
let mut unit_index = 0;
while size >= 1024.0 && unit_index < UNITS.len() - 1 {
size /= 1024.0;
unit_index += 1;
}
if unit_index == 0 {
format!("{} {}", size as u64, UNITS[unit_index])
} else {
format!("{:.1} {}", size, UNITS[unit_index])
}
}
pub fn format_duration(duration: Duration) -> String {
let secs = duration.as_secs();
if secs < 60 {
format!("{}s", secs)
} else if secs < 3600 {
format!("{}m {}s", secs / 60, secs % 60)
} else {
format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
}
}
pub fn truncate(s: &str, max_len: usize) -> String {
if s.len() <= max_len {
s.to_string()
} else {
format!("{}...", &s[..max_len.saturating_sub(3)])
}
}
pub fn terminal_width() -> usize {
terminal_size::terminal_size()
.map(|(terminal_size::Width(w), _)| w as usize)
.unwrap_or(80)
}
pub fn supports_color() -> bool {
atty::is(atty::Stream::Stderr) &&
std::env::var("NO_COLOR").is_err() &&
std::env::var("TERM").map(|t| t != "dumb").unwrap_or(true)
}
pub fn open_url(url: &str) -> Result<()> {
open::that(url)
.map_err(|e| CliError::CommandFailed(format!("Failed to open URL: {}", e)))?;
Ok(())
}
pub fn is_valid_url(url: &str) -> bool {
url.starts_with("http://") || url.starts_with("https://")
}
pub fn backup_file(path: &Path) -> Result<PathBuf> {
if !path.exists() {
return Err(CliError::FileSystem("File does not exist".to_string()));
}
let backup_path = path.with_extension(format!(
"{}.backup.{}",
path.extension().and_then(|s| s.to_str()).unwrap_or(""),
Self::timestamp()
));
fs::copy(path, &backup_path)
.map_err(|e| CliError::FileSystem(format!("Failed to create backup: {}", e)))?;
Ok(backup_path)
}
pub fn cleanup_backups(dir: &Path, keep_count: usize) -> Result<usize> {
let mut backups = Vec::new();
for entry in fs::read_dir(dir)
.map_err(|e| CliError::FileSystem(format!("Failed to read directory: {}", e)))?
{
let entry = entry
.map_err(|e| CliError::FileSystem(format!("Failed to read entry: {}", e)))?;
let path = entry.path();
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
if name.contains(".backup.") {
backups.push(path);
}
}
}
backups.sort_by_key(|path| {
fs::metadata(path)
.and_then(|m| m.modified())
.unwrap_or(SystemTime::UNIX_EPOCH)
});
backups.reverse();
let mut removed = 0;
for backup in backups.iter().skip(keep_count) {
if fs::remove_file(backup).is_ok() {
removed += 1;
}
}
Ok(removed)
}
}
pub struct ProjectDetector;
impl ProjectDetector {
pub fn detect_project_type(path: &Path) -> Option<String> {
let files = fs::read_dir(path).ok()?;
let mut file_names = Vec::new();
for entry in files.flatten() {
if let Some(name) = entry.file_name().to_str() {
file_names.push(name.to_string());
}
}
if file_names.contains(&"Cargo.toml".to_string()) {
return Some("rust".to_string());
}
if file_names.contains(&"package.json".to_string()) {
return Some("javascript".to_string());
}
if file_names.contains(&"pyproject.toml".to_string()) ||
file_names.contains(&"setup.py".to_string()) ||
file_names.contains(&"requirements.txt".to_string()) {
return Some("python".to_string());
}
if file_names.contains(&"go.mod".to_string()) {
return Some("go".to_string());
}
if file_names.contains(&"pom.xml".to_string()) ||
file_names.contains(&"build.gradle".to_string()) {
return Some("java".to_string());
}
None
}
pub fn get_project_name(path: &Path) -> String {
path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_string()
}
pub fn is_project_root(path: &Path) -> bool {
Self::detect_project_type(path).is_some() || Utils::is_git_repo(path)
}
}
pub struct Validator;
impl Validator {
pub fn validate_model_name(model: &str) -> Result<(String, String)> {
let parts: Vec<&str> = model.split('/').collect();
if parts.len() != 2 {
return Err(CliError::InvalidInput(
"Model must be in format 'provider/model'".to_string(),
));
}
let provider = parts[0].trim();
let model = parts[1].trim();
if provider.is_empty() || model.is_empty() {
return Err(CliError::InvalidInput(
"Provider and model names cannot be empty".to_string(),
));
}
Ok((provider.to_string(), model.to_string()))
}
pub fn validate_session_id(session_id: &str) -> Result<()> {
if session_id.is_empty() {
return Err(CliError::InvalidInput("Session ID cannot be empty".to_string()));
}
if !session_id.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
return Err(CliError::InvalidInput(
"Session ID can only contain alphanumeric characters, hyphens, and underscores".to_string(),
));
}
Ok(())
}
pub fn validate_port(port: u16) -> Result<()> {
if port < 1024 {
return Err(CliError::InvalidInput(
"Port number must be 1024 or higher".to_string(),
));
}
Ok(())
}
pub fn validate_timeout(timeout: u64) -> Result<()> {
if timeout == 0 {
return Err(CliError::InvalidInput(
"Timeout must be greater than 0".to_string(),
));
}
if timeout > 3600 {
return Err(CliError::InvalidInput(
"Timeout cannot be more than 1 hour (3600 seconds)".to_string(),
));
}
Ok(())
}
}