From 7fc7c5f1dd12171ea52a7dc30b4440cedafe0eae Mon Sep 17 00:00:00 2001 From: proboscis Date: Tue, 20 Jan 2026 16:05:16 +0900 Subject: [PATCH] feat: add 'runbox skill export' command with multi-platform install guides Implement skill export functionality for AI coding assistants: - Add 'runbox skill list' to discover skills across platforms - Add 'runbox skill show ' to view skill details - Add 'runbox skill export --output ' to export skills Export creates: - SKILL.md (main skill file) - references/ and examples/ (if present) - INSTALL.md (unified guide) - install/.md for each platform - install.sh (auto-detect and install script) Supported platforms: - Claude Code (~/.claude/skills/) - OpenCode (~/.opencode/skills/) - Gemini CLI - Codex (OpenAI) - Cursor (~/.cursor/rules/) Closes: ISSUE-037 --- crates/runbox-cli/src/main.rs | 225 ++++++++++++ crates/runbox-cli/tests/skill_export.rs | 49 +++ crates/runbox-core/Cargo.toml | 1 + crates/runbox-core/src/lib.rs | 3 + crates/runbox-core/src/skill.rs | 299 +++++++++++++++ crates/runbox-core/src/skill_export.rs | 463 ++++++++++++++++++++++++ 6 files changed, 1040 insertions(+) create mode 100644 crates/runbox-cli/tests/skill_export.rs create mode 100644 crates/runbox-core/src/skill.rs create mode 100644 crates/runbox-core/src/skill_export.rs diff --git a/crates/runbox-cli/src/main.rs b/crates/runbox-cli/src/main.rs index 39cfe8c..3083b1f 100644 --- a/crates/runbox-cli/src/main.rs +++ b/crates/runbox-cli/src/main.rs @@ -675,6 +675,45 @@ RELATED COMMANDS: #[command(subcommand)] command: CreateCommands, }, + /// Manage skills (export, list) for AI coding assistants + #[command(after_help = "\ +EXAMPLES: + # List available skills + runbox skill list + + # Export a skill with installation guides + runbox skill export runbox-cli --output ./exported-skill + + # Show skill details + runbox skill show runbox-cli + +OUTPUT STRUCTURE: + exported-skill/ + ├── SKILL.md # The skill content + ├── references/ # Reference files (if any) + ├── examples/ # Example files (if any) + ├── INSTALL.md # Unified install guide + ├── install/ + │ ├── claude-code.md # Claude Code installation + │ ├── opencode.md # OpenCode installation + │ ├── gemini.md # Gemini CLI installation + │ ├── codex.md # Codex installation + │ └── cursor.md # Cursor installation + └── install.sh # Auto-install script + +SUPPORTED PLATFORMS: + - Claude Code (~/.claude/skills/) + - OpenCode (~/.opencode/skills/) + - Gemini CLI (project GEMINI.md) + - Codex (AGENTS.md) + - Cursor (~/.cursor/rules/) + +RELATED COMMANDS: + runbox template list List available templates")] + Skill { + #[command(subcommand)] + command: SkillCommands, + }, #[command(after_help = "\ EXAMPLES: # Show the complete tutorial @@ -946,6 +985,52 @@ EXAMPLES: }, } +#[derive(Subcommand)] +enum SkillCommands { + /// List all available skills from all platforms + #[command(after_help = "\ +EXAMPLES: + runbox skill list + +OUTPUT: + NAME PLATFORM PATH + ──────────────────────────────────────────────────────────── + runbox-cli Claude Code ~/.claude/skills/runbox-cli + daily-progress Claude Code ~/.claude/skills/daily-progress")] + List, + + /// Show details about a specific skill + #[command(after_help = "\ +EXAMPLES: + runbox skill show runbox-cli + runbox skill show daily-progress")] + Show { + /// Skill name + skill_name: String, + }, + + /// Export a skill with platform-specific installation guides + #[command(after_help = "\ +EXAMPLES: + # Export to a directory + runbox skill export runbox-cli --output ./my-skill + + # The output directory will contain: + # - SKILL.md (the skill file) + # - references/ (if any) + # - examples/ (if any) + # - INSTALL.md (unified guide) + # - install/ (platform-specific guides) + # - install.sh (auto-install script)")] + Export { + /// Skill name to export + skill_name: String, + /// Output directory + #[arg(short, long)] + output: PathBuf, + }, +} + fn main() -> Result<()> { let cli = Cli::parse(); let storage = if let Ok(home) = std::env::var("RUNBOX_HOME") { @@ -1077,6 +1162,11 @@ fn main() -> Result<()> { Commands::Create { command } => match command { CreateCommands::Record { from_file } => cmd_create_record(&storage, from_file), }, + Commands::Skill { command } => match command { + SkillCommands::List => cmd_skill_list(), + SkillCommands::Show { skill_name } => cmd_skill_show(&skill_name), + SkillCommands::Export { skill_name, output } => cmd_skill_export(&skill_name, &output), + }, Commands::Daemon { command } => match command { DaemonCommands::Start => cmd_daemon_start(), DaemonCommands::Stop => cmd_daemon_stop(), @@ -3021,3 +3111,138 @@ fn cmd_create_record(storage: &Storage, from_file: Option) -> Result<()> Ok(()) } + +// === Skill Commands === + +fn cmd_skill_list() -> Result<()> { + use runbox_core::{find_skills, Platform}; + + let skills = find_skills(); + + if skills.is_empty() { + println!("No skills found."); + println!(); + println!("Skills are searched in the following locations:"); + for platform in Platform::all() { + if let Some(dir) = platform.skill_dir() { + println!(" {} - {}", platform.name(), dir.display()); + } + } + return Ok(()); + } + + // Print header + println!( + "{:<25} {:<15} {}", + "NAME", "PLATFORM", "PATH" + ); + println!("{}", "─".repeat(80)); + + // Print each skill + for (platform, path, name) in &skills { + let path_str = path.to_string_lossy(); + let short_path = if path_str.len() > 40 { + format!("...{}", &path_str[path_str.len() - 37..]) + } else { + path_str.to_string() + }; + println!( + "{:<25} {:<15} {}", + name, + platform.name(), + short_path + ); + } + + println!(); + println!("{} skill(s) found", skills.len()); + + Ok(()) +} + +fn cmd_skill_show(skill_name: &str) -> Result<()> { + use runbox_core::{find_skill_by_name, Skill}; + + let (platform, path) = find_skill_by_name(skill_name) + .ok_or_else(|| anyhow::anyhow!("Skill not found: {}", skill_name))?; + + let skill = Skill::load(&path)?; + + println!("Skill: {}", skill.metadata.name); + println!("Platform: {}", platform.name()); + println!("Path: {}", path.display()); + if let Some(ref version) = skill.metadata.version { + println!("Version: {}", version); + } + println!(); + println!("Description:"); + println!(" {}", skill.metadata.description); + println!(); + + if !skill.references.is_empty() { + println!("References ({}):", skill.references.len()); + for ref_path in &skill.references { + println!(" - {}", ref_path.display()); + } + println!(); + } + + if !skill.examples.is_empty() { + println!("Examples ({}):", skill.examples.len()); + for ex_path in &skill.examples { + println!(" - {}", ex_path.display()); + } + println!(); + } + + println!("Content preview (first 20 lines):"); + println!("{}", "─".repeat(60)); + for (i, line) in skill.content.lines().take(20).enumerate() { + println!("{:3} │ {}", i + 1, line); + } + if skill.content.lines().count() > 20 { + println!("... ({} more lines)", skill.content.lines().count() - 20); + } + + Ok(()) +} + +fn cmd_skill_export(skill_name: &str, output: &Path) -> Result<()> { + use runbox_core::{find_skill_by_name, Skill}; + + let (_platform, path) = find_skill_by_name(skill_name) + .ok_or_else(|| anyhow::anyhow!("Skill not found: {}", skill_name))?; + + let skill = Skill::load(&path)?; + + println!("Exporting skill: {}", skill.metadata.name); + println!("Source: {}", path.display()); + println!("Output: {}", output.display()); + println!(); + + let result = skill.export(output)?; + + println!("Export complete!"); + println!(); + println!("Created files:"); + println!(" SKILL.md - Main skill file"); + if result.references_count > 0 { + println!(" references/ - {} reference file(s)", result.references_count); + } + if result.examples_count > 0 { + println!(" examples/ - {} example file(s)", result.examples_count); + } + println!(" INSTALL.md - Unified installation guide"); + println!(" install/ - Platform-specific guides"); + println!(" claude-code.md"); + println!(" opencode.md"); + println!(" gemini.md"); + println!(" codex.md"); + println!(" cursor.md"); + println!(" install.sh - Auto-install script"); + println!(); + println!("To install, run:"); + println!(" cd {} && ./install.sh", output.display()); + + Ok(()) +} diff --git a/crates/runbox-cli/tests/skill_export.rs b/crates/runbox-cli/tests/skill_export.rs new file mode 100644 index 0000000..3293a92 --- /dev/null +++ b/crates/runbox-cli/tests/skill_export.rs @@ -0,0 +1,49 @@ +use assert_cmd::Command; +use predicates::prelude::*; +use tempfile::tempdir; +use std::fs; + +#[test] +fn test_skill_list_help() { + let mut cmd = Command::cargo_bin("runbox").unwrap(); + cmd.arg("skill").arg("list").arg("--help"); + cmd.assert() + .success() + .stdout(predicate::str::contains("List all available skills")); +} + +#[test] +fn test_skill_export_help() { + let mut cmd = Command::cargo_bin("runbox").unwrap(); + cmd.arg("skill").arg("export").arg("--help"); + cmd.assert() + .success() + .stdout(predicate::str::contains("Export a skill with platform-specific installation guides")); +} + +#[test] +fn test_skill_export_missing_skill() { + let tmp = tempdir().unwrap(); + let output = tmp.path().join("output"); + + let mut cmd = Command::cargo_bin("runbox").unwrap(); + cmd.arg("skill") + .arg("export") + .arg("nonexistent-skill-12345") + .arg("--output") + .arg(&output); + cmd.assert() + .failure() + .stderr(predicate::str::contains("Skill not found")); +} + +#[test] +fn test_skill_show_missing_skill() { + let mut cmd = Command::cargo_bin("runbox").unwrap(); + cmd.arg("skill") + .arg("show") + .arg("nonexistent-skill-12345"); + cmd.assert() + .failure() + .stderr(predicate::str::contains("Skill not found")); +} diff --git a/crates/runbox-core/Cargo.toml b/crates/runbox-core/Cargo.toml index c0e2673..c4784aa 100644 --- a/crates/runbox-core/Cargo.toml +++ b/crates/runbox-core/Cargo.toml @@ -8,6 +8,7 @@ description = "Core types and logic for runbox" [dependencies] serde = { workspace = true } serde_json = { workspace = true } +serde_yaml = "0.9" uuid = { workspace = true } thiserror = { workspace = true } anyhow = { workspace = true } diff --git a/crates/runbox-core/src/lib.rs b/crates/runbox-core/src/lib.rs index 0704434..6fc7fdf 100644 --- a/crates/runbox-core/src/lib.rs +++ b/crates/runbox-core/src/lib.rs @@ -15,6 +15,8 @@ pub mod local_storage; pub mod record; pub mod task; pub mod index; +pub mod skill; +pub mod skill_export; pub use binding::BindingResolver; pub use config::{ConfigResolver, ConfigSource, ResolvedValue, RunboxConfig, VerboseLogger}; @@ -36,3 +38,4 @@ pub use local_storage::{LayeredStorage, Scope, locate_local_runbox_dir}; pub use record::{Record, RecordCommand, RecordGitState, RecordValidationError}; pub use task::{Task, TaskHandle, TaskRuntime, TaskStatus}; pub use index::{EntityType, Index, IndexedEntity}; +pub use skill::{find_skill_by_name, find_skills, ExportResult, Platform, Skill, SkillError, SkillMetadata}; diff --git a/crates/runbox-core/src/skill.rs b/crates/runbox-core/src/skill.rs new file mode 100644 index 0000000..78dc14d --- /dev/null +++ b/crates/runbox-core/src/skill.rs @@ -0,0 +1,299 @@ +//! Skill management for AI coding assistants +//! +//! Skills are reusable instruction sets that teach AI assistants how to use +//! specific tools or follow specific patterns. This module handles loading +//! skills from various AI assistant platforms and exporting them in a +//! portable format. + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fs; +use std::path::{Path, PathBuf}; +use thiserror::Error; + +/// Platform-specific skill storage locations and formats +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Platform { + /// Claude Code (~/.claude/skills/) + ClaudeCode, + /// OpenCode (~/.opencode/skills/) + OpenCode, + /// Gemini CLI + GeminiCli, + /// OpenAI Codex CLI + Codex, + /// Cursor IDE + Cursor, +} + +impl Platform { + /// Get the default skill directory for this platform + pub fn skill_dir(&self) -> Option { + let home = dirs::home_dir()?; + match self { + Platform::ClaudeCode => Some(home.join(".claude").join("skills")), + Platform::OpenCode => Some(home.join(".opencode").join("skills")), + Platform::GeminiCli => None, // No standard location yet + Platform::Codex => None, // No standard location yet + Platform::Cursor => Some(home.join(".cursor").join("rules")), + } + } + + /// Get the name of this platform + pub fn name(&self) -> &'static str { + match self { + Platform::ClaudeCode => "Claude Code", + Platform::OpenCode => "OpenCode", + Platform::GeminiCli => "Gemini CLI", + Platform::Codex => "Codex", + Platform::Cursor => "Cursor", + } + } + + /// Get the slug for this platform (used in filenames) + pub fn slug(&self) -> &'static str { + match self { + Platform::ClaudeCode => "claude-code", + Platform::OpenCode => "opencode", + Platform::GeminiCli => "gemini", + Platform::Codex => "codex", + Platform::Cursor => "cursor", + } + } + + /// All supported platforms + pub fn all() -> &'static [Platform] { + &[ + Platform::ClaudeCode, + Platform::OpenCode, + Platform::GeminiCli, + Platform::Codex, + Platform::Cursor, + ] + } +} + +/// Skill metadata from YAML frontmatter +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SkillMetadata { + pub name: String, + pub description: String, + #[serde(default)] + pub version: Option, + #[serde(flatten)] + pub extra: HashMap, +} + +/// A complete skill with metadata and content +#[derive(Debug, Clone)] +pub struct Skill { + /// Skill metadata from frontmatter + pub metadata: SkillMetadata, + /// Main skill content (markdown body) + pub content: String, + /// Path to the skill directory + pub path: PathBuf, + /// Reference files (relative paths within skill dir) + pub references: Vec, + /// Example files (relative paths within skill dir) + pub examples: Vec, +} + +impl Skill { + /// Load a skill from a directory + pub fn load(path: &Path) -> Result { + let skill_file = path.join("SKILL.md"); + if !skill_file.exists() { + return Err(SkillError::NotFound(path.to_path_buf())); + } + + let content = fs::read_to_string(&skill_file) + .map_err(|e| SkillError::ReadError(skill_file.clone(), e))?; + + let (metadata, body) = parse_frontmatter(&content)?; + + // Find reference files + let references = find_files_in_dir(&path.join("references")); + let examples = find_files_in_dir(&path.join("examples")); + + Ok(Self { + metadata, + content: body, + path: path.to_path_buf(), + references, + examples, + }) + } + + /// Get the skill name (from metadata or directory name) + pub fn name(&self) -> &str { + &self.metadata.name + } +} + +/// Result of a skill export operation +#[derive(Debug)] +pub struct ExportResult { + pub output_dir: PathBuf, + pub skill_file: PathBuf, + pub references_count: usize, + pub examples_count: usize, +} + +/// Skill-related errors +#[derive(Debug, Error)] +pub enum SkillError { + #[error("Skill not found: {0}")] + NotFound(PathBuf), + + #[error("Failed to read {0}: {1}")] + ReadError(PathBuf, std::io::Error), + + #[error("Failed to write {0}: {1}")] + WriteError(PathBuf, std::io::Error), + + #[error("Failed to copy {0} to {1}: {2}")] + CopyError(PathBuf, PathBuf, std::io::Error), + + #[error("Invalid frontmatter: {0}")] + InvalidFrontmatter(String), + + #[error("Missing frontmatter in skill file")] + MissingFrontmatter, +} + +/// Parse YAML frontmatter from a markdown file +fn parse_frontmatter(content: &str) -> Result<(SkillMetadata, String), SkillError> { + let content = content.trim(); + + if !content.starts_with("---") { + return Err(SkillError::MissingFrontmatter); + } + + // Find the closing --- + let rest = &content[3..]; + let end_pos = rest + .find("\n---") + .ok_or(SkillError::MissingFrontmatter)?; + + let yaml_str = &rest[..end_pos].trim(); + let body = rest[end_pos + 4..].trim(); + + let metadata: SkillMetadata = serde_yaml::from_str(yaml_str) + .map_err(|e| SkillError::InvalidFrontmatter(e.to_string()))?; + + Ok((metadata, body.to_string())) +} + +/// Format a skill file with frontmatter +pub fn format_skill_file(metadata: &SkillMetadata, content: &str) -> String { + let yaml = serde_yaml::to_string(metadata).unwrap_or_default(); + format!("---\n{}---\n\n{}", yaml, content) +} + +/// Find all files in a directory (recursively) +fn find_files_in_dir(dir: &Path) -> Vec { + let mut files = Vec::new(); + if dir.is_dir() { + if let Ok(entries) = fs::read_dir(dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_file() { + if let Ok(rel) = path.strip_prefix(dir) { + files.push(rel.to_path_buf()); + } + } else if path.is_dir() { + let subfiles = find_files_in_dir(&path); + for subfile in subfiles { + if let Ok(rel) = path.strip_prefix(dir) { + files.push(rel.join(&subfile)); + } + } + } + } + } + } + files.sort(); + files +} + +/// Find skills across all platforms +pub fn find_skills() -> Vec<(Platform, PathBuf, String)> { + let mut skills = Vec::new(); + + for platform in Platform::all() { + if let Some(skill_dir) = platform.skill_dir() { + if skill_dir.exists() { + if let Ok(entries) = fs::read_dir(&skill_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() && path.join("SKILL.md").exists() { + let name = path.file_name() + .and_then(|n| n.to_str()) + .map(|s| s.to_string()); + if let Some(name) = name { + skills.push((*platform, path, name)); + } + } + } + } + } + } + } + + skills.sort_by(|a, b| a.2.cmp(&b.2)); + skills +} + +/// Find a skill by name across all platforms +pub fn find_skill_by_name(name: &str) -> Option<(Platform, PathBuf)> { + for platform in Platform::all() { + if let Some(skill_dir) = platform.skill_dir() { + let skill_path = skill_dir.join(name); + if skill_path.exists() && skill_path.join("SKILL.md").exists() { + return Some((*platform, skill_path)); + } + } + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_frontmatter() { + let content = r#"--- +name: test-skill +description: A test skill +version: 1.0.0 +--- + +# Test Skill + +This is the skill content."#; + + let (metadata, body) = parse_frontmatter(content).unwrap(); + assert_eq!(metadata.name, "test-skill"); + assert_eq!(metadata.description, "A test skill"); + assert_eq!(metadata.version, Some("1.0.0".to_string())); + assert!(body.contains("# Test Skill")); + } + + #[test] + fn test_parse_frontmatter_missing() { + let content = "# No frontmatter here"; + assert!(matches!( + parse_frontmatter(content), + Err(SkillError::MissingFrontmatter) + )); + } + + #[test] + fn test_platform_slug() { + assert_eq!(Platform::ClaudeCode.slug(), "claude-code"); + assert_eq!(Platform::OpenCode.slug(), "opencode"); + assert_eq!(Platform::Cursor.slug(), "cursor"); + } +} diff --git a/crates/runbox-core/src/skill_export.rs b/crates/runbox-core/src/skill_export.rs new file mode 100644 index 0000000..e4989fd --- /dev/null +++ b/crates/runbox-core/src/skill_export.rs @@ -0,0 +1,463 @@ +//! Skill export functionality +//! +//! Generates platform-specific installation guides and exports skills +//! to a portable directory structure. + +use crate::skill::{format_skill_file, Platform, Skill, SkillError, ExportResult}; +use std::fs; +use std::path::Path; + +impl Skill { + /// Export the skill to a directory with platform-specific install guides + pub fn export(&self, output_dir: &Path) -> Result { + // Create output directory + fs::create_dir_all(output_dir) + .map_err(|e| SkillError::WriteError(output_dir.to_path_buf(), e))?; + + // Write SKILL.md + let skill_path = output_dir.join("SKILL.md"); + let skill_content = format_skill_file(&self.metadata, &self.content); + fs::write(&skill_path, &skill_content) + .map_err(|e| SkillError::WriteError(skill_path.clone(), e))?; + + // Copy references + if !self.references.is_empty() { + let refs_dir = output_dir.join("references"); + fs::create_dir_all(&refs_dir) + .map_err(|e| SkillError::WriteError(refs_dir.clone(), e))?; + + for ref_path in &self.references { + let src = self.path.join("references").join(ref_path); + let dst = refs_dir.join(ref_path); + if let Some(parent) = dst.parent() { + fs::create_dir_all(parent) + .map_err(|e| SkillError::WriteError(parent.to_path_buf(), e))?; + } + if src.exists() { + fs::copy(&src, &dst) + .map_err(|e| SkillError::CopyError(src.clone(), dst.clone(), e))?; + } + } + } + + // Copy examples + if !self.examples.is_empty() { + let examples_dir = output_dir.join("examples"); + fs::create_dir_all(&examples_dir) + .map_err(|e| SkillError::WriteError(examples_dir.clone(), e))?; + + for ex_path in &self.examples { + let src = self.path.join("examples").join(ex_path); + let dst = examples_dir.join(ex_path); + if let Some(parent) = dst.parent() { + fs::create_dir_all(parent) + .map_err(|e| SkillError::WriteError(parent.to_path_buf(), e))?; + } + if src.exists() { + fs::copy(&src, &dst) + .map_err(|e| SkillError::CopyError(src.clone(), dst.clone(), e))?; + } + } + } + + // Generate install guides + let install_dir = output_dir.join("install"); + fs::create_dir_all(&install_dir) + .map_err(|e| SkillError::WriteError(install_dir.clone(), e))?; + + for platform in Platform::all() { + let guide = generate_install_guide(&self.metadata.name, platform); + let guide_path = install_dir.join(format!("{}.md", platform.slug())); + fs::write(&guide_path, guide) + .map_err(|e| SkillError::WriteError(guide_path.clone(), e))?; + } + + // Generate unified INSTALL.md + let install_md = generate_unified_install(&self.metadata.name); + let install_path = output_dir.join("INSTALL.md"); + fs::write(&install_path, install_md) + .map_err(|e| SkillError::WriteError(install_path.clone(), e))?; + + // Generate install.sh + let install_sh = generate_install_script(&self.metadata.name); + let script_path = output_dir.join("install.sh"); + fs::write(&script_path, install_sh) + .map_err(|e| SkillError::WriteError(script_path.clone(), e))?; + + // Make install.sh executable on Unix + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&script_path) + .map_err(|e| SkillError::WriteError(script_path.clone(), e))? + .permissions(); + perms.set_mode(0o755); + fs::set_permissions(&script_path, perms) + .map_err(|e| SkillError::WriteError(script_path.clone(), e))?; + } + + Ok(ExportResult { + output_dir: output_dir.to_path_buf(), + skill_file: skill_path, + references_count: self.references.len(), + examples_count: self.examples.len(), + }) + } +} + +/// Generate a platform-specific installation guide +fn generate_install_guide(skill_name: &str, platform: &Platform) -> String { + match platform { + Platform::ClaudeCode => format!( + r#"# Installing {} for Claude Code + +## Location + +Skills for Claude Code are stored in: +``` +~/.claude/skills/{}/ +``` + +## Installation Steps + +1. **Create the skill directory:** + ```bash + mkdir -p ~/.claude/skills/{} + ``` + +2. **Copy the skill files:** + ```bash + cp SKILL.md ~/.claude/skills/{}/ + cp -r references/ ~/.claude/skills/{}/ 2>/dev/null || true + cp -r examples/ ~/.claude/skills/{}/ 2>/dev/null || true + ``` + +3. **Verify installation:** + ```bash + ls -la ~/.claude/skills/{}/ + ``` + +## Skill Format + +Claude Code skills use YAML frontmatter in a SKILL.md file: + +```markdown +--- +name: {} +description: When to use this skill +version: 1.0.0 +--- + +# Skill content here +``` + +## Usage + +Once installed, Claude Code will automatically load the skill based on the +trigger phrases in the description field. +"#, + skill_name, skill_name, skill_name, skill_name, skill_name, skill_name, skill_name, skill_name + ), + + Platform::OpenCode => format!( + r#"# Installing {} for OpenCode + +## Location + +Skills for OpenCode are stored in: +``` +~/.opencode/skills/{}/ +``` + +## Installation Steps + +1. **Create the skill directory:** + ```bash + mkdir -p ~/.opencode/skills/{} + ``` + +2. **Copy the skill files:** + ```bash + cp SKILL.md ~/.opencode/skills/{}/ + cp -r references/ ~/.opencode/skills/{}/ 2>/dev/null || true + cp -r examples/ ~/.opencode/skills/{}/ 2>/dev/null || true + ``` + +3. **Verify installation:** + ```bash + ls -la ~/.opencode/skills/{}/ + ``` + +## Skill Format + +OpenCode uses the same format as Claude Code - YAML frontmatter in SKILL.md: + +```markdown +--- +name: {} +description: When to use this skill +version: 1.0.0 +--- + +# Skill content here +``` + +## Usage + +OpenCode will automatically detect and load installed skills. +"#, + skill_name, skill_name, skill_name, skill_name, skill_name, skill_name, skill_name, skill_name + ), + + Platform::GeminiCli => format!( + r#"# Installing {} for Gemini CLI + +## Status + +Gemini CLI custom instruction support is still evolving. This guide will be +updated as the platform matures. + +## Current Options + +### Option 1: GEMINI.md File (Project-level) + +Create a `GEMINI.md` file in your project root: + +```bash +cp SKILL.md ./GEMINI.md +``` + +### Option 2: System Instructions + +Gemini CLI may support system instructions via configuration. Check the +latest Gemini CLI documentation for the current approach. + +## Manual Usage + +For now, you can reference the skill content manually when starting a +Gemini CLI session or include it in your project's context. + +## Resources + +- [Gemini CLI Documentation](https://github.com/google-gemini/gemini-cli) +"#, + skill_name + ), + + Platform::Codex => format!( + r#"# Installing {} for OpenAI Codex CLI + +## Status + +The OpenAI Codex CLI instruction format is still being documented. This +guide will be updated as more information becomes available. + +## Current Options + +### Option 1: AGENTS.md File + +Some OpenAI tools support an AGENTS.md file for custom instructions: + +```bash +cp SKILL.md ./AGENTS.md +``` + +### Option 2: Configuration File + +Check if your Codex CLI version supports a configuration file with +custom instructions. + +## Manual Usage + +You can include the skill content in your prompts or session initialization. + +## Resources + +- [OpenAI Codex Documentation](https://platform.openai.com/docs) +"#, + skill_name + ), + + Platform::Cursor => format!( + r#"# Installing {} for Cursor + +## Location + +Cursor rules can be stored in two locations: + +1. **Global rules:** `~/.cursor/rules/` +2. **Project rules:** `.cursor/rules/` (in your project directory) + +## Installation Steps + +### Global Installation + +1. **Create the rules directory:** + ```bash + mkdir -p ~/.cursor/rules + ``` + +2. **Copy the skill as a rule:** + ```bash + cp SKILL.md ~/.cursor/rules/{}.md + ``` + +### Project-level Installation + +1. **Create the project rules directory:** + ```bash + mkdir -p .cursor/rules + ``` + +2. **Copy the skill:** + ```bash + cp SKILL.md .cursor/rules/{}.md + ``` + +## Cursor Rules Format + +Cursor uses markdown files without specific frontmatter requirements. +The skill content will work directly, though you may want to remove +the YAML frontmatter: + +```bash +# Remove frontmatter and save +sed '1,/^---$/d' SKILL.md | sed '1,/^---$/d' > ~/.cursor/rules/{}.md +``` + +## Alternative: .cursorrules File + +For simpler cases, you can append to a `.cursorrules` file in your project: + +```bash +cat SKILL.md >> .cursorrules +``` + +## Usage + +Cursor will automatically include rules from the rules directory +in its context. +"#, + skill_name, skill_name, skill_name, skill_name + ), + } +} + +/// Generate a unified installation guide +fn generate_unified_install(skill_name: &str) -> String { + format!( + r#"# Installing {} + +This skill can be installed into multiple AI coding assistants. Choose your platform below. + +## Quick Install (Auto-detect) + +Run the install script to automatically detect your installed tools and install the skill: + +```bash +./install.sh +``` + +## Platform-Specific Guides + +| Platform | Guide | Location | +|----------|-------|----------| +| Claude Code | [install/claude-code.md](install/claude-code.md) | `~/.claude/skills/{}/` | +| OpenCode | [install/opencode.md](install/opencode.md) | `~/.opencode/skills/{}/` | +| Gemini CLI | [install/gemini.md](install/gemini.md) | Project-level GEMINI.md | +| Codex | [install/codex.md](install/codex.md) | AGENTS.md or configuration | +| Cursor | [install/cursor.md](install/cursor.md) | `~/.cursor/rules/` or `.cursor/rules/` | + +## Manual Installation + +For any platform, the core installation is: + +1. Copy `SKILL.md` to the platform's skill directory +2. Copy the `references/` directory if it exists +3. Copy the `examples/` directory if it exists + +## Skill Contents + +- `SKILL.md` - Main skill file with instructions +- `references/` - Reference documentation and schemas +- `examples/` - Example configurations and usage patterns +- `install/` - Platform-specific installation guides + +## Verification + +After installation, verify by asking the AI assistant about the skill's topic. +The assistant should reference the skill content in its response. +"#, + skill_name, skill_name, skill_name + ) +} + +/// Generate an install script +fn generate_install_script(skill_name: &str) -> String { + format!( + r##"#!/usr/bin/env bash +# Install script for {} skill +# Auto-detects installed AI coding assistants and installs the skill + +set -e + +SKILL_NAME="{}" +SCRIPT_DIR="$(cd "$(dirname "${{BASH_SOURCE[0]}}")" && pwd)" + +echo "Installing skill: $SKILL_NAME" +echo "" + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +installed=0 + +# Claude Code +CLAUDE_SKILLS_DIR="$HOME/.claude/skills" +if [ -d "$HOME/.claude" ] || command -v claude &> /dev/null; then + echo -e "${{GREEN}}Found Claude Code${{NC}}" + mkdir -p "$CLAUDE_SKILLS_DIR/$SKILL_NAME" + cp "$SCRIPT_DIR/SKILL.md" "$CLAUDE_SKILLS_DIR/$SKILL_NAME/" + [ -d "$SCRIPT_DIR/references" ] && cp -r "$SCRIPT_DIR/references" "$CLAUDE_SKILLS_DIR/$SKILL_NAME/" 2>/dev/null || true + [ -d "$SCRIPT_DIR/examples" ] && cp -r "$SCRIPT_DIR/examples" "$CLAUDE_SKILLS_DIR/$SKILL_NAME/" 2>/dev/null || true + echo " Installed to: $CLAUDE_SKILLS_DIR/$SKILL_NAME/" + installed=$((installed + 1)) +fi + +# OpenCode +OPENCODE_SKILLS_DIR="$HOME/.opencode/skills" +if [ -d "$HOME/.opencode" ] || [ -d "$HOME/.config/opencode" ] || command -v opencode &> /dev/null; then + echo -e "${{GREEN}}Found OpenCode${{NC}}" + mkdir -p "$OPENCODE_SKILLS_DIR/$SKILL_NAME" + cp "$SCRIPT_DIR/SKILL.md" "$OPENCODE_SKILLS_DIR/$SKILL_NAME/" + [ -d "$SCRIPT_DIR/references" ] && cp -r "$SCRIPT_DIR/references" "$OPENCODE_SKILLS_DIR/$SKILL_NAME/" 2>/dev/null || true + [ -d "$SCRIPT_DIR/examples" ] && cp -r "$SCRIPT_DIR/examples" "$OPENCODE_SKILLS_DIR/$SKILL_NAME/" 2>/dev/null || true + echo " Installed to: $OPENCODE_SKILLS_DIR/$SKILL_NAME/" + installed=$((installed + 1)) +fi + +# Cursor +CURSOR_RULES_DIR="$HOME/.cursor/rules" +if [ -d "$HOME/.cursor" ] || [ -d "/Applications/Cursor.app" ]; then + echo -e "${{GREEN}}Found Cursor${{NC}}" + mkdir -p "$CURSOR_RULES_DIR" + cp "$SCRIPT_DIR/SKILL.md" "$CURSOR_RULES_DIR/$SKILL_NAME.md" + echo " Installed to: $CURSOR_RULES_DIR/$SKILL_NAME.md" + installed=$((installed + 1)) +fi + +echo "" +if [ $installed -eq 0 ]; then + echo -e "${{YELLOW}}No supported AI coding assistants found.${{NC}}" + echo "See INSTALL.md for manual installation instructions." + exit 1 +else + echo -e "${{GREEN}}Successfully installed to $installed platform(s)${{NC}}" +fi +"##, + skill_name, skill_name + ) +}