use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
pub use merge::MergeStrategy;
pub mod detection;
pub mod loader;
pub mod merge;
#[cfg(test)]
mod tests;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Platform {
pub id: String,
pub name: String,
pub directory: String,
pub detection: Vec<String>,
pub transforms: Vec<TransformRule>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TransformRule {
pub from: String,
pub to: String,
#[serde(default)]
pub merge: MergeStrategy,
#[serde(skip_serializing_if = "Option::is_none")]
pub extension: Option<String>,
}
impl Platform {
pub fn new(
id: impl Into<String>,
name: impl Into<String>,
directory: impl Into<String>,
) -> Self {
Self {
id: id.into(),
name: name.into(),
directory: directory.into(),
detection: Vec::new(),
transforms: Vec::new(),
}
}
pub fn with_detection(mut self, pattern: impl Into<String>) -> Self {
self.detection.push(pattern.into());
self
}
pub fn with_transform(mut self, rule: TransformRule) -> Self {
self.transforms.push(rule);
self
}
#[allow(dead_code)]
pub fn is_detected(&self, workspace_root: &Path) -> bool {
self.detection.iter().any(|pattern| {
let check_path = workspace_root.join(pattern);
check_path.exists()
})
}
pub fn directory_path(&self, workspace_root: &Path) -> PathBuf {
workspace_root.join(&self.directory)
}
}
impl TransformRule {
pub fn new(from: impl Into<String>, to: impl Into<String>) -> Self {
Self {
from: from.into(),
to: to.into(),
merge: MergeStrategy::Replace,
extension: None,
}
}
pub fn with_merge(mut self, strategy: MergeStrategy) -> Self {
self.merge = strategy;
self
}
pub fn with_extension(mut self, ext: impl Into<String>) -> Self {
self.extension = Some(ext.into());
self
}
}
pub fn default_platforms() -> Vec<Platform> {
vec![
Platform::new("antigravity", "Google Antigravity", ".agent")
.with_detection(".agent")
.with_transform(TransformRule::new("rules/**/*.md", ".agent/rules/**/*.md"))
.with_transform(TransformRule::new(
"commands/**/*.md",
".agent/workflows/**/*.md",
))
.with_transform(TransformRule::new(
"skills/**/SKILL.md",
".agent/skills/{name}/SKILL.md",
))
.with_transform(TransformRule::new(
"skills/**/*",
".agent/skills/{name}/**/*",
)),
Platform::new("augment", "Augment Code", ".augment")
.with_detection(".augment")
.with_transform(TransformRule::new(
"rules/**/*.md",
".augment/rules/**/*.md",
))
.with_transform(TransformRule::new(
"commands/**/*.md",
".augment/commands/**/*.md",
)),
Platform::new("claude", "Claude Code", ".claude")
.with_detection(".claude")
.with_detection("CLAUDE.md")
.with_transform(TransformRule::new(
"commands/**/*.md",
".claude/commands/**/*.md",
))
.with_transform(TransformRule::new("rules/**/*.md", ".claude/rules/**/*.md"))
.with_transform(TransformRule::new(
"agents/**/*.md",
".claude/agents/**/*.md",
))
.with_transform(TransformRule::new(
"skills/**/SKILL.md",
".claude/skills/{name}/SKILL.md",
))
.with_transform(TransformRule::new(
"skills/**/*",
".claude/skills/{name}/**/*",
))
.with_transform(
TransformRule::new("mcp.jsonc", ".mcp.json").with_merge(MergeStrategy::Deep),
)
.with_transform(
TransformRule::new("AGENTS.md", "CLAUDE.md").with_merge(MergeStrategy::Composite),
),
Platform::new("claude-plugin", "Claude Code Plugin", ".claude-plugin")
.with_detection(".claude-plugin/plugin.json")
.with_transform(TransformRule::new("rules/**/*.md", "rules/**/*.md"))
.with_transform(TransformRule::new("commands/**/*.md", "commands/**/*.md"))
.with_transform(TransformRule::new("agents/**/*.md", "agents/**/*.md"))
.with_transform(TransformRule::new(
"skills/**/SKILL.md",
"skills/{name}/SKILL.md",
))
.with_transform(TransformRule::new("skills/**/*", "skills/{name}/**/*"))
.with_transform(
TransformRule::new("mcp.jsonc", ".mcp.json").with_merge(MergeStrategy::Deep),
),
Platform::new("copilot", "GitHub Copilot", ".github")
.with_detection(".github/copilot-instructions.md")
.with_detection(".github/instructions")
.with_detection(".github/skills")
.with_detection(".github/prompts")
.with_detection("AGENTS.md")
.with_transform(
TransformRule::new(
"rules/**/*.md",
".github/instructions/{name}.instructions.md",
)
.with_extension("instructions.md"),
)
.with_transform(
TransformRule::new("commands/**/*.md", ".github/prompts/{name}.prompt.md")
.with_extension("prompt.md"),
)
.with_transform(TransformRule::new(
"agents/**/*.md",
".github/agents/{name}/AGENTS.md",
))
.with_transform(TransformRule::new(
"skills/**/SKILL.md",
".github/skills/{name}/SKILL.md",
))
.with_transform(TransformRule::new(
"skills/**/*",
".github/skills/{name}/**/*",
))
.with_transform(
TransformRule::new("mcp.jsonc", ".github/mcp.json").with_merge(MergeStrategy::Deep),
)
.with_transform(
TransformRule::new("AGENTS.md", "AGENTS.md").with_merge(MergeStrategy::Composite),
),
Platform::new("cursor", "Cursor", ".cursor")
.with_detection(".cursor")
.with_detection("AGENTS.md")
.with_transform(TransformRule::new(
"commands/**/*.md",
".cursor/commands/**/*.md",
))
.with_transform(
TransformRule::new("rules/**/*.md", ".cursor/rules/**/*.mdc").with_extension("mdc"),
)
.with_transform(TransformRule::new(
"agents/**/*.md",
".cursor/agents/**/*.md",
))
.with_transform(TransformRule::new(
"skills/**/SKILL.md",
".cursor/skills/{name}/SKILL.md",
))
.with_transform(TransformRule::new(
"skills/**/*",
".cursor/skills/{name}/**/*",
))
.with_transform(
TransformRule::new("mcp.jsonc", ".cursor/mcp.json").with_merge(MergeStrategy::Deep),
)
.with_transform(
TransformRule::new("AGENTS.md", "AGENTS.md").with_merge(MergeStrategy::Composite),
),
Platform::new("codex", "Codex CLI", ".codex")
.with_detection(".codex")
.with_detection("AGENTS.md")
.with_transform(TransformRule::new(
"commands/**/*.md",
".codex/prompts/**/*.md",
))
.with_transform(TransformRule::new(
"skills/**/SKILL.md",
".codex/skills/{name}/SKILL.md",
))
.with_transform(TransformRule::new(
"skills/**/*",
".codex/skills/{name}/**/*",
))
.with_transform(
TransformRule::new("mcp.jsonc", ".codex/config.toml")
.with_merge(MergeStrategy::Deep),
)
.with_transform(
TransformRule::new("AGENTS.md", "AGENTS.md").with_merge(MergeStrategy::Composite),
),
Platform::new("factory", "Factory AI", ".factory")
.with_detection(".factory")
.with_detection("AGENTS.md")
.with_transform(TransformRule::new(
"commands/**/*.md",
".factory/commands/**/*.md",
))
.with_transform(TransformRule::new(
"agents/**/*.md",
".factory/droids/**/*.md",
))
.with_transform(TransformRule::new(
"skills/**/SKILL.md",
".factory/skills/{name}/SKILL.md",
))
.with_transform(TransformRule::new(
"skills/**/*",
".factory/skills/{name}/**/*",
))
.with_transform(
TransformRule::new("mcp.jsonc", ".factory/settings/mcp.json")
.with_merge(MergeStrategy::Deep),
)
.with_transform(
TransformRule::new("AGENTS.md", "AGENTS.md").with_merge(MergeStrategy::Composite),
),
Platform::new("junie", "JetBrains Junie", ".junie")
.with_detection(".junie")
.with_detection("AGENTS.md")
.with_transform(
TransformRule::new("rules/**/*.md", ".junie/guidelines.md")
.with_merge(MergeStrategy::Composite),
)
.with_transform(TransformRule::new(
"commands/**/*.md",
".junie/commands/**/*.md",
))
.with_transform(TransformRule::new(
"agents/**/*.md",
".junie/agents/**/*.md",
))
.with_transform(TransformRule::new(
"skills/**/SKILL.md",
".junie/skills/{name}/SKILL.md",
))
.with_transform(TransformRule::new(
"skills/**/*",
".junie/skills/{name}/**/*",
))
.with_transform(
TransformRule::new("mcp.jsonc", ".junie/mcp.json").with_merge(MergeStrategy::Deep),
)
.with_transform(
TransformRule::new("AGENTS.md", "AGENTS.md").with_merge(MergeStrategy::Composite),
),
Platform::new("kilo", "Kilo Code", ".kilocode")
.with_detection(".kilocode")
.with_detection("AGENTS.md")
.with_transform(TransformRule::new(
"rules/**/*.md",
".kilocode/rules/**/*.md",
))
.with_transform(TransformRule::new(
"commands/**/*.md",
".kilocode/workflows/**/*.md",
))
.with_transform(TransformRule::new(
"skills/**/SKILL.md",
".kilocode/skills/{name}/SKILL.md",
))
.with_transform(TransformRule::new(
"skills/**/*",
".kilocode/skills/{name}/**/*",
))
.with_transform(
TransformRule::new("mcp.jsonc", ".kilocode/mcp.json")
.with_merge(MergeStrategy::Deep),
)
.with_transform(
TransformRule::new("AGENTS.md", "AGENTS.md").with_merge(MergeStrategy::Composite),
),
Platform::new("kiro", "Kiro", ".kiro")
.with_detection(".kiro")
.with_transform(TransformRule::new(
"rules/**/*.md",
".kiro/steering/**/*.md",
))
.with_transform(
TransformRule::new("mcp.jsonc", ".kiro/settings/mcp.json")
.with_merge(MergeStrategy::Deep),
),
Platform::new("opencode", "OpenCode", ".opencode")
.with_detection(".opencode")
.with_detection("AGENTS.md")
.with_transform(TransformRule::new(
"commands/**/*.md",
".opencode/commands/**/*.md",
))
.with_transform(TransformRule::new(
"rules/**/*.md",
".opencode/rules/**/*.md",
))
.with_transform(TransformRule::new(
"agents/**/*.md",
".opencode/agents/**/*.md",
))
.with_transform(TransformRule::new(
"skills/**/SKILL.md",
".opencode/skills/{name}/SKILL.md",
))
.with_transform(TransformRule::new(
"skills/**/*",
".opencode/skills/{name}/**/*",
))
.with_transform(
TransformRule::new("mcp.jsonc", ".opencode/opencode.json")
.with_merge(MergeStrategy::Deep),
)
.with_transform(
TransformRule::new("AGENTS.md", "AGENTS.md").with_merge(MergeStrategy::Composite),
),
Platform::new("qwen", "Qwen Code", ".qwen")
.with_detection(".qwen")
.with_detection("QWEN.md")
.with_transform(TransformRule::new("agents/**/*.md", ".qwen/agents/**/*.md"))
.with_transform(TransformRule::new(
"skills/**/SKILL.md",
".qwen/skills/{name}/SKILL.md",
))
.with_transform(TransformRule::new(
"skills/**/*",
".qwen/skills/{name}/**/*",
))
.with_transform(
TransformRule::new("AGENTS.md", "QWEN.md").with_merge(MergeStrategy::Composite),
)
.with_transform(
TransformRule::new("mcp.jsonc", ".qwen/settings.json")
.with_merge(MergeStrategy::Deep),
),
Platform::new("roo", "Roo Code", ".roo")
.with_detection(".roo")
.with_detection("AGENTS.md")
.with_transform(TransformRule::new(
"commands/**/*.md",
".roo/commands/**/*.md",
))
.with_transform(TransformRule::new(
"skills/**/SKILL.md",
".roo/skills/{name}/SKILL.md",
))
.with_transform(TransformRule::new("skills/**/*", ".roo/skills/{name}/**/*"))
.with_transform(
TransformRule::new("mcp.jsonc", ".roo/mcp.json").with_merge(MergeStrategy::Deep),
)
.with_transform(
TransformRule::new("AGENTS.md", "AGENTS.md").with_merge(MergeStrategy::Composite),
),
Platform::new("warp", "Warp", ".warp")
.with_detection(".warp")
.with_detection("WARP.md")
.with_transform(
TransformRule::new("AGENTS.md", "WARP.md").with_merge(MergeStrategy::Composite),
),
Platform::new("windsurf", "Windsurf", ".windsurf")
.with_detection(".windsurf")
.with_transform(TransformRule::new(
"rules/**/*.md",
".windsurf/rules/**/*.md",
))
.with_transform(TransformRule::new(
"skills/**/SKILL.md",
".windsurf/skills/{name}/SKILL.md",
))
.with_transform(TransformRule::new(
"skills/**/*",
".windsurf/skills/{name}/**/*",
)),
Platform::new("gemini", "Gemini CLI", ".gemini")
.with_detection(".gemini")
.with_detection("GEMINI.md")
.with_transform(TransformRule::new(
"commands/**/*.md",
".gemini/commands/**/*.md",
))
.with_transform(TransformRule::new(
"agents/**/*.md",
".gemini/agents/**/*.md",
))
.with_transform(TransformRule::new(
"skills/**/SKILL.md",
".gemini/skills/{name}/SKILL.md",
))
.with_transform(TransformRule::new(
"skills/**/*",
".gemini/skills/{name}/**/*",
))
.with_transform(
TransformRule::new("mcp.jsonc", ".gemini/settings.json")
.with_merge(MergeStrategy::Deep),
)
.with_transform(
TransformRule::new("AGENTS.md", "GEMINI.md").with_merge(MergeStrategy::Composite),
)
.with_transform(TransformRule::new("root/**/*", ".gemini/**/*")),
]
}
#[cfg(test)]
mod unit_tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_platform_new() {
let platform = Platform::new("test", "Test Platform", ".test");
assert_eq!(platform.id, "test");
assert_eq!(platform.name, "Test Platform");
assert_eq!(platform.directory, ".test");
}
#[test]
fn test_platform_detection() {
let temp = TempDir::new_in(crate::temp::temp_dir_base()).unwrap();
std::fs::create_dir(temp.path().join(".claude")).unwrap();
let claude = Platform::new("claude", "Claude", ".claude").with_detection(".claude");
assert!(claude.is_detected(temp.path()));
let cursor = Platform::new("cursor", "Cursor", ".cursor").with_detection(".cursor");
assert!(!cursor.is_detected(temp.path()));
}
#[test]
fn test_transform_rule() {
let rule = TransformRule::new("commands/**/*.md", ".cursor/rules/**/*.mdc")
.with_merge(MergeStrategy::Replace)
.with_extension("mdc");
assert_eq!(rule.from, "commands/**/*.md");
assert_eq!(rule.to, ".cursor/rules/**/*.mdc");
assert_eq!(rule.merge, MergeStrategy::Replace);
assert_eq!(rule.extension, Some("mdc".to_string()));
}
#[test]
fn test_default_platforms() {
let platforms = default_platforms();
assert_eq!(platforms.len(), 17);
let ids: Vec<_> = platforms.iter().map(|p| p.id.as_str()).collect();
assert!(ids.contains(&"antigravity"));
assert!(ids.contains(&"augment"));
assert!(ids.contains(&"claude"));
assert!(ids.contains(&"claude-plugin"));
assert!(ids.contains(&"codex"));
assert!(ids.contains(&"copilot"));
assert!(ids.contains(&"cursor"));
assert!(ids.contains(&"junie"));
assert!(ids.contains(&"factory"));
assert!(ids.contains(&"gemini"));
assert!(ids.contains(&"kilo"));
assert!(ids.contains(&"kiro"));
assert!(ids.contains(&"opencode"));
assert!(ids.contains(&"qwen"));
assert!(ids.contains(&"roo"));
assert!(ids.contains(&"warp"));
assert!(ids.contains(&"windsurf"));
}
}