intent_engine/setup/
common.rs1use crate::error::{IntentError, Result};
4use serde_json::Value;
5use std::env;
6use std::fs;
7use std::path::{Path, PathBuf};
8
9pub fn resolve_absolute_path(path: &Path) -> Result<PathBuf> {
11 path.canonicalize().or_else(|_| {
12 if path.is_absolute() {
15 Ok(path.to_path_buf())
16 } else {
17 let current_dir = env::current_dir().map_err(IntentError::IoError)?;
18 Ok(current_dir.join(path))
19 }
20 })
21}
22
23pub fn get_home_dir() -> Result<PathBuf> {
25 env::var("HOME")
26 .or_else(|_| env::var("USERPROFILE"))
27 .map(PathBuf::from)
28 .map_err(|_| IntentError::InvalidInput("Cannot determine home directory".to_string()))
29}
30
31pub fn create_backup(file_path: &Path) -> Result<Option<PathBuf>> {
33 if !file_path.exists() {
34 return Ok(None);
35 }
36
37 let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
38 let backup_path = file_path.with_extension(format!(
39 "{}.backup.{}",
40 file_path.extension().and_then(|s| s.to_str()).unwrap_or(""),
41 timestamp
42 ));
43
44 fs::copy(file_path, &backup_path).map_err(IntentError::IoError)?;
45 Ok(Some(backup_path))
46}
47
48pub fn read_json_config(path: &Path) -> Result<Value> {
50 if path.exists() {
51 let content = fs::read_to_string(path).map_err(IntentError::IoError)?;
52 serde_json::from_str(&content)
53 .map_err(|e| IntentError::InvalidInput(format!("Failed to parse JSON config: {}", e)))
54 } else {
55 Ok(serde_json::json!({}))
56 }
57}
58
59pub fn write_json_config(path: &Path, config: &Value) -> Result<()> {
61 if let Some(parent) = path.parent() {
63 fs::create_dir_all(parent).map_err(IntentError::IoError)?;
64 }
65
66 let content = serde_json::to_string_pretty(config)?;
67 fs::write(path, content).map_err(IntentError::IoError)?;
68 Ok(())
69}
70
71pub fn find_ie_binary() -> Result<PathBuf> {
73 if let Ok(current_exe) = env::current_exe() {
76 if let Some(file_name) = current_exe.file_name() {
78 let name = file_name.to_string_lossy();
79 if name == "ie"
80 || name.starts_with("ie.")
81 || name == "intent-engine"
82 || name.starts_with("intent-engine.")
83 {
84 return Ok(current_exe);
85 }
86 }
87 }
88
89 if let Ok(path) = env::var("CARGO_BIN_EXE_ie") {
91 let binary = PathBuf::from(path);
92 if binary.exists() {
93 return Ok(binary);
94 }
95 }
96
97 which::which("ie")
99 .or_else(|_| {
100 let home = get_home_dir()?;
102 let cargo_bin = home.join(".cargo").join("bin").join("ie");
103 if cargo_bin.exists() {
104 Ok(cargo_bin)
105 } else {
106 Err(IntentError::InvalidInput(
107 "ie binary not found in PATH or ~/.cargo/bin".to_string(),
108 ))
109 }
110 })
111 .or_else(|_| {
112 let candidate_paths = vec![
114 PathBuf::from("./target/debug/ie"),
115 PathBuf::from("./target/release/ie"),
116 PathBuf::from("../target/debug/ie"),
117 PathBuf::from("../target/release/ie"),
118 ];
119
120 for path in candidate_paths {
121 if path.exists() {
122 return Ok(path);
123 }
124 }
125
126 Err(IntentError::InvalidInput(
127 "ie binary not found in relative paths".to_string(),
128 ))
129 })
130 .or_else(|_| {
131 which::which("intent-engine").map_err(|_| {
133 IntentError::InvalidInput(
134 "intent-engine binary not found. Please install with: cargo install intent-engine".to_string()
135 )
136 })
137 })
138}
139
140#[cfg(unix)]
142pub fn set_executable(path: &Path) -> Result<()> {
143 use std::os::unix::fs::PermissionsExt;
144 let mut perms = fs::metadata(path)
145 .map_err(IntentError::IoError)?
146 .permissions();
147 perms.set_mode(0o755);
148 fs::set_permissions(path, perms).map_err(IntentError::IoError)?;
149 Ok(())
150}
151
152#[cfg(not(unix))]
154pub fn set_executable(_path: &Path) -> Result<()> {
155 Ok(())
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use std::fs;
162 use tempfile::TempDir;
163
164 #[test]
167 fn test_resolve_absolute_path() {
168 let _temp = TempDir::new().unwrap();
169 let rel_path = PathBuf::from("test.txt");
170 let abs_path = resolve_absolute_path(&rel_path).unwrap();
171 assert!(abs_path.is_absolute());
172 }
173
174 #[test]
175 fn test_resolve_absolute_path_already_absolute() {
176 let abs_path = PathBuf::from("/tmp/test.txt");
177 let result = resolve_absolute_path(&abs_path).unwrap();
178 assert!(result.is_absolute());
179 }
180
181 #[test]
182 fn test_resolve_absolute_path_relative() {
183 let rel_path = PathBuf::from("./test.txt");
184 let result = resolve_absolute_path(&rel_path).unwrap();
185 assert!(result.is_absolute());
186 }
187
188 #[test]
191 fn test_get_home_dir() {
192 let result = get_home_dir();
193 assert!(result.is_ok());
194
195 let home = result.unwrap();
196 assert!(home.is_absolute());
197 }
198
199 #[test]
202 fn test_create_backup_creates_file() {
203 let temp = TempDir::new().unwrap();
204 let file_path = temp.path().join("test.json");
205 fs::write(&file_path, "original content").unwrap();
206
207 let backup = create_backup(&file_path).unwrap();
209 assert!(backup.is_some());
210 let backup_path = backup.unwrap();
211 assert!(backup_path.exists());
212
213 let backup_content = fs::read_to_string(&backup_path).unwrap();
215 assert_eq!(backup_content, "original content");
216 }
217
218 #[test]
219 fn test_create_backup_nonexistent_file() {
220 let temp = TempDir::new().unwrap();
221 let file_path = temp.path().join("nonexistent.txt");
222
223 let backup = create_backup(&file_path).unwrap();
225 assert!(backup.is_none());
226 }
227
228 #[test]
229 fn test_create_backup_filename_format() {
230 let temp = TempDir::new().unwrap();
231 let file_path = temp.path().join("test.json");
232 fs::write(&file_path, "content").unwrap();
233
234 let backup = create_backup(&file_path).unwrap();
235 assert!(backup.is_some());
236
237 let backup_path = backup.unwrap();
238 let filename = backup_path.file_name().unwrap().to_string_lossy();
239
240 assert!(filename.contains(".backup."));
242 }
243
244 #[test]
247 fn test_json_config_ops() {
248 let temp = TempDir::new().unwrap();
249 let config_path = temp.path().join("config.json");
250
251 let config = read_json_config(&config_path).unwrap();
253 assert_eq!(config, serde_json::json!({}));
254
255 let test_config = serde_json::json!({
257 "key": "value",
258 "number": 42
259 });
260 write_json_config(&config_path, &test_config).unwrap();
261 assert!(config_path.exists());
262
263 let read_config = read_json_config(&config_path).unwrap();
265 assert_eq!(read_config, test_config);
266 }
267
268 #[test]
269 fn test_read_json_config_invalid_json() {
270 let temp = TempDir::new().unwrap();
271 let config_path = temp.path().join("invalid.json");
272
273 fs::write(&config_path, "{invalid json}").unwrap();
275
276 let result = read_json_config(&config_path);
278 assert!(result.is_err());
279 }
280
281 #[test]
282 fn test_write_json_config_creates_parent_dir() {
283 let temp = TempDir::new().unwrap();
284 let config_path = temp.path().join("nested").join("dir").join("config.json");
285
286 let test_config = serde_json::json!({"test": "value"});
287 write_json_config(&config_path, &test_config).unwrap();
288
289 assert!(config_path.parent().unwrap().exists());
291 assert!(config_path.exists());
292 }
293
294 #[test]
295 fn test_write_json_config_pretty_format() {
296 let temp = TempDir::new().unwrap();
297 let config_path = temp.path().join("config.json");
298
299 let test_config = serde_json::json!({
300 "key": "value",
301 "nested": {
302 "item": 123
303 }
304 });
305 write_json_config(&config_path, &test_config).unwrap();
306
307 let content = fs::read_to_string(&config_path).unwrap();
309
310 assert!(content.contains('\n'));
312 assert!(content.contains(" ")); }
314
315 #[test]
316 fn test_json_config_complex_types() {
317 let temp = TempDir::new().unwrap();
318 let config_path = temp.path().join("complex.json");
319
320 let test_config = serde_json::json!({
321 "string": "value",
322 "number": 42,
323 "boolean": true,
324 "null": null,
325 "array": [1, 2, 3],
326 "object": {
327 "nested": "value"
328 }
329 });
330
331 write_json_config(&config_path, &test_config).unwrap();
332 let read_config = read_json_config(&config_path).unwrap();
333
334 assert_eq!(read_config, test_config);
335 }
336
337 #[test]
340 fn test_find_ie_binary() {
341 let result = find_ie_binary();
343 match result {
345 Ok(path) => {
346 assert!(!path.to_string_lossy().is_empty());
348 },
349 Err(e) => {
350 let msg = format!("{:?}", e);
352 assert!(
353 msg.contains("binary not found")
354 || msg.contains("intent-engine")
355 || msg.contains("ie")
356 );
357 },
358 }
359 }
360
361 #[cfg(unix)]
364 #[test]
365 fn test_set_executable_unix() {
366 use std::os::unix::fs::PermissionsExt;
367
368 let temp = TempDir::new().unwrap();
369 let file_path = temp.path().join("script.sh");
370 fs::write(&file_path, "#!/bin/bash\necho test").unwrap();
371
372 set_executable(&file_path).unwrap();
374
375 let metadata = fs::metadata(&file_path).unwrap();
377 let permissions = metadata.permissions();
378 let mode = permissions.mode();
379
380 assert_ne!(mode & 0o111, 0); }
383
384 #[cfg(not(unix))]
385 #[test]
386 fn test_set_executable_non_unix() {
387 let temp = TempDir::new().unwrap();
388 let file_path = temp.path().join("script.sh");
389 fs::write(&file_path, "echo test").unwrap();
390
391 let result = set_executable(&file_path);
393 assert!(result.is_ok());
394 }
395}