dot_agent_core/
platform.rs1use std::path::PathBuf;
8
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
13#[serde(rename_all = "kebab-case")]
14pub enum Platform {
15 #[default]
17 Claude,
18 Codex,
20}
21
22pub const CLAUDE_SUPPORTED_DIRS: &[&str] = &[
24 "skills", "agents", "hooks", "rules", "commands",
25 "settings",
26 ];
28
29pub const CODEX_SUPPORTED_DIRS: &[&str] = &["skills"];
31
32pub const CLAUDE_SPECIFIC_FILES: &[&str] = &[
34 "CLAUDE.md",
35 "settings.json",
36 "settings.local.json",
37 "mcp.json",
38 ".mcp.json",
39 "hooks.json",
40];
41
42impl Platform {
43 pub fn base_dir(&self) -> PathBuf {
45 let home = dirs::home_dir().expect("Could not determine home directory");
46 match self {
47 Self::Claude => home.join(".claude"),
48 Self::Codex => home.join(".codex").join("skills"),
49 }
50 }
51
52 pub fn name(&self) -> &'static str {
54 match self {
55 Self::Claude => "Claude Code",
56 Self::Codex => "Codex CLI",
57 }
58 }
59
60 pub fn id(&self) -> &'static str {
62 match self {
63 Self::Claude => "claude",
64 Self::Codex => "codex",
65 }
66 }
67
68 pub fn all() -> &'static [Platform] {
70 &[Platform::Claude, Platform::Codex]
71 }
72
73 pub fn supported_dirs(&self) -> &'static [&'static str] {
75 match self {
76 Self::Claude => CLAUDE_SUPPORTED_DIRS,
77 Self::Codex => CODEX_SUPPORTED_DIRS,
78 }
79 }
80
81 pub fn supports_path(&self, path: &std::path::Path) -> bool {
87 match self {
88 Self::Claude => true, Self::Codex => {
90 if let Some(first_component) = path.components().next() {
92 let dir_name = first_component.as_os_str().to_string_lossy();
93 dir_name == "skills" || path.extension().is_some_and(|ext| ext == "md")
95 } else {
96 false
97 }
98 }
99 }
100 }
101
102 pub fn is_platform_specific_file(&self, filename: &str) -> bool {
104 match self {
105 Self::Claude => false, Self::Codex => CLAUDE_SPECIFIC_FILES.contains(&filename),
107 }
108 }
109}
110
111impl std::fmt::Display for Platform {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 write!(f, "{}", self.name())
114 }
115}
116
117impl std::str::FromStr for Platform {
118 type Err = String;
119
120 fn from_str(s: &str) -> Result<Self, Self::Err> {
121 match s.to_lowercase().as_str() {
122 "claude" | "claude-code" => Ok(Self::Claude),
123 "codex" | "codex-cli" => Ok(Self::Codex),
124 _ => Err(format!("Unknown platform: {}", s)),
125 }
126 }
127}
128
129#[derive(Debug, Clone, PartialEq, Eq)]
131pub enum InstallTarget {
132 Single(Platform),
134 All,
136 Custom(PathBuf),
138}
139
140impl Default for InstallTarget {
141 fn default() -> Self {
142 Self::Single(Platform::default())
143 }
144}
145
146impl InstallTarget {
147 pub fn claude() -> Self {
149 Self::Single(Platform::Claude)
150 }
151
152 pub fn codex() -> Self {
154 Self::Single(Platform::Codex)
155 }
156
157 pub fn all() -> Self {
159 Self::All
160 }
161
162 pub fn custom(path: PathBuf) -> Self {
164 Self::Custom(path)
165 }
166
167 pub fn platforms(&self) -> Vec<Platform> {
169 match self {
170 Self::Single(p) => vec![*p],
171 Self::All => Platform::all().to_vec(),
172 Self::Custom(_) => vec![], }
174 }
175
176 pub fn install_dirs(&self) -> Vec<PathBuf> {
178 match self {
179 Self::Single(p) => vec![p.base_dir()],
180 Self::All => Platform::all().iter().map(|p| p.base_dir()).collect(),
181 Self::Custom(path) => vec![path.clone()],
182 }
183 }
184
185 pub fn includes(&self, platform: Platform) -> bool {
187 match self {
188 Self::Single(p) => *p == platform,
189 Self::All => true,
190 Self::Custom(_) => false,
191 }
192 }
193
194 pub fn is_multi(&self) -> bool {
196 matches!(self, Self::All)
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 #[test]
205 fn platform_base_dir() {
206 let home = dirs::home_dir().unwrap();
207 assert_eq!(Platform::Claude.base_dir(), home.join(".claude"));
208 assert_eq!(Platform::Codex.base_dir(), home.join(".codex/skills"));
209 }
210
211 #[test]
212 fn platform_from_str() {
213 assert_eq!("claude".parse::<Platform>().unwrap(), Platform::Claude);
214 assert_eq!("codex".parse::<Platform>().unwrap(), Platform::Codex);
215 assert!("unknown".parse::<Platform>().is_err());
216 }
217
218 #[test]
219 fn install_target_platforms() {
220 assert_eq!(InstallTarget::claude().platforms(), vec![Platform::Claude]);
221 assert_eq!(InstallTarget::codex().platforms(), vec![Platform::Codex]);
222 assert_eq!(
223 InstallTarget::all().platforms(),
224 vec![Platform::Claude, Platform::Codex]
225 );
226 }
227
228 #[test]
229 fn install_target_includes() {
230 assert!(InstallTarget::claude().includes(Platform::Claude));
231 assert!(!InstallTarget::claude().includes(Platform::Codex));
232 assert!(InstallTarget::all().includes(Platform::Claude));
233 assert!(InstallTarget::all().includes(Platform::Codex));
234 }
235}