use std::path::PathBuf;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Platform {
#[default]
Claude,
Codex,
}
pub const CLAUDE_SUPPORTED_DIRS: &[&str] = &[
"skills", "agents", "hooks", "rules", "commands",
"settings",
];
pub const CODEX_SUPPORTED_DIRS: &[&str] = &["skills"];
pub const CLAUDE_SPECIFIC_FILES: &[&str] = &[
"CLAUDE.md",
"settings.json",
"settings.local.json",
"mcp.json",
".mcp.json",
"hooks.json",
];
impl Platform {
pub fn base_dir(&self) -> PathBuf {
let home = dirs::home_dir().expect("Could not determine home directory");
match self {
Self::Claude => home.join(".claude"),
Self::Codex => home.join(".codex").join("skills"),
}
}
pub fn name(&self) -> &'static str {
match self {
Self::Claude => "Claude Code",
Self::Codex => "Codex CLI",
}
}
pub fn id(&self) -> &'static str {
match self {
Self::Claude => "claude",
Self::Codex => "codex",
}
}
pub fn all() -> &'static [Platform] {
&[Platform::Claude, Platform::Codex]
}
pub fn supported_dirs(&self) -> &'static [&'static str] {
match self {
Self::Claude => CLAUDE_SUPPORTED_DIRS,
Self::Codex => CODEX_SUPPORTED_DIRS,
}
}
pub fn supports_path(&self, path: &std::path::Path) -> bool {
match self {
Self::Claude => true, Self::Codex => {
if let Some(first_component) = path.components().next() {
let dir_name = first_component.as_os_str().to_string_lossy();
dir_name == "skills" || path.extension().is_some_and(|ext| ext == "md")
} else {
false
}
}
}
}
pub fn is_platform_specific_file(&self, filename: &str) -> bool {
match self {
Self::Claude => false, Self::Codex => CLAUDE_SPECIFIC_FILES.contains(&filename),
}
}
}
impl std::fmt::Display for Platform {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name())
}
}
impl std::str::FromStr for Platform {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"claude" | "claude-code" => Ok(Self::Claude),
"codex" | "codex-cli" => Ok(Self::Codex),
_ => Err(format!("Unknown platform: {}", s)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InstallTarget {
Single(Platform),
All,
Custom(PathBuf),
}
impl Default for InstallTarget {
fn default() -> Self {
Self::Single(Platform::default())
}
}
impl InstallTarget {
pub fn claude() -> Self {
Self::Single(Platform::Claude)
}
pub fn codex() -> Self {
Self::Single(Platform::Codex)
}
pub fn all() -> Self {
Self::All
}
pub fn custom(path: PathBuf) -> Self {
Self::Custom(path)
}
pub fn platforms(&self) -> Vec<Platform> {
match self {
Self::Single(p) => vec![*p],
Self::All => Platform::all().to_vec(),
Self::Custom(_) => vec![], }
}
pub fn install_dirs(&self) -> Vec<PathBuf> {
match self {
Self::Single(p) => vec![p.base_dir()],
Self::All => Platform::all().iter().map(|p| p.base_dir()).collect(),
Self::Custom(path) => vec![path.clone()],
}
}
pub fn includes(&self, platform: Platform) -> bool {
match self {
Self::Single(p) => *p == platform,
Self::All => true,
Self::Custom(_) => false,
}
}
pub fn is_multi(&self) -> bool {
matches!(self, Self::All)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn platform_base_dir() {
let home = dirs::home_dir().unwrap();
assert_eq!(Platform::Claude.base_dir(), home.join(".claude"));
assert_eq!(Platform::Codex.base_dir(), home.join(".codex/skills"));
}
#[test]
fn platform_from_str() {
assert_eq!("claude".parse::<Platform>().unwrap(), Platform::Claude);
assert_eq!("codex".parse::<Platform>().unwrap(), Platform::Codex);
assert!("unknown".parse::<Platform>().is_err());
}
#[test]
fn install_target_platforms() {
assert_eq!(InstallTarget::claude().platforms(), vec![Platform::Claude]);
assert_eq!(InstallTarget::codex().platforms(), vec![Platform::Codex]);
assert_eq!(
InstallTarget::all().platforms(),
vec![Platform::Claude, Platform::Codex]
);
}
#[test]
fn install_target_includes() {
assert!(InstallTarget::claude().includes(Platform::Claude));
assert!(!InstallTarget::claude().includes(Platform::Codex));
assert!(InstallTarget::all().includes(Platform::Claude));
assert!(InstallTarget::all().includes(Platform::Codex));
}
}