use std::fs;
use std::path::{Path, PathBuf};
const CLAUDE_ADAPTER: &str = include_str!("../assets/skills/claude-port.md");
const SKILL_GUIDE: &str = include_str!("../assets/skills/port-guide.md");
const SKILL_INSTRUCTIONS: &str = include_str!("../assets/skills/port-instructions.md");
struct SkillInfo {
name: &'static str,
description: &'static str,
}
const SKILLS: &[SkillInfo] = &[
SkillInfo {
name: "port",
description: "Port PyTorch scripts to flodl",
},
];
#[derive(Debug, Clone, Copy)]
enum Tool {
Claude,
Cursor,
}
impl Tool {
fn name(&self) -> &'static str {
match self {
Tool::Claude => "Claude Code",
Tool::Cursor => "Cursor",
}
}
}
fn detect_tools() -> Vec<Tool> {
let mut tools = Vec::new();
if Path::new(".claude").is_dir() || Path::new(".claude").exists() {
tools.push(Tool::Claude);
}
if Path::new(".cursor").is_dir() || Path::new(".cursorrules").exists() {
tools.push(Tool::Cursor);
}
tools
}
fn parse_tool(name: &str) -> Option<Tool> {
match name.to_lowercase().as_str() {
"claude" | "claude-code" => Some(Tool::Claude),
"cursor" => Some(Tool::Cursor),
_ => None,
}
}
fn find_ai_dir() -> Option<PathBuf> {
let mut dir = std::env::current_dir().ok()?;
for _ in 0..5 {
let candidate = dir.join("ai/skills");
if candidate.is_dir() {
return Some(dir.join("ai"));
}
if !dir.pop() {
break;
}
}
None
}
pub fn install(tool_override: Option<&str>, skill_filter: Option<&str>) -> Result<(), String> {
let tools = if let Some(name) = tool_override {
vec![parse_tool(name).ok_or_else(|| {
format!("unknown tool: '{}'. Supported: claude, cursor", name)
})?]
} else {
let detected = detect_tools();
if detected.is_empty() {
println!("No AI tool config detected. Defaulting to Claude Code.");
println!(" (Override with: fdl skill install --tool cursor)");
println!();
vec![Tool::Claude]
} else {
detected
}
};
let ai_dir = find_ai_dir();
for tool in &tools {
match tool {
Tool::Claude => install_claude(&ai_dir, skill_filter)?,
Tool::Cursor => install_cursor(&ai_dir, skill_filter)?,
}
}
Ok(())
}
fn install_claude(ai_dir: &Option<PathBuf>, skill_filter: Option<&str>) -> Result<(), String> {
let skills_to_install: Vec<&SkillInfo> = SKILLS.iter()
.filter(|s| skill_filter.is_none() || skill_filter == Some(s.name))
.collect();
if skills_to_install.is_empty() {
return Err(format!(
"unknown skill: '{}'. Available: {}",
skill_filter.unwrap_or(""),
SKILLS.iter().map(|s| s.name).collect::<Vec<_>>().join(", ")
));
}
for skill in &skills_to_install {
let skill_dir = PathBuf::from(format!(".claude/skills/{}", skill.name));
let updating = skill_dir.join("SKILL.md").exists();
fs::create_dir_all(&skill_dir)
.map_err(|e| format!("cannot create {}: {}", skill_dir.display(), e))?;
let adapter_content = if let Some(ai) = ai_dir {
let adapter_path = ai.join("adapters/claude/port-skill.md");
fs::read_to_string(&adapter_path).unwrap_or_else(|_| CLAUDE_ADAPTER.to_string())
} else {
CLAUDE_ADAPTER.to_string()
};
write_file(&skill_dir.join("SKILL.md"), &adapter_content)?;
let guide_content = if let Some(ai) = ai_dir {
let path = ai.join(format!("skills/{}/guide.md", skill.name));
fs::read_to_string(&path).unwrap_or_else(|_| SKILL_GUIDE.to_string())
} else {
SKILL_GUIDE.to_string()
};
write_file(&skill_dir.join("guide.md"), &guide_content)?;
let instructions_content = if let Some(ai) = ai_dir {
let path = ai.join(format!("skills/{}/instructions.md", skill.name));
fs::read_to_string(&path).unwrap_or_else(|_| SKILL_INSTRUCTIONS.to_string())
} else {
SKILL_INSTRUCTIONS.to_string()
};
write_file(&skill_dir.join("instructions.md"), &instructions_content)?;
let verb = if updating { "Updated" } else { "Installed" };
println!(" {} /{} skill for Claude Code", verb, skill.name);
println!(" -> .claude/skills/{}/SKILL.md", skill.name);
println!(" -> .claude/skills/{}/guide.md", skill.name);
println!(" -> .claude/skills/{}/instructions.md", skill.name);
}
println!();
println!("Claude Code skills ready. Try: /port my_model.py");
Ok(())
}
fn install_cursor(ai_dir: &Option<PathBuf>, skill_filter: Option<&str>) -> Result<(), String> {
if skill_filter.is_some() && skill_filter != Some("port") {
return Err(format!("unknown skill: '{}'", skill_filter.unwrap_or("")));
}
let rules_path = PathBuf::from(".cursorrules");
let existing = fs::read_to_string(&rules_path).unwrap_or_default();
if existing.contains("flodl porting") {
println!(" Cursor rules already contain flodl porting context.");
return Ok(());
}
let guide_content = if let Some(ai) = ai_dir {
let path = ai.join("skills/port/guide.md");
fs::read_to_string(&path).unwrap_or_else(|_| SKILL_GUIDE.to_string())
} else {
SKILL_GUIDE.to_string()
};
let cursor_block = format!(
"\n\n# flodl porting\n\n\
When asked to port PyTorch code to flodl, follow this guide:\n\n\
{}\n",
guide_content
);
let new_content = format!("{}{}", existing, cursor_block);
write_file(&rules_path, &new_content)?;
println!(" Installed flodl porting context for Cursor");
println!(" -> .cursorrules (appended)");
println!();
println!("Cursor ready. Ask: \"Port this PyTorch code to flodl\"");
Ok(())
}
fn write_file(path: &Path, content: &str) -> Result<(), String> {
fs::write(path, content)
.map_err(|e| format!("cannot write {}: {}", path.display(), e))
}
pub fn list() {
println!("Available skills:");
println!();
for skill in SKILLS {
println!(" {:<12} {}", skill.name, skill.description);
}
println!();
let tools = detect_tools();
if tools.is_empty() {
println!("No AI tool detected. Install with: fdl skill install");
} else {
println!("Detected tools:");
for tool in &tools {
let installed = check_installed(tool);
let status = if installed { "installed" } else { "not installed" };
println!(" {:<16} {}", tool.name(), status);
}
println!();
if tools.iter().any(|t| !check_installed(t)) {
println!("Run: fdl skill install");
}
}
}
fn check_installed(tool: &Tool) -> bool {
match tool {
Tool::Claude => Path::new(".claude/skills/port/SKILL.md").exists(),
Tool::Cursor => {
fs::read_to_string(".cursorrules")
.map(|c| c.contains("flodl porting"))
.unwrap_or(false)
}
}
}
pub fn print_usage() {
println!("fdl skill -- manage AI coding assistant skills");
println!();
println!("USAGE:");
println!(" fdl skill <command> [options]");
println!();
println!("COMMANDS:");
println!(" install Install skills for detected AI tool");
println!(" --tool <name> Force a specific tool (claude, cursor)");
println!(" --skill <name> Install only one skill");
println!(" list Show available skills and detected tools");
println!();
println!("SUPPORTED TOOLS:");
println!(" claude Claude Code (.claude/skills/)");
println!(" cursor Cursor (.cursorrules)");
println!();
println!("EXAMPLES:");
println!(" fdl skill install # auto-detect tool, install all skills");
println!(" fdl skill install --tool claude # force Claude Code");
println!(" fdl skill list # show what's available");
}