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/// Convert a string to PascalCase using heck crate
129pub fn to_pascal_case(input: &str) -> String {
130    heck::AsPascalCase(input).to_string()
131}
132
133/// Convert a string to snake_case using heck crate
134pub fn to_snake_case(input: &str) -> String {
135    heck::AsSnakeCase(input).to_string()
136}
137
138/// Ensure a directory exists, creating it if necessary
139#[allow(dead_code)]
140pub fn ensure_dir_exists(path: &Path) -> Result<()> {
141    if !path.exists() {
142        debug!("Creating directory: {}", path.display());
143        std::fs::create_dir_all(path)?;
144    }
145    Ok(())
146}
147
148/// Copy a file, creating parent directories as needed
149#[allow(dead_code)]
150pub fn copy_file_with_dirs(from: &Path, to: &Path) -> Result<()> {
151    if let Some(parent) = to.parent() {
152        ensure_dir_exists(parent)?;
153    }
154    std::fs::copy(from, to)?;
155    Ok(())
156}
157
158/// Check if the current directory contains an Actr.toml file
159pub fn is_actr_project() -> bool {
160    Path::new("Actr.toml").exists()
161}
162
163/// Warn if not in an actr project directory
164pub fn warn_if_not_actr_project() {
165    if !is_actr_project() {
166        warn!("Not in an Actor-RTC project directory (no Actr.toml found)");
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use tempfile::TempDir;
174
175    #[test]
176    fn test_command_exists() {
177        // These commands should exist on most systems
178        assert!(command_exists("ls") || command_exists("dir"));
179        assert!(!command_exists("this_command_definitely_does_not_exist"));
180    }
181
182    #[test]
183    fn test_ensure_dir_exists() {
184        let temp_dir = TempDir::new().unwrap();
185        let test_path = temp_dir.path().join("test/nested/dir");
186
187        assert!(!test_path.exists());
188        ensure_dir_exists(&test_path).unwrap();
189        assert!(test_path.exists());
190
191        // Should not fail if directory already exists
192        ensure_dir_exists(&test_path).unwrap();
193    }
194
195    #[tokio::test]
196    async fn test_execute_command() {
197        // Test a simple command that should succeed
198        let result = execute_command("echo", &["hello"], None).await;
199        assert!(result.is_ok());
200
201        let output = result.unwrap();
202        let stdout = String::from_utf8_lossy(&output.stdout);
203        assert!(stdout.contains("hello"));
204    }
205}