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 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 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 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}