cascade_cli/utils/
platform.rs1use std::path::{Path, PathBuf};
2
3pub fn path_separator() -> &'static str {
10 if cfg!(windows) {
11 ";"
12 } else {
13 ":"
14 }
15}
16
17pub fn executable_extension() -> &'static str {
19 if cfg!(windows) {
20 ".exe"
21 } else {
22 ""
23 }
24}
25
26pub fn executable_name(name: &str) -> String {
28 format!("{}{}", name, executable_extension())
29}
30
31pub fn is_executable(path: &Path) -> bool {
33 #[cfg(unix)]
34 {
35 use std::os::unix::fs::PermissionsExt;
36 if let Ok(metadata) = std::fs::metadata(path) {
37 metadata.permissions().mode() & 0o111 != 0
39 } else {
40 false
41 }
42 }
43
44 #[cfg(windows)]
45 {
46 if !path.exists() {
48 return false;
49 }
50
51 if let Some(extension) = path.extension() {
52 let ext = extension.to_string_lossy().to_lowercase();
53 matches!(ext.as_str(), "exe" | "bat" | "cmd" | "com" | "scr" | "ps1")
54 } else {
55 false
56 }
57 }
58}
59
60#[cfg(unix)]
62pub fn make_executable(path: &Path) -> std::io::Result<()> {
63 use std::os::unix::fs::PermissionsExt;
64
65 let mut perms = std::fs::metadata(path)?.permissions();
66 let current_mode = perms.mode();
68 let new_mode = current_mode | ((current_mode & 0o444) >> 2);
69 perms.set_mode(new_mode);
70 std::fs::set_permissions(path, perms)
71}
72
73#[cfg(windows)]
74pub fn make_executable(_path: &Path) -> std::io::Result<()> {
75 Ok(())
78}
79
80pub fn shell_completion_dirs() -> Vec<(String, PathBuf)> {
82 let mut dirs = Vec::new();
83
84 #[cfg(unix)]
85 {
86 if let Some(home) = dirs::home_dir() {
88 dirs.push(("bash (user)".to_string(), home.join(".bash_completion.d")));
90 dirs.push(("zsh (user)".to_string(), home.join(".zsh/completions")));
91 dirs.push((
92 "fish (user)".to_string(),
93 home.join(".config/fish/completions"),
94 ));
95 }
96
97 dirs.push((
99 "bash (system)".to_string(),
100 PathBuf::from("/usr/local/etc/bash_completion.d"),
101 ));
102 dirs.push((
103 "bash (system alt)".to_string(),
104 PathBuf::from("/etc/bash_completion.d"),
105 ));
106 dirs.push((
107 "zsh (system)".to_string(),
108 PathBuf::from("/usr/local/share/zsh/site-functions"),
109 ));
110 dirs.push((
111 "zsh (system alt)".to_string(),
112 PathBuf::from("/usr/share/zsh/site-functions"),
113 ));
114 dirs.push((
115 "fish (system)".to_string(),
116 PathBuf::from("/usr/local/share/fish/completions"),
117 ));
118 dirs.push((
119 "fish (system alt)".to_string(),
120 PathBuf::from("/usr/share/fish/completions"),
121 ));
122 }
123
124 #[cfg(windows)]
125 {
126 if let Some(home) = dirs::home_dir() {
128 let ps_profile_dir = home
130 .join("Documents")
131 .join("WindowsPowerShell")
132 .join("Modules");
133 dirs.push(("PowerShell (user)".to_string(), ps_profile_dir));
134
135 let git_bash_completion = home
137 .join("AppData")
138 .join("Local")
139 .join("Programs")
140 .join("Git")
141 .join("etc")
142 .join("bash_completion.d");
143 dirs.push(("Git Bash (user)".to_string(), git_bash_completion));
144 }
145
146 if let Ok(program_files) = std::env::var("PROGRAMFILES") {
148 let git_bash_system = PathBuf::from(program_files)
149 .join("Git")
150 .join("etc")
151 .join("bash_completion.d");
152 dirs.push(("Git Bash (system)".to_string(), git_bash_system));
153 }
154 }
155
156 dirs
157}
158
159pub fn git_hook_extension() -> &'static str {
161 if cfg!(windows) {
162 ".bat"
163 } else {
164 ""
165 }
166}
167
168pub fn create_git_hook_content(hook_name: &str, command: &str) -> String {
170 #[cfg(windows)]
171 {
172 format!(
173 "@echo off\n\
174 rem Cascade CLI Git Hook: {}\n\
175 rem Generated automatically - do not edit manually\n\n\
176 \"{}\" %*\n\
177 if %ERRORLEVEL% neq 0 exit /b %ERRORLEVEL%\n",
178 hook_name, command
179 )
180 }
181
182 #[cfg(not(windows))]
183 {
184 format!(
185 "#!/bin/sh\n\
186 # Cascade CLI Git Hook: {hook_name}\n\
187 # Generated automatically - do not edit manually\n\n\
188 exec \"{command}\" \"$@\"\n"
189 )
190 }
191}
192
193pub fn default_shell() -> Option<String> {
195 #[cfg(windows)]
196 {
197 if which_shell("powershell").is_some() {
199 Some("powershell".to_string())
200 } else if which_shell("cmd").is_some() {
201 Some("cmd".to_string())
202 } else {
203 None
204 }
205 }
206
207 #[cfg(not(windows))]
208 {
209 for shell in &["zsh", "bash", "fish", "sh"] {
211 if which_shell(shell).is_some() {
212 return Some(shell.to_string());
213 }
214 }
215 None
216 }
217}
218
219fn which_shell(shell_name: &str) -> Option<PathBuf> {
221 let path_var = std::env::var("PATH").ok()?;
222 let executable_name = executable_name(shell_name);
223
224 for path_dir in path_var.split(path_separator()) {
225 let shell_path = PathBuf::from(path_dir).join(&executable_name);
226 if is_executable(&shell_path) {
227 return Some(shell_path);
228 }
229 }
230 None
231}
232
233pub fn secure_temp_dir() -> std::io::Result<PathBuf> {
235 let temp_dir = std::env::temp_dir();
236
237 #[cfg(unix)]
238 {
239 use std::os::unix::fs::PermissionsExt;
241 let temp_subdir = temp_dir.join(format!("cascade-{}", std::process::id()));
242 std::fs::create_dir_all(&temp_subdir)?;
243
244 let mut perms = std::fs::metadata(&temp_subdir)?.permissions();
245 perms.set_mode(0o700);
246 std::fs::set_permissions(&temp_subdir, perms)?;
247 Ok(temp_subdir)
248 }
249
250 #[cfg(windows)]
251 {
252 let temp_subdir = temp_dir.join(format!("cascade-{}", std::process::id()));
254 std::fs::create_dir_all(&temp_subdir)?;
255 Ok(temp_subdir)
256 }
257}
258
259pub fn normalize_line_endings(content: &str) -> String {
261 content.replace("\r\n", "\n").replace('\r', "\n")
264}
265
266pub fn default_editor() -> Option<String> {
268 for var in &["EDITOR", "VISUAL"] {
270 if let Ok(editor) = std::env::var(var) {
271 if !editor.trim().is_empty() {
272 return Some(editor);
273 }
274 }
275 }
276
277 #[cfg(windows)]
279 {
280 Some("notepad".to_string())
282 }
283
284 #[cfg(not(windows))]
285 {
286 for editor in &["nano", "vim", "vi"] {
288 if which_shell(editor).is_some() {
289 return Some(editor.to_string());
290 }
291 }
292 Some("vi".to_string()) }
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299
300 #[test]
301 fn test_path_separator() {
302 let separator = path_separator();
303 if cfg!(windows) {
304 assert_eq!(separator, ";");
305 } else {
306 assert_eq!(separator, ":");
307 }
308 }
309
310 #[test]
311 fn test_executable_extension() {
312 let ext = executable_extension();
313 if cfg!(windows) {
314 assert_eq!(ext, ".exe");
315 } else {
316 assert_eq!(ext, "");
317 }
318 }
319
320 #[test]
321 fn test_executable_name() {
322 let name = executable_name("test");
323 if cfg!(windows) {
324 assert_eq!(name, "test.exe");
325 } else {
326 assert_eq!(name, "test");
327 }
328 }
329
330 #[test]
331 fn test_git_hook_extension() {
332 let ext = git_hook_extension();
333 if cfg!(windows) {
334 assert_eq!(ext, ".bat");
335 } else {
336 assert_eq!(ext, "");
337 }
338 }
339
340 #[test]
341 fn test_line_ending_normalization() {
342 assert_eq!(normalize_line_endings("hello\r\nworld"), "hello\nworld");
343 assert_eq!(normalize_line_endings("hello\rworld"), "hello\nworld");
344 assert_eq!(normalize_line_endings("hello\nworld"), "hello\nworld");
345 assert_eq!(normalize_line_endings("hello world"), "hello world");
346 }
347
348 #[test]
349 fn test_shell_completion_dirs() {
350 let dirs = shell_completion_dirs();
351 assert!(
352 !dirs.is_empty(),
353 "Should return at least one completion directory"
354 );
355
356 for (name, path) in &dirs {
358 assert!(
359 path.is_absolute(),
360 "Completion directory should be absolute: {name} -> {path:?}"
361 );
362 }
363 }
364
365 #[test]
366 fn test_default_shell() {
367 let shell = default_shell();
369 if cfg!(windows) {
370 if let Some(shell_name) = shell {
372 assert!(shell_name == "powershell" || shell_name == "cmd");
373 }
374 } else {
375 if let Some(shell_name) = shell {
377 assert!(["zsh", "bash", "fish", "sh"].contains(&shell_name.as_str()));
378 }
379 }
380 }
381
382 #[test]
383 fn test_git_hook_content() {
384 let content = create_git_hook_content("pre-commit", "/usr/bin/cc");
385
386 if cfg!(windows) {
387 assert!(content.contains("@echo off"));
388 assert!(content.contains(".bat"));
389 assert!(content.contains("ERRORLEVEL"));
390 } else {
391 assert!(content.starts_with("#!/bin/sh"));
392 assert!(content.contains("exec"));
393 }
394 }
395}