1use 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#[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
36pub 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
60pub 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
69pub 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
96pub 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
118pub fn get_target_dir(project_root: &Path) -> std::path::PathBuf {
120 if let Ok(Some(workspace_root)) = find_workspace_root() {
122 workspace_root.join("target")
123 } else {
124 project_root.join("target")
125 }
126}
127
128pub fn to_pascal_case(input: &str) -> String {
130 heck::AsPascalCase(input).to_string()
131}
132
133pub fn to_snake_case(input: &str) -> String {
135 heck::AsSnakeCase(input).to_string()
136}
137
138#[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#[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
158pub fn is_actr_project() -> bool {
160 Path::new("Actr.toml").exists()
161}
162
163pub 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 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 ensure_dir_exists(&test_path).unwrap();
193 }
194
195 #[tokio::test]
196 async fn test_execute_command() {
197 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}