1use std::path::{Path, PathBuf};
2
3pub const FORGE_IMAGES_DIR: &str = ".forge-images";
5
6pub fn make_path_relative(path: &str, worktree_path: &str) -> String {
9 tracing::debug!("Making path relative: {} -> {}", path, worktree_path);
10
11 let path_obj = normalize_macos_private_alias(Path::new(&path));
12 let worktree_path_obj = normalize_macos_private_alias(Path::new(worktree_path));
13
14 if path_obj.is_relative() {
16 return path.to_string();
17 }
18
19 if let Ok(relative_path) = path_obj.strip_prefix(&worktree_path_obj) {
20 let result = relative_path.to_string_lossy().to_string();
21 tracing::debug!("Successfully made relative: '{}' -> '{}'", path, result);
22 if result.is_empty() {
23 return ".".to_string();
24 }
25 return result;
26 }
27
28 if !path_obj.exists() || !worktree_path_obj.exists() {
29 return path.to_string();
30 }
31
32 let canonical_path = std::fs::canonicalize(&path_obj);
34 let canonical_worktree = std::fs::canonicalize(&worktree_path_obj);
35
36 match (canonical_path, canonical_worktree) {
37 (Ok(canon_path), Ok(canon_worktree)) => {
38 tracing::debug!(
39 "Trying canonical path resolution: '{}' -> '{}', '{}' -> '{}'",
40 path,
41 canon_path.display(),
42 worktree_path,
43 canon_worktree.display()
44 );
45
46 if !canon_path.starts_with(&canon_worktree) {
49 tracing::debug!(
50 "Path is outside worktree root (cross-root scenario): '{}' not under '{}', returning absolute path",
51 canon_path.display(),
52 canon_worktree.display()
53 );
54 return path.to_string();
55 }
56
57 match canon_path.strip_prefix(&canon_worktree) {
58 Ok(relative_path) => {
59 let result = relative_path.to_string_lossy().to_string();
60 tracing::debug!(
61 "Successfully made relative with canonical paths: '{}' -> '{}'",
62 path,
63 result
64 );
65 if result.is_empty() {
66 return ".".to_string();
67 }
68 result
69 }
70 Err(e) => {
71 tracing::warn!(
72 "Failed to make canonical path relative: '{}' relative to '{}', error: {}, returning original",
73 canon_path.display(),
74 canon_worktree.display(),
75 e
76 );
77 path.to_string()
78 }
79 }
80 }
81 _ => {
82 tracing::debug!(
83 "Could not canonicalize paths (paths may not exist): '{}', '{}', returning original",
84 path,
85 worktree_path
86 );
87 path.to_string()
88 }
89 }
90}
91
92fn normalize_macos_private_alias<P: AsRef<Path>>(p: P) -> PathBuf {
95 let p = p.as_ref();
96 if cfg!(target_os = "macos")
97 && let Some(s) = p.to_str()
98 {
99 if s == "/private/var" {
100 return PathBuf::from("/var");
101 }
102 if let Some(rest) = s.strip_prefix("/private/var/") {
103 return PathBuf::from(format!("/var/{rest}"));
104 }
105 if s == "/private/tmp" {
106 return PathBuf::from("/tmp");
107 }
108 if let Some(rest) = s.strip_prefix("/private/tmp/") {
109 return PathBuf::from(format!("/tmp/{rest}"));
110 }
111 }
112 p.to_path_buf()
113}
114
115pub fn get_automagik_forge_temp_dir() -> std::path::PathBuf {
116 let dir_name = if cfg!(debug_assertions) {
117 "automagik-forge-dev"
118 } else {
119 "automagik-forge"
120 };
121
122 if cfg!(target_os = "macos") {
123 std::env::temp_dir().join(dir_name)
125 } else if cfg!(target_os = "linux") {
126 std::path::PathBuf::from("/var/tmp").join(dir_name)
128 } else {
129 std::env::temp_dir().join(dir_name)
131 }
132}
133
134pub fn expand_tilde(path_str: &str) -> std::path::PathBuf {
136 shellexpand::tilde(path_str).as_ref().into()
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn test_make_path_relative() {
145 assert_eq!(
147 make_path_relative("src/main.rs", "/tmp/test-worktree"),
148 "src/main.rs"
149 );
150
151 let test_worktree = "/tmp/test-worktree";
153 let absolute_path = format!("{test_worktree}/src/main.rs");
154 let result = make_path_relative(&absolute_path, test_worktree);
155 assert_eq!(result, "src/main.rs");
156
157 assert_eq!(
159 make_path_relative("/other/path/file.js", "/tmp/test-worktree"),
160 "/other/path/file.js"
161 );
162 }
163
164 #[cfg(target_os = "macos")]
165 #[test]
166 fn test_make_path_relative_macos_private_alias() {
167 let worktree = "/var/folders/zz/abc123/T/automagik-forge-dev/worktrees/af-test";
169 let path_under_private = format!(
170 "/private/var{}/hello-world.txt",
171 worktree.strip_prefix("/var").unwrap()
172 );
173 assert_eq!(
174 make_path_relative(&path_under_private, worktree),
175 "hello-world.txt"
176 );
177
178 let worktree_private = format!("/private{worktree}");
180 let path_under_var = format!("{worktree}/hello-world.txt");
181 assert_eq!(
182 make_path_relative(&path_under_var, &worktree_private),
183 "hello-world.txt"
184 );
185 }
186}