Skip to main content

dstack/
cmd_skills.rs

1use crate::config::Config;
2use std::path::{Path, PathBuf};
3
4fn skills_repo_path(cfg: &Config) -> anyhow::Result<PathBuf> {
5    let path = cfg
6        .skills_repo
7        .as_deref()
8        .ok_or_else(|| anyhow::anyhow!(
9            "No skills_repo configured. Set skills_repo in ~/.config/dstack/config.toml"
10        ))?;
11    let p = PathBuf::from(path);
12    if !p.exists() {
13        anyhow::bail!(
14            "Skills repo not found at {}. Set skills_repo to a valid local path",
15            path
16        );
17    }
18    Ok(p)
19}
20
21fn claude_skills_dir() -> PathBuf {
22    dirs::home_dir()
23        .unwrap_or_else(|| PathBuf::from("/root"))
24        .join(".claude")
25        .join("skills")
26}
27
28fn available_skills(repo: &Path) -> Vec<String> {
29    let mut skills = Vec::new();
30    if let Ok(entries) = std::fs::read_dir(repo) {
31        for entry in entries.flatten() {
32            let path = entry.path();
33            if path.is_dir() && path.join("SKILL.md").exists() {
34                if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
35                    skills.push(name.to_string());
36                }
37            }
38        }
39    }
40    skills.sort();
41    skills
42}
43
44pub fn list(cfg: &Config) -> anyhow::Result<()> {
45    let repo = skills_repo_path(cfg)?;
46    let target = claude_skills_dir();
47    let skills = available_skills(&repo);
48
49    if skills.is_empty() {
50        eprintln!("No skills found in {}", repo.display());
51        return Ok(());
52    }
53
54    println!(
55        "{:<25} {:<10} {}",
56        "SKILL", "STATUS", "DESCRIPTION"
57    );
58    println!("{}", "-".repeat(65));
59
60    for name in &skills {
61        let installed = target.join(name).join("SKILL.md").exists();
62        let status = if installed { "installed" } else { "-" };
63
64        // Read first line of description from SKILL.md frontmatter
65        let desc = read_skill_description(&repo.join(name).join("SKILL.md"));
66
67        println!("{:<25} {:<10} {}", name, status, desc);
68    }
69
70    println!(
71        "\n{} skill(s) available. Install with: dstack skills install <name>",
72        skills.len()
73    );
74    Ok(())
75}
76
77pub fn install(cfg: &Config, name: &str) -> anyhow::Result<()> {
78    let repo = skills_repo_path(cfg)?;
79    let source = repo.join(name).join("SKILL.md");
80    if !source.exists() {
81        anyhow::bail!(
82            "Skill '{}' not found in {}. Run: dstack skills list",
83            name,
84            repo.display()
85        );
86    }
87
88    let target_dir = claude_skills_dir().join(name);
89    std::fs::create_dir_all(&target_dir)?;
90    std::fs::copy(&source, target_dir.join("SKILL.md"))?;
91
92    eprintln!("Installed: {} → {}", name, target_dir.display());
93    Ok(())
94}
95
96pub fn sync_all(cfg: &Config) -> anyhow::Result<()> {
97    let repo = skills_repo_path(cfg)?;
98    let skills = available_skills(&repo);
99    let mut installed = 0;
100    let mut skipped = 0;
101
102    for name in &skills {
103        let target = claude_skills_dir().join(name).join("SKILL.md");
104        if target.exists() {
105            skipped += 1;
106            continue;
107        }
108        install(cfg, name)?;
109        installed += 1;
110    }
111
112    eprintln!(
113        "\n{} installed, {} already present, {} total.",
114        installed, skipped, skills.len()
115    );
116    Ok(())
117}
118
119pub fn update(cfg: &Config) -> anyhow::Result<()> {
120    let repo = skills_repo_path(cfg)?;
121
122    // Pull latest if it's a git repo
123    if repo.join(".git").exists() {
124        eprint!("Pulling latest skills... ");
125        let status = std::process::Command::new("git")
126            .args(["-C", &repo.to_string_lossy(), "pull", "--ff-only"])
127            .status()?;
128        if status.success() {
129            eprintln!("done.");
130        } else {
131            eprintln!("pull failed (diverged?)");
132        }
133    }
134
135    // Overwrite all installed skills with latest from repo
136    let skills = available_skills(&repo);
137    let target_root = claude_skills_dir();
138    let mut updated = 0;
139
140    for name in &skills {
141        let target = target_root.join(name).join("SKILL.md");
142        if target.exists() {
143            let source = repo.join(name).join("SKILL.md");
144            std::fs::copy(&source, &target)?;
145            updated += 1;
146        }
147    }
148
149    eprintln!("{} skill(s) updated from {}.", updated, repo.display());
150    Ok(())
151}
152
153fn read_skill_description(path: &Path) -> String {
154    let content = std::fs::read_to_string(path).unwrap_or_default();
155    for line in content.lines() {
156        let trimmed = line.trim();
157        if trimmed.starts_with("description:") {
158            return trimmed
159                .trim_start_matches("description:")
160                .trim()
161                .to_string();
162        }
163    }
164    String::new()
165}