foundry_mcp/core/installation/
utils.rs

1//! Common utilities for MCP server installation
2
3use anyhow::{Context, Result};
4use std::env;
5use std::path::PathBuf;
6
7/// Result of an installation operation
8#[derive(Debug, Clone)]
9pub struct InstallationResult {
10    pub success: bool,
11    pub config_path: String,
12    pub actions_taken: Vec<String>,
13}
14
15/// Result of an uninstallation operation
16#[derive(Debug, Clone)]
17pub struct UninstallationResult {
18    pub success: bool,
19    pub config_path: String,
20    pub actions_taken: Vec<String>,
21    pub files_removed: Vec<String>,
22}
23
24/// Detect the current binary path
25///
26/// Attempts to detect the path of the currently running foundry binary
27/// This is used for creating MCP server configurations
28pub fn detect_binary_path() -> Result<String> {
29    let current_exe = env::current_exe().context("Failed to get current executable path")?;
30
31    let binary_path = current_exe
32        .to_str()
33        .context("Binary path contains invalid Unicode characters")?
34        .to_string();
35
36    Ok(binary_path)
37}
38
39/// Check if the binary at the given path is accessible
40pub fn check_binary_accessible(binary_path: &str) -> bool {
41    let path = PathBuf::from(binary_path);
42    path.exists() && path.is_file()
43}
44
45/// Validate that a binary path exists and is executable
46pub fn validate_binary_path(binary_path: &str) -> Result<()> {
47    let path = PathBuf::from(binary_path);
48
49    if !path.exists() {
50        return Err(anyhow::anyhow!(
51            "Binary path does not exist: {}",
52            binary_path
53        ));
54    }
55
56    if !path.is_file() {
57        return Err(anyhow::anyhow!(
58            "Binary path is not a file: {}",
59            binary_path
60        ));
61    }
62
63    Ok(())
64}
65
66/// Create a standardized installation result
67pub fn create_installation_result(
68    success: bool,
69    config_path: String,
70    actions_taken: Vec<String>,
71) -> InstallationResult {
72    InstallationResult {
73        success,
74        config_path,
75        actions_taken,
76    }
77}
78
79/// Create a standardized uninstallation result
80pub fn create_uninstallation_result(
81    success: bool,
82    config_path: String,
83    actions_taken: Vec<String>,
84    files_removed: Vec<String>,
85) -> UninstallationResult {
86    UninstallationResult {
87        success,
88        config_path,
89        actions_taken,
90        files_removed,
91    }
92}
93
94/// Check if a file exists at the given path
95pub fn file_exists(path: &str) -> bool {
96    PathBuf::from(path).exists()
97}
98
99/// Read file content if it exists, otherwise return None
100pub fn read_file_content(path: &str) -> Option<String> {
101    std::fs::read_to_string(path).ok()
102}
103
104/// Format actions taken into human-readable strings
105pub fn format_actions(actions: &[String]) -> Vec<String> {
106    actions
107        .iter()
108        .map(|action| format!("• {}", action))
109        .collect()
110}
111
112/// Get the current user's home directory
113pub fn get_home_dir() -> Result<PathBuf> {
114    dirs::home_dir().context("Failed to determine home directory")
115}
116
117/// Create a directory if it doesn't exist
118pub fn ensure_directory_exists(dir_path: &PathBuf) -> Result<()> {
119    if !dir_path.exists() {
120        std::fs::create_dir_all(dir_path)
121            .context(format!("Failed to create directory: {:?}", dir_path))?;
122    }
123    Ok(())
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn test_detect_binary_path() {
132        let result = detect_binary_path();
133        assert!(result.is_ok(), "Should be able to detect binary path");
134        let path = result.unwrap();
135        assert!(!path.is_empty(), "Binary path should not be empty");
136        assert!(
137            path.contains("foundry"),
138            "Binary path should contain 'foundry'"
139        );
140    }
141
142    #[test]
143    fn test_validate_binary_path_valid() {
144        let binary_path = detect_binary_path().unwrap();
145        let result = validate_binary_path(&binary_path);
146        assert!(result.is_ok(), "Valid binary path should pass validation");
147    }
148
149    #[test]
150    fn test_validate_binary_path_invalid() {
151        let result = validate_binary_path("/nonexistent/path");
152        assert!(result.is_err(), "Nonexistent path should fail validation");
153    }
154
155    #[test]
156    fn test_get_home_dir() {
157        let result = get_home_dir();
158        assert!(result.is_ok(), "Should be able to get home directory");
159        let home = result.unwrap();
160        assert!(home.exists(), "Home directory should exist");
161        assert!(home.is_dir(), "Home directory should be a directory");
162    }
163
164    #[test]
165    fn test_create_installation_result() {
166        let result = create_installation_result(
167            true,
168            "/path/to/config".to_string(),
169            vec![
170                "Created config file".to_string(),
171                "Updated environment".to_string(),
172            ],
173        );
174
175        assert!(result.success);
176        assert_eq!(result.config_path, "/path/to/config");
177        assert_eq!(result.actions_taken.len(), 2);
178    }
179
180    #[test]
181    fn test_format_actions() {
182        let actions = vec![
183            "Created config file".to_string(),
184            "Updated environment".to_string(),
185        ];
186
187        let formatted = format_actions(&actions);
188        assert_eq!(formatted.len(), 2);
189        assert!(formatted[0].starts_with("• "));
190        assert!(formatted[1].starts_with("• "));
191    }
192
193    #[test]
194    fn test_format_actions_empty() {
195        let actions: Vec<String> = vec![];
196        let formatted = format_actions(&actions);
197        assert!(formatted.is_empty());
198    }
199
200    #[test]
201    fn test_file_exists() {
202        let temp_dir = tempfile::tempdir().unwrap();
203        let file_path = temp_dir.path().join("test.txt");
204        std::fs::write(&file_path, "test").unwrap();
205
206        assert!(file_exists(file_path.to_str().unwrap()));
207        assert!(!file_exists("/nonexistent/file"));
208    }
209
210    #[test]
211    fn test_read_file_content() {
212        let temp_dir = tempfile::tempdir().unwrap();
213        let file_path = temp_dir.path().join("test.txt");
214        std::fs::write(&file_path, "test content").unwrap();
215
216        let content = read_file_content(file_path.to_str().unwrap());
217        assert_eq!(content, Some("test content".to_string()));
218
219        let nonexistent = read_file_content("/nonexistent/file");
220        assert!(nonexistent.is_none());
221    }
222
223    #[test]
224    fn test_ensure_directory_exists() {
225        let temp_dir = tempfile::tempdir().unwrap();
226        let sub_dir = temp_dir.path().join("subdir").join("nested");
227
228        let result = ensure_directory_exists(&sub_dir);
229        assert!(result.is_ok());
230        assert!(sub_dir.exists());
231        assert!(sub_dir.is_dir());
232    }
233
234    #[test]
235    fn test_create_uninstallation_result() {
236        let result = create_uninstallation_result(
237            false,
238            "/path/to/config.json".to_string(),
239            vec!["Action 1".to_string()],
240            vec!["file1.json".to_string()],
241        );
242
243        assert!(!result.success);
244        assert_eq!(result.config_path, "/path/to/config.json");
245        assert_eq!(result.actions_taken.len(), 1);
246        assert_eq!(result.files_removed.len(), 1);
247    }
248
249    #[test]
250    fn test_check_binary_accessible_valid() {
251        let binary_path = detect_binary_path().unwrap();
252        let accessible = check_binary_accessible(&binary_path);
253        assert!(accessible, "Current binary should be accessible");
254    }
255
256    #[test]
257    fn test_check_binary_accessible_invalid() {
258        let accessible = check_binary_accessible("/nonexistent/path");
259        assert!(!accessible, "Nonexistent path should not be accessible");
260    }
261}