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
128#[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#[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
148pub fn is_actr_project() -> bool {
150 Path::new("Actr.toml").exists()
151}
152
153pub 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 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 ensure_dir_exists(&test_path).unwrap();
183 }
184
185 #[tokio::test]
186 async fn test_execute_command() {
187 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}