use std::path::{Path, PathBuf};
pub(super) const TRUSTY_TOOLS_DIR: &str = ".trusty-tools";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ProjectMarker {
Claude,
ClaudeMd,
Git,
TrustyTools,
None,
}
pub(super) fn detect_project_marker(dir: &Path) -> ProjectMarker {
if dir.join(".claude").is_dir() {
return ProjectMarker::Claude;
}
if dir.join("CLAUDE.md").is_file() {
return ProjectMarker::ClaudeMd;
}
if dir.join(".git").exists() {
return ProjectMarker::Git;
}
if dir.join(TRUSTY_TOOLS_DIR).is_dir() {
return ProjectMarker::TrustyTools;
}
ProjectMarker::None
}
pub(super) fn default_scan_paths() -> Vec<PathBuf> {
let Some(home) = dirs::home_dir() else {
return Vec::new();
};
["Projects", "code", "src"]
.iter()
.map(|p| home.join(p))
.filter(|p| p.is_dir())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
fn tempdir_unique(label: &str) -> PathBuf {
let pid = std::process::id();
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let p = std::env::temp_dir().join(format!("trusty-discover-{label}-{pid}-{nanos}"));
let _ = fs::remove_dir_all(&p);
fs::create_dir_all(&p).unwrap();
p
}
#[test]
fn detect_project_marker_claude_dir_wins() {
let dir = tempdir_unique("claude");
fs::create_dir_all(dir.join(".claude")).unwrap();
fs::write(dir.join("CLAUDE.md"), "x").unwrap();
fs::create_dir_all(dir.join(".git")).unwrap();
assert_eq!(detect_project_marker(&dir), ProjectMarker::Claude);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn detect_project_marker_claude_md_beats_git() {
let dir = tempdir_unique("claudemd");
fs::write(dir.join("CLAUDE.md"), "x").unwrap();
fs::create_dir_all(dir.join(".git")).unwrap();
assert_eq!(detect_project_marker(&dir), ProjectMarker::ClaudeMd);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn detect_project_marker_git_when_only_git() {
let dir = tempdir_unique("git");
fs::create_dir_all(dir.join(".git")).unwrap();
assert_eq!(detect_project_marker(&dir), ProjectMarker::Git);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn detect_project_marker_none_when_empty() {
let dir = tempdir_unique("empty");
assert_eq!(detect_project_marker(&dir), ProjectMarker::None);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn detect_project_marker_ignores_claude_md_as_dir() {
let dir = tempdir_unique("claudedir");
fs::create_dir_all(dir.join("CLAUDE.md")).unwrap();
assert_eq!(detect_project_marker(&dir), ProjectMarker::None);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn detect_project_marker_trusty_tools_dir() {
let dir = tempdir_unique("trustytools");
fs::create_dir_all(dir.join(TRUSTY_TOOLS_DIR)).unwrap();
assert_eq!(
detect_project_marker(&dir),
ProjectMarker::TrustyTools,
".trusty-tools/ dir must yield TrustyTools marker"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn detect_project_marker_claude_wins_over_trusty_tools() {
let dir = tempdir_unique("claude-plus-trusty");
fs::create_dir_all(dir.join(".claude")).unwrap();
fs::create_dir_all(dir.join(TRUSTY_TOOLS_DIR)).unwrap();
assert_eq!(
detect_project_marker(&dir),
ProjectMarker::Claude,
".claude/ must take priority over .trusty-tools/"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn detect_project_marker_trusty_tools_file_not_dir_is_none() {
let dir = tempdir_unique("trustytools-file");
fs::write(dir.join(TRUSTY_TOOLS_DIR), "not a dir").unwrap();
assert_eq!(
detect_project_marker(&dir),
ProjectMarker::None,
".trusty-tools as a file must not trigger TrustyTools marker"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn detect_project_marker_none_when_no_markers_after_trusty_tools_added() {
let dir = tempdir_unique("no-markers");
assert_eq!(
detect_project_marker(&dir),
ProjectMarker::None,
"directory with no markers must still return None"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn default_scan_paths_does_not_panic() {
let _ = default_scan_paths();
}
}