actr_cli/
utils.rs

1//! Utility functions for actr-cli
2
3use crate::error::{ActrCliError, Result};
4use std::path::Path;
5use std::process::{Command, Output};
6use tokio::process::Command as TokioCommand;
7use tracing::{debug, info, warn};
8
9/// Execute a command and return the output
10#[allow(dead_code)]
11pub async fn execute_command(cmd: &str, args: &[&str], cwd: Option<&Path>) -> Result<Output> {
12    debug!("Executing command: {} {}", cmd, args.join(" "));
13
14    let mut command = TokioCommand::new(cmd);
15    command.args(args);
16
17    if let Some(cwd) = cwd {
18        command.current_dir(cwd);
19    }
20
21    let output = command.output().await?;
22
23    if !output.status.success() {
24        let stderr = String::from_utf8_lossy(&output.stderr);
25        return Err(ActrCliError::command_error(format!(
26            "Command '{}' failed with exit code {:?}: {}",
27            cmd,
28            output.status.code(),
29            stderr
30        )));
31    }
32
33    Ok(output)
34}
35
36/// Execute a command and stream its output
37pub async fn execute_command_streaming(cmd: &str, args: &[&str], cwd: Option<&Path>) -> Result<()> {
38    info!("Running: {} {}", cmd, args.join(" "));
39
40    let mut command = TokioCommand::new(cmd);
41    command.args(args);
42
43    if let Some(cwd) = cwd {
44        command.current_dir(cwd);
45    }
46
47    let status = command.status().await?;
48
49    if !status.success() {
50        return Err(ActrCliError::command_error(format!(
51            "Command '{}' failed with exit code {:?}",
52            cmd,
53            status.code()
54        )));
55    }
56
57    Ok(())
58}
59
60/// Check if a command is available in the system PATH
61pub fn command_exists(cmd: &str) -> bool {
62    Command::new("which")
63        .arg(cmd)
64        .output()
65        .map(|output| output.status.success())
66        .unwrap_or(false)
67}
68
69/// Check if required tools are available
70pub fn check_required_tools() -> Result<()> {
71    let required_tools = vec![
72        ("cargo", "Rust toolchain"),
73        ("protoc", "Protocol Buffers compiler"),
74    ];
75
76    let mut missing_tools = Vec::new();
77
78    for (tool, description) in required_tools {
79        if !command_exists(tool) {
80            missing_tools.push((tool, description));
81        }
82    }
83
84    if !missing_tools.is_empty() {
85        let mut error_msg = "Missing required tools:\n".to_string();
86        for (tool, description) in missing_tools {
87            error_msg.push_str(&format!("  - {tool} ({description})\n"));
88        }
89        error_msg.push_str("\nPlease install the missing tools and try again.");
90        return Err(ActrCliError::command_error(error_msg));
91    }
92
93    Ok(())
94}
95
96/// Find the workspace root by looking for Cargo.toml with [workspace]
97pub fn find_workspace_root() -> Result<Option<std::path::PathBuf>> {
98    let mut current = std::env::current_dir()?;
99
100    loop {
101        let cargo_toml = current.join("Cargo.toml");
102        if cargo_toml.exists() {
103            let content = std::fs::read_to_string(&cargo_toml)?;
104            if content.contains("[workspace]") {
105                return Ok(Some(current));
106            }
107        }
108
109        match current.parent() {
110            Some(parent) => current = parent.to_path_buf(),
111            None => break,
112        }
113    }
114
115    Ok(None)
116}
117
118/// Get the target directory for build outputs
119pub fn get_target_dir(project_root: &Path) -> std::path::PathBuf {
120    // Check for workspace root first
121    if let Ok(Some(workspace_root)) = find_workspace_root() {
122        workspace_root.join("target")
123    } else {
124        project_root.join("target")
125    }
126}
127
128/// Ensure a directory exists, creating it if necessary
129#[allow(dead_code)]
130pub fn ensure_dir_exists(path: &Path) -> Result<()> {
131    if !path.exists() {
132        debug!("Creating directory: {}", path.display());
133        std::fs::create_dir_all(path)?;
134    }
135    Ok(())
136}
137
138/// Copy a file, creating parent directories as needed
139#[allow(dead_code)]
140pub fn copy_file_with_dirs(from: &Path, to: &Path) -> Result<()> {
141    if let Some(parent) = to.parent() {
142        ensure_dir_exists(parent)?;
143    }
144    std::fs::copy(from, to)?;
145    Ok(())
146}
147
148/// Check if the current directory contains an Actr.toml file
149pub fn is_actr_project() -> bool {
150    Path::new("Actr.toml").exists()
151}
152
153/// Warn if not in an actr project directory
154pub fn warn_if_not_actr_project() {
155    if !is_actr_project() {
156        warn!("Not in an Actor-RTC project directory (no Actr.toml found)");
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163    use tempfile::TempDir;
164
165    #[test]
166    fn test_command_exists() {
167        // These commands should exist on most systems
168        assert!(command_exists("ls") || command_exists("dir"));
169        assert!(!command_exists("this_command_definitely_does_not_exist"));
170    }
171
172    #[test]
173    fn test_ensure_dir_exists() {
174        let temp_dir = TempDir::new().unwrap();
175        let test_path = temp_dir.path().join("test/nested/dir");
176
177        assert!(!test_path.exists());
178        ensure_dir_exists(&test_path).unwrap();
179        assert!(test_path.exists());
180
181        // Should not fail if directory already exists
182        ensure_dir_exists(&test_path).unwrap();
183    }
184
185    #[tokio::test]
186    async fn test_execute_command() {
187        // Test a simple command that should succeed
188        let result = execute_command("echo", &["hello"], None).await;
189        assert!(result.is_ok());
190
191        let output = result.unwrap();
192        let stdout = String::from_utf8_lossy(&output.stdout);
193        assert!(stdout.contains("hello"));
194    }
195}