use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Scope {
Project,
Home,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AlwaysOnFile {
ClaudeMd,
AgentsMd,
GeminiMd,
CopilotInstructions,
KiroSteering,
AntigravityRules,
}
impl AlwaysOnFile {
pub fn filename(self) -> &'static str {
match self {
AlwaysOnFile::ClaudeMd => "CLAUDE.md",
AlwaysOnFile::AgentsMd => "AGENTS.md",
AlwaysOnFile::GeminiMd => "GEMINI.md",
AlwaysOnFile::CopilotInstructions => ".github/copilot-instructions.md",
AlwaysOnFile::KiroSteering => ".kiro/steering/AGENTS.md",
AlwaysOnFile::AntigravityRules => ".antigravity/rules.md",
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct SkillDir {
pub home: &'static str,
pub project: Option<&'static str>,
}
impl SkillDir {
fn expand(template: &str, slug: &str, home: &Path) -> PathBuf {
let filled = template.replace("{slug}", slug);
if let Some(rest) = filled.strip_prefix("~/") {
home.join(rest)
} else {
PathBuf::from(filled)
}
}
pub fn home_dir(&self, slug: &str) -> Option<PathBuf> {
let home = std::env::var_os("HOME")?;
Some(self.home_dir_under(slug, Path::new(&home)))
}
pub fn home_dir_under(&self, slug: &str, home: &Path) -> PathBuf {
Self::expand(self.home, slug, home)
}
pub fn project_dir(&self, slug: &str, root: &Path) -> Option<PathBuf> {
self.project.map(|t| root.join(t.replace("{slug}", slug)))
}
}
#[derive(Debug, Clone, Copy)]
pub struct Platform {
pub key: &'static str,
pub display: &'static str,
pub always_on: AlwaysOnFile,
pub scope: Scope,
pub skill: Option<SkillDir>,
}
impl Platform {
pub fn supports_skill(&self) -> bool {
self.skill.is_some()
}
}
use AlwaysOnFile::*;
use Scope::*;
const fn skill(home: &'static str, project: Option<&'static str>) -> Option<SkillDir> {
Some(SkillDir { home, project })
}
pub const PLATFORMS: &[Platform] = &[
Platform {
key: "claude-code",
display: "Claude Code",
always_on: ClaudeMd,
scope: Project,
skill: skill("~/.claude/skills/{slug}", Some(".claude/skills/{slug}")),
},
Platform {
key: "codex",
display: "Codex CLI",
always_on: AgentsMd,
scope: Project,
skill: skill("~/.codex/skills/{slug}", None),
},
Platform {
key: "cursor",
display: "Cursor",
always_on: AgentsMd,
scope: Project,
skill: None,
},
Platform {
key: "gemini-cli",
display: "Gemini CLI",
always_on: GeminiMd,
scope: Project,
skill: skill("~/.gemini/skills/{slug}", Some(".gemini/skills/{slug}")),
},
Platform {
key: "copilot",
display: "GitHub Copilot (VS Code)",
always_on: CopilotInstructions,
scope: Project,
skill: None,
},
Platform {
key: "opencode",
display: "OpenCode",
always_on: AgentsMd,
scope: Project,
skill: skill(
"~/.config/opencode/skills/{slug}",
Some(".opencode/skills/{slug}"),
),
},
Platform {
key: "kiro",
display: "Kiro",
always_on: KiroSteering,
scope: Project,
skill: skill("~/.kiro/skills/{slug}", None),
},
Platform {
key: "antigravity",
display: "Antigravity",
always_on: AntigravityRules,
scope: Project,
skill: None,
},
Platform {
key: "windsurf",
display: "Windsurf",
always_on: AgentsMd,
scope: Project,
skill: None,
},
Platform {
key: "zed",
display: "Zed",
always_on: AgentsMd,
scope: Project,
skill: None,
},
Platform {
key: "cline",
display: "Cline",
always_on: AgentsMd,
scope: Project,
skill: None,
},
Platform {
key: "aider",
display: "Aider",
always_on: AgentsMd,
scope: Project,
skill: None,
},
Platform {
key: "devin",
display: "Devin",
always_on: AgentsMd,
scope: Home,
skill: skill("~/.devin/skills/{slug}", None),
},
Platform {
key: "amp",
display: "Amp",
always_on: AgentsMd,
scope: Project,
skill: skill("~/.config/amp/skills/{slug}", None),
},
Platform {
key: "agents",
display: "Generic (AGENTS.md)",
always_on: AgentsMd,
scope: Home,
skill: skill("~/.agents/skills/{slug}", None),
},
];
pub fn platform(key: &str) -> Option<&'static Platform> {
PLATFORMS.iter().find(|p| p.key == key)
}