use std::path::Path;
use crate::cli_args::AdoptTarget;
pub(crate) const NON_HOOK_V1_TARGETS: [AdoptTarget; 6] = [
AdoptTarget::ClaudeMd,
AdoptTarget::AgentsMd,
AdoptTarget::Copilot,
AdoptTarget::Cursor,
AdoptTarget::McpProject,
AdoptTarget::McpCursor,
];
pub(crate) const DEFAULT_TARGETS: [AdoptTarget; 2] = [AdoptTarget::AgentsMd, AdoptTarget::ClaudeMd];
pub(crate) fn select_targets(
root: &Path,
requested_targets: &[AdoptTarget],
all: bool,
) -> Vec<AdoptTarget> {
let mut targets = if all {
NON_HOOK_V1_TARGETS.to_vec()
} else if requested_targets.is_empty() {
detect_known_project_client_markers(root)
} else {
requested_targets.to_vec()
};
if all {
for target in requested_targets {
push_unique(&mut targets, *target);
}
}
targets
}
pub(crate) fn detect_known_project_client_markers(root: &Path) -> Vec<AdoptTarget> {
let mut targets = Vec::new();
for target in NON_HOOK_V1_TARGETS {
if target_marker_exists(root, target) {
push_unique(&mut targets, target);
}
}
let claude_inferred_from_directory_only =
targets == [AdoptTarget::ClaudeMd] && !root.join(AdoptTarget::ClaudeMd.path()).exists();
if claude_inferred_from_directory_only {
push_unique(&mut targets, AdoptTarget::AgentsMd);
}
if targets.is_empty() {
targets.extend(DEFAULT_TARGETS);
}
targets
}
fn target_marker_exists(root: &Path, target: AdoptTarget) -> bool {
match target {
AdoptTarget::Cursor => {
root.join(".cursor/rules").is_dir() || root.join(target.path()).exists()
}
AdoptTarget::ClaudeMd => root.join(".claude").is_dir() || root.join(target.path()).exists(),
_ => root.join(target.path()).exists(),
}
}
fn push_unique(targets: &mut Vec<AdoptTarget>, target: AdoptTarget) {
if !targets.contains(&target) {
targets.push(target);
}
}
#[cfg(test)]
mod tests {
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use super::{detect_known_project_client_markers, select_targets};
use crate::cli_args::AdoptTarget;
#[test]
fn adopt_defaults_to_agents_and_claude_without_markers() {
let root = temp_dir("adopt-default-targets");
fs::create_dir_all(&root).expect("create temp root");
let targets = detect_known_project_client_markers(&root);
assert_eq!(targets, vec![AdoptTarget::AgentsMd, AdoptTarget::ClaudeMd]);
fs::remove_dir_all(root).expect("remove temp root");
}
#[test]
fn adopt_detects_existing_project_markers() {
let root = temp_dir("adopt-detect-targets");
fs::create_dir_all(root.join(".cursor/rules")).expect("create cursor rules dir");
fs::create_dir_all(root.join(".github")).expect("create .github dir");
fs::write(root.join(".github/copilot-instructions.md"), "").expect("write copilot marker");
fs::write(root.join(".cursor/rules/frigg.mdc"), "").expect("write cursor marker");
let targets = detect_known_project_client_markers(&root);
assert_eq!(targets, vec![AdoptTarget::Copilot, AdoptTarget::Cursor]);
fs::remove_dir_all(root).expect("remove temp root");
}
#[test]
fn adopt_detects_cursor_project_marker_without_frigg_rule() {
let root = temp_dir("adopt-detect-cursor-project-marker");
fs::create_dir_all(root.join(".cursor/rules")).expect("create cursor rules dir");
let targets = detect_known_project_client_markers(&root);
assert_eq!(targets, vec![AdoptTarget::Cursor]);
fs::remove_dir_all(root).expect("remove temp root");
}
#[test]
fn adopt_detects_claude_directory_without_claude_md() {
let root = temp_dir("adopt-detect-claude-dir");
fs::create_dir_all(root.join(".claude")).expect("create .claude dir");
let targets = detect_known_project_client_markers(&root);
assert!(
targets.contains(&AdoptTarget::ClaudeMd),
"detection must not depend on the doc file existing"
);
assert!(
targets.contains(&AdoptTarget::AgentsMd),
"the directory alone must not drop the cross-agent doc the default would have written"
);
fs::remove_dir_all(root).expect("remove temp root");
}
#[test]
fn frigg_created_claude_dir_does_not_narrow_the_default_target_set() {
let root = temp_dir("adopt-claude-dir-feedback");
fs::create_dir_all(&root).expect("create temp root");
let before = detect_known_project_client_markers(&root);
fs::create_dir_all(root.join(".claude")).expect("create .claude dir");
fs::write(root.join(".claude/settings.json"), "{}").expect("write settings");
let after = detect_known_project_client_markers(&root);
for target in &before {
assert!(
after.contains(target),
"{target:?} was managed before the hook install and must still be after"
);
}
fs::remove_dir_all(root).expect("remove temp root");
}
#[test]
fn claude_md_file_alone_stays_a_claude_only_target_set() {
let root = temp_dir("adopt-claude-md-only");
fs::create_dir_all(&root).expect("create temp root");
fs::write(root.join("CLAUDE.md"), "").expect("write claude md");
let targets = detect_known_project_client_markers(&root);
assert_eq!(targets, vec![AdoptTarget::ClaudeMd]);
fs::remove_dir_all(root).expect("remove temp root");
}
#[test]
fn adopt_requested_targets_win_over_detection() {
let root = temp_dir("adopt-requested-targets");
fs::create_dir_all(&root).expect("create temp root");
let targets = select_targets(&root, &[AdoptTarget::McpProject], false);
assert_eq!(targets, vec![AdoptTarget::McpProject]);
fs::remove_dir_all(root).expect("remove temp root");
}
#[test]
fn adopt_all_does_not_select_hook_target() {
let root = temp_dir("adopt-all-targets");
fs::create_dir_all(&root).expect("create temp root");
let targets = select_targets(&root, &[], true);
assert!(!targets.contains(&AdoptTarget::Hook));
fs::remove_dir_all(root).expect("remove temp root");
}
#[test]
fn adopt_all_preserves_explicit_hook_target() {
let root = temp_dir("adopt-all-hook-targets");
fs::create_dir_all(&root).expect("create temp root");
let targets = select_targets(&root, &[AdoptTarget::Hook], true);
assert!(targets.contains(&AdoptTarget::Hook));
fs::remove_dir_all(root).expect("remove temp root");
}
fn temp_dir(stem: &str) -> PathBuf {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time should be after epoch")
.as_nanos();
std::env::temp_dir().join(format!("{stem}-{unique}"))
}
}