Skip to main content

torsh_cli/
utils.rs

1//! Utility functions for ToRSh CLI
2
3// Framework infrastructure - components designed for future use
4#![allow(dead_code)]
5use anyhow::{Context, Result};
6use byte_unit::Byte;
7use chrono::Local;
8use colored::*;
9// Console utilities available when needed
10use indicatif::{ProgressBar, ProgressStyle};
11use serde_json::Value;
12use std::collections::HashMap;
13use std::fmt::Write as FmtWrite;
14use std::path::{Path, PathBuf};
15use std::time::{Duration, Instant};
16use sysinfo::System;
17use tracing::{debug, info};
18
19/// Display the ToRSh banner
20pub fn display_banner() {
21    let banner = r#"
22  ______         _____   _____ _
23 |__   _|       |  __ \ / ____| |
24    | | ___  _ _| |__) | (___ | |__
25    | |/ _ \| '__|  _  / \___ \| '_ \
26   _| | (_) | |  | | \ \ ____) | | | |
27  |_| \___/|_|  |_|  \_\_____/|_| |_|
28
29"#;
30
31    println!("{}", banner.bright_cyan().bold());
32    println!(
33        "{}",
34        "ToRSh CLI - Advanced Deep Learning Framework Tools"
35            .bright_white()
36            .bold()
37    );
38    println!(
39        "{}",
40        format!("Version: {} | Build: {}", env!("CARGO_PKG_VERSION"), "dev").bright_black()
41    );
42    println!();
43}
44
45/// Output formatting utilities
46pub mod output {
47    use super::*;
48    use serde::Serialize;
49
50    /// Format output based on the specified format
51    pub fn format_output<T: Serialize>(data: &T, format: &str) -> Result<String> {
52        match format {
53            "json" => {
54                serde_json::to_string_pretty(data).with_context(|| "Failed to serialize to JSON")
55            }
56            "yaml" => serde_norway::to_string(data).with_context(|| "Failed to serialize to YAML"),
57            "table" => {
58                // For table format, we'll need to implement custom formatting
59                // This is a simplified version
60                format_as_table(data)
61            }
62            _ => {
63                anyhow::bail!("Unsupported output format: {}", format)
64            }
65        }
66    }
67
68    /// Format data as a table (simplified implementation)
69    fn format_as_table<T: Serialize>(data: &T) -> Result<String> {
70        let json_value = serde_json::to_value(data)?;
71        format_json_as_table(&json_value, 0)
72    }
73
74    fn format_json_as_table(value: &Value, indent: usize) -> Result<String> {
75        let mut output = String::new();
76        let indent_str = "  ".repeat(indent);
77
78        match value {
79            Value::Object(map) => {
80                for (key, val) in map {
81                    match val {
82                        Value::Object(_) | Value::Array(_) => {
83                            writeln!(output, "{}{}:", indent_str, key.bright_cyan())?;
84                            output.push_str(&format_json_as_table(val, indent + 1)?);
85                        }
86                        _ => {
87                            writeln!(
88                                output,
89                                "{}{}: {}",
90                                indent_str,
91                                key.bright_cyan(),
92                                format_json_value(val)
93                            )?;
94                        }
95                    }
96                }
97            }
98            Value::Array(arr) => {
99                for (i, val) in arr.iter().enumerate() {
100                    writeln!(output, "{}[{}]:", indent_str, i.to_string().bright_yellow())?;
101                    output.push_str(&format_json_as_table(val, indent + 1)?);
102                }
103            }
104            _ => {
105                writeln!(output, "{}{}", indent_str, format_json_value(value))?;
106            }
107        }
108
109        Ok(output)
110    }
111
112    fn format_json_value(value: &Value) -> String {
113        match value {
114            Value::String(s) => s.green().to_string(),
115            Value::Number(n) => n.to_string().yellow().to_string(),
116            Value::Bool(b) => {
117                if *b {
118                    "true".bright_green().to_string()
119                } else {
120                    "false".bright_red().to_string()
121                }
122            }
123            Value::Null => "null".bright_black().to_string(),
124            _ => value.to_string(),
125        }
126    }
127
128    /// Print a formatted table
129    pub fn print_table<T: Serialize>(title: &str, data: &T, format: &str) -> Result<()> {
130        println!("{}", title.bright_cyan().bold());
131        println!("{}", "=".repeat(title.len()).bright_cyan());
132        println!();
133
134        let formatted = format_output(data, format)?;
135        println!("{}", formatted);
136
137        Ok(())
138    }
139
140    /// Print a success message
141    pub fn print_success(message: &str) {
142        println!("{} {}", "✓".bright_green().bold(), message);
143    }
144
145    /// Print an error message
146    pub fn print_error(message: &str) {
147        eprintln!("{} {}", "✗".bright_red().bold(), message);
148    }
149
150    /// Print a warning message
151    pub fn print_warning(message: &str) {
152        println!("{} {}", "⚠".bright_yellow().bold(), message);
153    }
154
155    /// Print an info message
156    pub fn print_info(message: &str) {
157        println!("{} {}", "ℹ".bright_blue().bold(), message);
158    }
159}
160
161/// Progress bar utilities
162pub mod progress {
163    use super::*;
164
165    /// Create a progress bar with custom style
166    pub fn create_progress_bar(len: u64, message: &str) -> ProgressBar {
167        let pb = ProgressBar::new(len);
168        pb.set_style(
169            ProgressStyle::default_bar()
170                .template("{msg} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos:>7}/{len:7} {eta}")
171                .expect("Invalid progress bar template")
172                .progress_chars("█▉▊▋▌▍▎▏  "),
173        );
174        pb.set_message(message.to_string());
175        pb
176    }
177
178    /// Create a spinner for indeterminate progress
179    pub fn create_spinner(message: &str) -> ProgressBar {
180        let pb = ProgressBar::new_spinner();
181        pb.set_style(
182            ProgressStyle::default_spinner()
183                .template("{spinner:.cyan} {msg}")
184                .expect("Invalid spinner template")
185                .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ "),
186        );
187        pb.set_message(message.to_string());
188        pb
189    }
190}
191
192/// File system utilities
193pub mod fs {
194    use super::*;
195
196    /// Get file size as human-readable string
197    pub fn format_file_size(size: u64) -> String {
198        Byte::from_u128(size as u128)
199            .unwrap_or_else(|| Byte::from_u128(0).expect("zero bytes should always be valid"))
200            .get_appropriate_unit(byte_unit::UnitType::Binary)
201            .to_string()
202    }
203
204    /// Get directory size recursively
205    pub fn get_directory_size(
206        path: &Path,
207    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<u64>> + Send + '_>> {
208        Box::pin(async move {
209            let mut total_size = 0u64;
210            let mut read_dir = tokio::fs::read_dir(path).await?;
211
212            while let Some(entry) = read_dir.next_entry().await? {
213                let metadata = entry.metadata().await?;
214                if metadata.is_file() {
215                    total_size += metadata.len();
216                } else if metadata.is_dir() {
217                    total_size += get_directory_size(&entry.path()).await?;
218                }
219            }
220
221            Ok(total_size)
222        })
223    }
224
225    /// Find files matching a pattern
226    pub fn find_files(directory: &Path, pattern: &str) -> Result<Vec<PathBuf>> {
227        let mut files = Vec::new();
228        let walker = walkdir::WalkDir::new(directory);
229
230        for entry in walker {
231            let entry = entry?;
232            if entry.file_type().is_file() {
233                let path = entry.path();
234                if glob::Pattern::new(pattern)?.matches_path(path) {
235                    files.push(path.to_path_buf());
236                }
237            }
238        }
239
240        Ok(files)
241    }
242
243    /// Create a backup of a file
244    pub async fn backup_file(file_path: &Path) -> Result<PathBuf> {
245        let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
246        let backup_path = file_path.with_extension(format!(
247            "{}.backup_{}",
248            file_path.extension().unwrap_or_default().to_string_lossy(),
249            timestamp
250        ));
251
252        tokio::fs::copy(file_path, &backup_path).await?;
253        info!("Created backup: {}", backup_path.display());
254
255        Ok(backup_path)
256    }
257
258    /// Clean up temporary files
259    pub async fn cleanup_temp_files(temp_dir: &Path) -> Result<()> {
260        if temp_dir.exists() {
261            tokio::fs::remove_dir_all(temp_dir).await?;
262            debug!("Cleaned up temporary directory: {}", temp_dir.display());
263        }
264        Ok(())
265    }
266}
267
268/// System information utilities
269pub mod system {
270    use super::*;
271
272    /// System information structure
273    #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
274    pub struct SystemInfo {
275        pub os: String,
276        pub kernel_version: String,
277        pub total_memory: String,
278        pub available_memory: String,
279        pub cpu_count: usize,
280        pub cpu_brand: String,
281        pub cpu_frequency: u64,
282        pub load_average: Vec<f64>,
283        pub uptime: String,
284    }
285
286    /// Get comprehensive system information
287    pub fn get_system_info() -> SystemInfo {
288        let mut sys = System::new_all();
289        sys.refresh_all();
290
291        SystemInfo {
292            os: format!(
293                "{} {}",
294                System::name().unwrap_or_default(),
295                System::os_version().unwrap_or_default()
296            ),
297            kernel_version: System::kernel_version().unwrap_or_default(),
298            total_memory: format_memory(sys.total_memory()),
299            available_memory: format_memory(sys.available_memory()),
300            cpu_count: sys.cpus().len(),
301            cpu_brand: sys
302                .cpus()
303                .first()
304                .map(|cpu| cpu.brand())
305                .unwrap_or("Unknown")
306                .to_string(),
307            cpu_frequency: sys.cpus().first().map(|cpu| cpu.frequency()).unwrap_or(0),
308            load_average: {
309                let load = System::load_average();
310                vec![load.one, load.five, load.fifteen]
311            },
312            uptime: format_duration(Duration::from_secs(System::uptime())),
313        }
314    }
315
316    /// Format memory size.
317    ///
318    /// `memory_bytes` must already be in bytes: `sysinfo::System::total_memory` and
319    /// `available_memory` both return bytes directly (see sysinfo 0.39 docs), so no
320    /// unit conversion is needed here.
321    fn format_memory(memory_bytes: u64) -> String {
322        Byte::from_u128(memory_bytes as u128)
323            .unwrap_or_else(|| Byte::from_u128(0).expect("zero bytes should always be valid"))
324            .get_appropriate_unit(byte_unit::UnitType::Binary)
325            .to_string()
326    }
327
328    /// Check if GPU is available with real hardware detection
329    pub fn check_gpu_availability() -> HashMap<String, bool> {
330        let mut gpu_info = HashMap::new();
331
332        // Check for CUDA with actual detection
333        // #[cfg(feature = "cuda")]
334        // {
335        //     gpu_info.insert("CUDA".to_string(), detect_cuda_availability());
336        // }
337        // #[cfg(not(feature = "cuda"))]
338        {
339            // Still check for CUDA runtime even if not compiled with CUDA support
340            gpu_info.insert("CUDA".to_string(), detect_cuda_runtime());
341        }
342
343        // Check for ROCm with actual detection
344        // #[cfg(feature = "rocm")]
345        // {
346        //     gpu_info.insert("ROCm".to_string(), detect_rocm_availability());
347        // }
348        // #[cfg(not(feature = "rocm"))]
349        {
350            gpu_info.insert("ROCm".to_string(), detect_rocm_runtime());
351        }
352
353        // Check for Metal (macOS) with actual detection
354        #[cfg(target_os = "macos")]
355        {
356            gpu_info.insert("Metal".to_string(), detect_metal_availability());
357        }
358
359        // Check for Vulkan support
360        gpu_info.insert("Vulkan".to_string(), detect_vulkan_availability());
361
362        // Check for OpenCL
363        gpu_info.insert("OpenCL".to_string(), detect_opencl_availability());
364
365        gpu_info
366    }
367
368    /// Detect CUDA availability at runtime
369    // #[cfg(feature = "cuda")]
370    #[allow(dead_code)]
371    fn detect_cuda_availability() -> bool {
372        // This would use CUDA runtime API calls
373        // For now, check if CUDA libraries are present
374        detect_cuda_runtime()
375    }
376
377    fn detect_cuda_runtime() -> bool {
378        // Check for CUDA runtime by looking for nvidia-smi command
379        std::process::Command::new("nvidia-smi")
380            .arg("--query-gpu=name")
381            .arg("--format=csv,noheader")
382            .output()
383            .map(|output| output.status.success())
384            .unwrap_or(false)
385    }
386
387    /// Detect ROCm availability
388    // #[cfg(feature = "rocm")]
389    #[allow(dead_code)]
390    fn detect_rocm_availability() -> bool {
391        detect_rocm_runtime()
392    }
393
394    fn detect_rocm_runtime() -> bool {
395        // Check for ROCm by looking for rocm-smi command
396        std::process::Command::new("rocm-smi")
397            .arg("--showproductname")
398            .output()
399            .map(|output| output.status.success())
400            .unwrap_or(false)
401    }
402
403    /// Detect Metal availability (macOS only)
404    #[cfg(target_os = "macos")]
405    fn detect_metal_availability() -> bool {
406        // Check if Metal is available by running system_profiler
407        std::process::Command::new("system_profiler")
408            .arg("SPDisplaysDataType")
409            .output()
410            .map(|output| {
411                output.status.success() && String::from_utf8_lossy(&output.stdout).contains("Metal")
412            })
413            .unwrap_or(true) // Assume available on macOS if detection fails
414    }
415
416    fn detect_vulkan_availability() -> bool {
417        // Check for Vulkan by looking for vulkaninfo command
418        std::process::Command::new("vulkaninfo")
419            .arg("--summary")
420            .output()
421            .map(|output| output.status.success())
422            .unwrap_or(false)
423    }
424
425    fn detect_opencl_availability() -> bool {
426        // Check for OpenCL by looking for clinfo command
427        std::process::Command::new("clinfo")
428            .output()
429            .map(|output| output.status.success())
430            .unwrap_or(false)
431    }
432
433    /// Get comprehensive device information
434    pub fn get_device_info() -> HashMap<String, serde_json::Value> {
435        let mut device_info = HashMap::new();
436
437        // Get system info for CPU details
438        let sys_info = get_system_info();
439
440        // CPU Information
441        device_info.insert(
442            "cpu".to_string(),
443            serde_json::json!({
444                "available": true,
445                "device_type": "cpu",
446                "description": "CPU device",
447                "brand": sys_info.cpu_brand,
448                "cores": sys_info.cpu_count,
449                "frequency_mhz": sys_info.cpu_frequency,
450                "capabilities": get_cpu_capabilities(),
451            }),
452        );
453
454        // GPU Information with detailed detection
455        let gpu_availability = check_gpu_availability();
456        for (gpu_type, available) in gpu_availability {
457            let detailed_info = if available {
458                match gpu_type.as_str() {
459                    "CUDA" => get_cuda_device_details(),
460                    "ROCm" => get_rocm_device_details(),
461                    "Metal" => get_metal_device_details(),
462                    "Vulkan" => get_vulkan_device_details(),
463                    "OpenCL" => get_opencl_device_details(),
464                    _ => serde_json::json!({}),
465                }
466            } else {
467                serde_json::json!({
468                    "reason": "Runtime or drivers not detected"
469                })
470            };
471
472            device_info.insert(
473                gpu_type.to_lowercase(),
474                serde_json::json!({
475                    "available": available,
476                    "device_type": "gpu",
477                    "description": format!("{} GPU device", gpu_type),
478                    "details": detailed_info
479                }),
480            );
481        }
482
483        device_info
484    }
485
486    /// Get CPU capabilities (SIMD instructions, etc.)
487    fn get_cpu_capabilities() -> Vec<String> {
488        let mut capabilities = Vec::new();
489
490        // Check for common SIMD instruction sets
491        #[cfg(target_arch = "x86_64")]
492        {
493            if is_x86_feature_detected!("sse") {
494                capabilities.push("SSE".to_string());
495            }
496            if is_x86_feature_detected!("sse2") {
497                capabilities.push("SSE2".to_string());
498            }
499            if is_x86_feature_detected!("sse3") {
500                capabilities.push("SSE3".to_string());
501            }
502            if is_x86_feature_detected!("sse4.1") {
503                capabilities.push("SSE4.1".to_string());
504            }
505            if is_x86_feature_detected!("sse4.2") {
506                capabilities.push("SSE4.2".to_string());
507            }
508            if is_x86_feature_detected!("avx") {
509                capabilities.push("AVX".to_string());
510            }
511            if is_x86_feature_detected!("avx2") {
512                capabilities.push("AVX2".to_string());
513            }
514            if is_x86_feature_detected!("fma") {
515                capabilities.push("FMA".to_string());
516            }
517        }
518
519        #[cfg(target_arch = "aarch64")]
520        {
521            if std::arch::is_aarch64_feature_detected!("neon") {
522                capabilities.push("NEON".to_string());
523            }
524        }
525
526        capabilities
527    }
528
529    /// Get detailed CUDA device information
530    fn get_cuda_device_details() -> serde_json::Value {
531        // Use nvidia-smi to get device details
532        if let Ok(output) = std::process::Command::new("nvidia-smi")
533            .arg("--query-gpu=name,memory.total,driver_version,cuda_version")
534            .arg("--format=csv,noheader,nounits")
535            .output()
536        {
537            if output.status.success() {
538                let info = String::from_utf8_lossy(&output.stdout);
539                let lines: Vec<&str> = info.trim().split('\n').collect();
540
541                return serde_json::json!({
542                    "devices": lines.iter().enumerate().map(|(i, line)| {
543                        let parts: Vec<&str> = line.split(',').map(|s| s.trim()).collect();
544                        if parts.len() >= 4 {
545                            serde_json::json!({
546                                "id": i,
547                                "name": parts[0],
548                                "memory_mb": parts[1],
549                                "driver_version": parts[2],
550                                "cuda_version": parts[3]
551                            })
552                        } else {
553                            serde_json::json!({
554                                "id": i,
555                                "name": "Unknown GPU",
556                                "error": "Failed to parse GPU info"
557                            })
558                        }
559                    }).collect::<Vec<_>>()
560                });
561            }
562        }
563
564        serde_json::json!({ "error": "Failed to query CUDA devices" })
565    }
566
567    /// Get detailed ROCm device information
568    fn get_rocm_device_details() -> serde_json::Value {
569        if let Ok(output) = std::process::Command::new("rocm-smi")
570            .arg("--showproductname")
571            .arg("--showmeminfo=vram")
572            .output()
573        {
574            if output.status.success() {
575                return serde_json::json!({
576                    "detected": true,
577                    "raw_output": String::from_utf8_lossy(&output.stdout)
578                });
579            }
580        }
581
582        serde_json::json!({ "error": "Failed to query ROCm devices" })
583    }
584
585    /// Get detailed Metal device information (macOS only)
586    #[cfg(target_os = "macos")]
587    fn get_metal_device_details() -> serde_json::Value {
588        if let Ok(output) = std::process::Command::new("system_profiler")
589            .arg("SPDisplaysDataType")
590            .arg("-detailLevel")
591            .arg("full")
592            .output()
593        {
594            if output.status.success() {
595                let info = String::from_utf8_lossy(&output.stdout);
596                return serde_json::json!({
597                    "detected": true,
598                    "metal_support": info.contains("Metal"),
599                    "summary": "Metal GPU acceleration available"
600                });
601            }
602        }
603
604        serde_json::json!({ "error": "Failed to query Metal devices" })
605    }
606
607    #[cfg(not(target_os = "macos"))]
608    fn get_metal_device_details() -> serde_json::Value {
609        serde_json::json!({ "error": "Metal is only available on macOS" })
610    }
611
612    /// Get Vulkan device information
613    fn get_vulkan_device_details() -> serde_json::Value {
614        if let Ok(output) = std::process::Command::new("vulkaninfo")
615            .arg("--summary")
616            .output()
617        {
618            if output.status.success() {
619                return serde_json::json!({
620                    "detected": true,
621                    "summary": "Vulkan runtime available"
622                });
623            }
624        }
625
626        serde_json::json!({ "error": "Failed to query Vulkan devices" })
627    }
628
629    /// Get OpenCL device information
630    fn get_opencl_device_details() -> serde_json::Value {
631        if let Ok(output) = std::process::Command::new("clinfo").arg("--list").output() {
632            if output.status.success() {
633                let info = String::from_utf8_lossy(&output.stdout);
634                return serde_json::json!({
635                    "detected": true,
636                    "devices_summary": info.lines().take(10).collect::<Vec<_>>()
637                });
638            }
639        }
640
641        serde_json::json!({ "error": "Failed to query OpenCL devices" })
642    }
643
644    #[cfg(test)]
645    mod tests {
646        use super::*;
647
648        #[test]
649        fn test_format_memory_treats_input_as_bytes() {
650            // sysinfo::System::total_memory()/available_memory() return bytes directly
651            // (sysinfo 0.39+), so format_memory must NOT multiply by 1024 again.
652            let eight_gib_in_bytes: u64 = 8 * 1024 * 1024 * 1024;
653            let formatted = format_memory(eight_gib_in_bytes);
654
655            assert!(
656                formatted.contains("GiB"),
657                "expected GiB-scale output for 8 GiB of bytes, got: {formatted}"
658            );
659            assert!(
660                !formatted.contains("TiB"),
661                "format_memory inflated bytes by 1024x (regression), got: {formatted}"
662            );
663        }
664    }
665}
666
667/// Time and duration utilities
668pub mod time {
669    use super::*;
670
671    /// Format duration as human-readable string
672    pub fn format_duration(duration: Duration) -> String {
673        let secs = duration.as_secs();
674        if secs < 60 {
675            format!("{}s", secs)
676        } else if secs < 3600 {
677            format!("{}m {}s", secs / 60, secs % 60)
678        } else if secs < 86400 {
679            format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
680        } else {
681            format!("{}d {}h", secs / 86400, (secs % 86400) / 3600)
682        }
683    }
684
685    /// Get current timestamp as string
686    pub fn current_timestamp() -> String {
687        Local::now().format("%Y-%m-%d %H:%M:%S").to_string()
688    }
689
690    /// Parse human-readable duration
691    pub fn parse_duration(s: &str) -> Result<Duration> {
692        humantime::parse_duration(s).with_context(|| format!("Failed to parse duration: {}", s))
693    }
694
695    /// Measure execution time
696    pub async fn measure_time<F, T>(f: F) -> (T, Duration)
697    where
698        F: std::future::Future<Output = T>,
699    {
700        let start = Instant::now();
701        let result = f.await;
702        let duration = start.elapsed();
703        (result, duration)
704    }
705}
706
707/// Network utilities
708pub mod network {
709    use super::*;
710
711    /// Download a file with progress
712    pub async fn download_file_with_progress(
713        url: &str,
714        output_path: &Path,
715        show_progress: bool,
716    ) -> Result<()> {
717        let client = crate::tls::client_builder()?.build()?;
718        let response = client.get(url).send().await?;
719
720        let total_size = response.content_length().unwrap_or(0);
721
722        let pb = if show_progress && total_size > 0 {
723            Some(progress::create_progress_bar(
724                total_size,
725                &format!(
726                    "Downloading {}",
727                    output_path
728                        .file_name()
729                        .unwrap_or_default()
730                        .to_string_lossy()
731                ),
732            ))
733        } else {
734            None
735        };
736
737        let mut file = tokio::fs::File::create(output_path).await?;
738        let mut downloaded = 0u64;
739        let mut stream = response.bytes_stream();
740
741        use futures_util::StreamExt;
742        use tokio::io::AsyncWriteExt;
743
744        while let Some(chunk) = stream.next().await {
745            let chunk = chunk?;
746            file.write_all(&chunk).await?;
747            downloaded += chunk.len() as u64;
748
749            if let Some(pb) = &pb {
750                pb.set_position(downloaded);
751            }
752        }
753
754        if let Some(pb) = pb {
755            pb.finish_with_message("Download completed");
756        }
757
758        Ok(())
759    }
760
761    /// Check if URL is accessible
762    pub async fn check_url_accessible(url: &str) -> bool {
763        let client = match crate::tls::client_builder().and_then(|b| b.build().map_err(Into::into))
764        {
765            Ok(client) => client,
766            Err(_) => return false,
767        };
768        client.head(url).send().await.is_ok()
769    }
770}
771
772/// Validation utilities
773pub mod validation {
774    use super::*;
775
776    /// Validate file exists and is readable
777    pub fn validate_file_exists(path: &Path) -> Result<()> {
778        if !path.exists() {
779            anyhow::bail!("File does not exist: {}", path.display());
780        }
781        if !path.is_file() {
782            anyhow::bail!("Path is not a file: {}", path.display());
783        }
784        Ok(())
785    }
786
787    /// Validate directory exists and is accessible
788    pub fn validate_directory_exists(path: &Path) -> Result<()> {
789        if !path.exists() {
790            anyhow::bail!("Directory does not exist: {}", path.display());
791        }
792        if !path.is_dir() {
793            anyhow::bail!("Path is not a directory: {}", path.display());
794        }
795        Ok(())
796    }
797
798    /// Validate model format
799    pub fn validate_model_format(format: &str) -> Result<()> {
800        let supported_formats = ["torsh", "pytorch", "onnx", "tensorflow", "tflite"];
801        if !supported_formats.contains(&format) {
802            anyhow::bail!(
803                "Unsupported model format: {}. Supported formats: {}",
804                format,
805                supported_formats.join(", ")
806            );
807        }
808        Ok(())
809    }
810
811    /// Validate device string
812    pub fn validate_device(device: &str) -> Result<()> {
813        if device == "cpu" {
814            return Ok(());
815        }
816
817        if device.starts_with("cuda") {
818            let parts: Vec<&str> = device.split(':').collect();
819            if parts.len() == 2 {
820                if parts[1].parse::<usize>().is_err() {
821                    anyhow::bail!("Invalid CUDA device ID: {}", parts[1]);
822                }
823                return Ok(());
824            } else if parts.len() == 1 && parts[0] == "cuda" {
825                return Ok(());
826            }
827        }
828
829        if device == "metal" {
830            return Ok(());
831        }
832
833        anyhow::bail!(
834            "Invalid device format: {}. Use 'cpu', 'cuda', 'cuda:N', or 'metal'",
835            device
836        );
837    }
838}
839
840/// Interactive utilities
841pub mod interactive {
842    use super::*;
843    use dialoguer::{Confirm, Input, Select};
844
845    /// Ask user for confirmation
846    pub fn confirm(message: &str, default: bool) -> Result<bool> {
847        Confirm::new()
848            .with_prompt(message)
849            .default(default)
850            .interact()
851            .with_context(|| "Failed to get user confirmation")
852    }
853
854    /// Get text input from user
855    pub fn input<T>(message: &str, default: Option<T>) -> Result<T>
856    where
857        T: Clone + std::fmt::Display + std::str::FromStr,
858        T::Err: std::fmt::Display + std::fmt::Debug + Send + Sync + 'static,
859    {
860        let mut input = Input::new().with_prompt(message);
861
862        if let Some(default_value) = default {
863            input = input.default(default_value);
864        }
865
866        input
867            .interact_text()
868            .with_context(|| "Failed to get user input")
869    }
870
871    /// Select from a list of options
872    pub fn select(message: &str, options: &[String]) -> Result<usize> {
873        Select::new()
874            .with_prompt(message)
875            .items(options)
876            .interact()
877            .with_context(|| "Failed to get user selection")
878    }
879}
880
881/// Export format_duration function at module level
882pub use time::format_duration;
883
884#[cfg(test)]
885mod tests {
886    use super::*;
887    use tempfile::tempdir;
888
889    #[test]
890    fn test_format_duration() {
891        assert_eq!(time::format_duration(Duration::from_secs(30)), "30s");
892        assert_eq!(time::format_duration(Duration::from_secs(90)), "1m 30s");
893        assert_eq!(time::format_duration(Duration::from_secs(3661)), "1h 1m");
894    }
895
896    #[test]
897    fn test_validation() {
898        assert!(validation::validate_model_format("torsh").is_ok());
899        assert!(validation::validate_model_format("invalid").is_err());
900
901        assert!(validation::validate_device("cpu").is_ok());
902        assert!(validation::validate_device("cuda:0").is_ok());
903        assert!(validation::validate_device("invalid").is_err());
904    }
905
906    #[tokio::test]
907    async fn test_file_operations() {
908        let temp_dir = tempdir().unwrap();
909        let test_file = temp_dir.path().join("test.txt");
910
911        tokio::fs::write(&test_file, "test content").await.unwrap();
912
913        let size = fs::get_directory_size(temp_dir.path()).await.unwrap();
914        assert!(size > 0);
915
916        let backup = fs::backup_file(&test_file).await.unwrap();
917        assert!(backup.exists());
918    }
919
920    #[test]
921    fn test_output_formatting() {
922        use serde_json::json;
923
924        let data = json!({
925            "name": "test",
926            "value": 42,
927            "active": true
928        });
929
930        let json_output = output::format_output(&data, "json").unwrap();
931        assert!(json_output.contains("test"));
932
933        let yaml_output = output::format_output(&data, "yaml").unwrap();
934        assert!(yaml_output.contains("name: test"));
935    }
936}