Skip to main content

difflore_core/infra/
skill_fs.rs

1use std::path::PathBuf;
2
3use crate::paths;
4
5pub fn skills_base_dir() -> Result<PathBuf, String> {
6    Ok(paths::data_home()?.join("skills"))
7}
8
9pub fn ensure_skill_dirs() -> Result<(), String> {
10    let base = skills_base_dir()?;
11    for source in &["github", "local", "cloud", "team"] {
12        std::fs::create_dir_all(base.join(source))
13            .map_err(|e| format!("failed to create skill directory: {e}"))?;
14    }
15    Ok(())
16}
17
18pub fn get_engine_skills_dir(engine: &str) -> Option<PathBuf> {
19    // Test-only redirect: when DIFFLORE_HOME is set (see db.rs) integration
20    // tests don't want real ~/.claude/skills symlinks polluting the user's
21    // dotfile directories. Point engine dirs into the same sandbox.
22    let home = if let Some(custom) = crate::env::difflore_home() {
23        PathBuf::from(custom)
24    } else {
25        dirs::home_dir()?
26    };
27    match engine {
28        "codex" => Some(home.join(".codex").join("skills")),
29        "claude" => Some(home.join(".claude").join("skills")),
30        "gemini" => Some(home.join(".gemini").join("skills")),
31        "cursor" => Some(home.join(".cursor").join("skills")),
32        _ => None,
33    }
34}
35
36pub fn sync_engine_link(
37    source: &str,
38    directory: &str,
39    engine: &str,
40    enabled: bool,
41) -> std::io::Result<()> {
42    let skill_dir = skills_base_dir()
43        .map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e))?
44        .join(source)
45        .join(directory);
46    let Some(engine_dir) = get_engine_skills_dir(engine) else {
47        return Ok(());
48    };
49    let _ = std::fs::create_dir_all(&engine_dir);
50    let link_path = engine_dir.join(directory);
51
52    if enabled {
53        if !skill_dir.exists() {
54            return Ok(());
55        }
56        match link_entry_kind(&link_path)? {
57            Some(LinkEntryKind::ManagedLink) => return Ok(()),
58            Some(LinkEntryKind::Other) => {
59                return Err(std::io::Error::new(
60                    std::io::ErrorKind::AlreadyExists,
61                    format!(
62                        "cannot enable skill link because a non-symlink entry exists at {}",
63                        link_path.display()
64                    ),
65                ));
66            }
67            None => {}
68        }
69        create_skill_link(&skill_dir, &link_path).or_else(|e| {
70            if e.kind() == std::io::ErrorKind::AlreadyExists
71                && matches!(
72                    link_entry_kind(&link_path)?,
73                    Some(LinkEntryKind::ManagedLink)
74                )
75            {
76                Ok(())
77            } else {
78                Err(e)
79            }
80        })?;
81    } else {
82        match link_entry_kind(&link_path)? {
83            Some(LinkEntryKind::ManagedLink) => remove_link_entry(&link_path)?,
84            Some(LinkEntryKind::Other) => {
85                return Err(std::io::Error::new(
86                    std::io::ErrorKind::AlreadyExists,
87                    format!(
88                        "cannot disable skill link because a non-symlink entry exists at {}",
89                        link_path.display()
90                    ),
91                ));
92            }
93            None => {}
94        }
95    }
96    Ok(())
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100enum LinkEntryKind {
101    ManagedLink,
102    Other,
103}
104
105fn link_entry_kind(path: &std::path::Path) -> std::io::Result<Option<LinkEntryKind>> {
106    match std::fs::symlink_metadata(path) {
107        Ok(meta) => {
108            if is_link_like(&meta) {
109                Ok(Some(LinkEntryKind::ManagedLink))
110            } else {
111                Ok(Some(LinkEntryKind::Other))
112            }
113        }
114        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
115        Err(e) => Err(e),
116    }
117}
118
119fn is_link_like(meta: &std::fs::Metadata) -> bool {
120    if meta.file_type().is_symlink() {
121        return true;
122    }
123    #[cfg(windows)]
124    {
125        use std::os::windows::fs::MetadataExt;
126        const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x0400;
127        meta.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0
128    }
129    #[cfg(not(windows))]
130    {
131        false
132    }
133}
134
135fn create_skill_link(
136    skill_dir: &std::path::Path,
137    link_path: &std::path::Path,
138) -> std::io::Result<()> {
139    #[cfg(unix)]
140    {
141        std::os::unix::fs::symlink(skill_dir, link_path)
142    }
143    #[cfg(windows)]
144    {
145        if skill_dir.is_dir() {
146            std::os::windows::fs::symlink_dir(skill_dir, link_path)
147        } else {
148            std::os::windows::fs::symlink_file(skill_dir, link_path)
149        }
150    }
151}
152
153fn remove_link_entry(path: &std::path::Path) -> std::io::Result<()> {
154    match std::fs::remove_file(path) {
155        Ok(()) => Ok(()),
156        Err(file_err) => match std::fs::remove_dir(path) {
157            Ok(()) => Ok(()),
158            Err(_) => Err(file_err),
159        },
160    }
161}