difflore_core/infra/
skill_fs.rs1use 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 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}