use std::path::{Path, PathBuf}; const EXCLUDED_DIR_NAMES: &[&str] = &[ ".kigi", ".cache", ".daemon", ".config", ".npm", ".cargo", ".rustup", ".vscode", ".gemini", ".hermes", ".claude", ]; fn known_os_dirs() -> Vec { [ dirs::desktop_dir(), dirs::download_dir(), dirs::document_dir(), dirs::audio_dir(), dirs::video_dir(), dirs::picture_dir(), dirs::public_dir(), ] .into_iter() .flatten() .collect() } pub fn is_project_dir(cwd: &Path) -> bool { if cwd.as_os_str().is_empty() || cwd.parent().is_none() { return false; } if cwd.ancestors().any(|p| p.join(".git").exists()) { return true; } if has_excluded_component(cwd) { return false; } if is_platform_system_dir(cwd) { return false; } let Some(home) = dirs::home_dir() else { return false; }; if cwd == home { return false; } if is_platform_home_excluded(cwd, &home) { return false; } if known_os_dirs().iter().any(|d| cwd == d) { return false; } true } #[cfg(not(target_os = "windows"))] fn is_platform_system_dir(cwd: &Path) -> bool { if cwd == Path::new("/tmp") || cwd.starts_with("/tmp/") || cwd == Path::new("/var/tmp") || cwd.starts_with("/var/tmp/") || cwd.starts_with("/var/folders/") { return true; } #[cfg(target_os = "macos")] if cwd == Path::new("/private/tmp") || cwd.starts_with("/private/tmp/") || cwd == Path::new("/private/var/tmp") || cwd.starts_with("/private/var/tmp/") || cwd.starts_with("/private/var/folders/") { return true; } #[cfg(target_os = "linux")] if cwd == Path::new("/root") { return true; } false } #[cfg(target_os = "windows")] fn is_platform_system_dir(cwd: &Path) -> bool { if let Ok(temp) = std::env::var("TEMP").or_else(|_| std::env::var("TMP")) { if cwd.starts_with(&temp) { return true; } } let path_lower = cwd.to_string_lossy().to_lowercase(); if path_lower.contains("\\windows\\") || path_lower.ends_with("\\windows") || path_lower.contains("\\program files") { return true; } if cwd.parent().map_or(false, |p| p.parent().is_none()) && cwd.to_string_lossy().len() <= 3 { return true; } false } #[cfg(target_os = "macos")] fn is_platform_home_excluded(cwd: &Path, home: &Path) -> bool { if cwd.starts_with(home.join("Library")) && !cwd.starts_with(home.join("Library/Mobile Documents")) { return true; } false } #[cfg(target_os = "linux")] fn is_platform_home_excluded(cwd: &Path, home: &Path) -> bool { let Ok(relative) = cwd.strip_prefix(home) else { return false; }; if relative.components().count() != 1 { return false; } let Some(std::path::Component::Normal(name)) = relative.components().next() else { return false; }; let name = name.to_string_lossy().to_lowercase(); [ "desktop", "downloads", "documents", "pictures", "music", "videos", ] .contains(&name.as_str()) } #[cfg(target_os = "windows")] fn is_platform_home_excluded(_cwd: &Path, _home: &Path) -> bool { false } fn has_excluded_component(path: &Path) -> bool { for component in path.components() { if let std::path::Component::Normal(name) = component { let name_lower = name.to_string_lossy().to_lowercase(); if EXCLUDED_DIR_NAMES.contains(&name_lower.as_str()) { return true; } if name_lower.starts_with(".kigi-") { return true; } } } false } #[cfg(test)] mod tests { use super::*; #[cfg(not(target_os = "windows"))] mod posix { use super::*; #[test] fn root_is_unsafe() { assert!(!is_project_dir(Path::new("/"))); } #[test] fn tmp_is_unsafe() { assert!(!is_project_dir(Path::new("/tmp"))); assert!(!is_project_dir(Path::new("/tmp/scratch"))); } #[test] fn tmp_prefix_not_greedy() { assert!(is_project_dir(Path::new("/tmpdata/foo"))); } #[test] fn var_folders_is_unsafe() { assert!(!is_project_dir(Path::new("/var/folders/ab/cd"))); } #[test] fn deep_project_is_safe() { assert!(is_project_dir(Path::new("/Users/someone/my-project/src"))); } #[test] fn home_subdir_is_safe() { assert!(is_project_dir(Path::new("/Users/someone/my-project"))); } } #[cfg(target_os = "macos")] mod macos { use super::*; #[test] fn private_tmp_is_unsafe() { assert!(!is_project_dir(Path::new("/private/tmp"))); assert!(!is_project_dir(Path::new("/private/tmp/scratch"))); assert!(!is_project_dir(Path::new("/private/var/folders/ab/cd"))); } #[test] fn library_is_unsafe() { if let Some(home) = dirs::home_dir() { assert!(!is_project_dir(&home.join("Library"))); assert!(!is_project_dir(&home.join("Library/Caches"))); assert!(!is_project_dir(&home.join("Library/Application Support"))); } } #[test] fn icloud_drive_projects_are_safe() { if let Some(home) = dirs::home_dir() { assert!(is_project_dir(&home.join( "Library/Mobile Documents/com~apple~CloudDocs/Projects/my-app" ))); } } } #[cfg(target_os = "linux")] mod linux { use super::*; #[test] fn bare_root_is_unsafe() { assert!(!is_project_dir(Path::new("/root"))); } #[test] fn root_project_is_safe() { assert!(is_project_dir(Path::new("/root/my-project"))); } } mod config_and_cache { use super::*; #[test] fn kigi_dirs_are_unsafe() { if let Some(home) = dirs::home_dir() { assert!(!is_project_dir(&home.join(".kigi"))); assert!(!is_project_dir(&home.join(".kigi/bin"))); } } #[test] fn kigi_prefixed_dirs_are_unsafe() { if let Some(home) = dirs::home_dir() { assert!(!is_project_dir(&home.join(".kigi-proxy-work"))); } } #[test] fn cache_dirs_are_unsafe() { if let Some(home) = dirs::home_dir() { assert!(!is_project_dir(&home.join(".cache/zoe-proc"))); assert!(!is_project_dir(&home.join(".config/nvim"))); } } #[test] fn other_ai_tool_dirs_are_unsafe() { if let Some(home) = dirs::home_dir() { assert!(!is_project_dir(&home.join(".gemini/antigravity"))); assert!(!is_project_dir(&home.join(".hermes/kanban"))); assert!(!is_project_dir(&home.join(".claude/projects"))); } } } mod home_and_os_dirs { use super::*; #[test] fn home_is_unsafe() { if let Some(home) = dirs::home_dir() { assert!(!is_project_dir(&home)); } } #[test] fn home_project_is_safe() { if let Some(home) = dirs::home_dir() { assert!(is_project_dir(&home.join("my-project"))); } } #[test] fn bare_desktop_is_unsafe() { if let Some(d) = dirs::desktop_dir() { assert!(!is_project_dir(&d)); } } #[test] fn desktop_project_is_safe() { if let Some(d) = dirs::desktop_dir() { assert!(is_project_dir(&d.join("my-project"))); } } #[test] fn bare_downloads_is_unsafe() { if let Some(d) = dirs::download_dir() { assert!(!is_project_dir(&d)); } } #[test] fn bare_documents_is_unsafe() { if let Some(d) = dirs::document_dir() { assert!(!is_project_dir(&d)); } } } mod edge_cases { use super::*; #[test] fn empty_path_is_unsafe() { assert!(!is_project_dir(Path::new(""))); } #[cfg(not(target_os = "windows"))] #[test] fn unicode_paths_work() { assert!(is_project_dir(Path::new( "/Users/me/code/\u{D3F4}\u{B9AC}\u{B9C8}\u{CF13}" ))); } #[cfg(not(target_os = "windows"))] #[test] fn spaces_work() { assert!(is_project_dir(Path::new("/Users/me/My Projects/cool app"))); } } mod git_detection { use super::*; #[test] fn inside_git_repo_is_safe() { let tmp = tempfile::tempdir().unwrap(); std::fs::create_dir(tmp.path().join(".git")).unwrap(); assert!(is_project_dir(tmp.path())); } #[test] fn subdirectory_of_git_repo_is_safe() { let tmp = tempfile::tempdir().unwrap(); std::fs::create_dir(tmp.path().join(".git")).unwrap(); let sub = tmp.path().join("deep/sub/dir"); std::fs::create_dir_all(&sub).unwrap(); assert!(is_project_dir(&sub)); } } }