systemprompt_cli/commands/core/plugins/generate/
skills.rs1use anyhow::Result;
7use std::path::{Path, PathBuf};
8use systemprompt_models::{ComponentFilter, ComponentSource, PluginConfig, strip_frontmatter};
9
10pub fn generate_skills(
11 plugin: &PluginConfig,
12 skills_path: &Path,
13 output_dir: &Path,
14 files_generated: &mut Vec<String>,
15) -> Result<()> {
16 let resolved_skills = resolve_skills(plugin, skills_path)?;
17
18 for (skill_id, skill_dir) in &resolved_skills {
19 let kebab_name = skill_id.replace('_', "-");
20 let output_skill_dir = output_dir.join("skills").join(&kebab_name);
21 std::fs::create_dir_all(&output_skill_dir)?;
22
23 let skill_md_content = build_skill_md(skill_id, skill_dir)?;
24 let skill_md_path = output_skill_dir.join("SKILL.md");
25 std::fs::write(&skill_md_path, skill_md_content)?;
26 files_generated.push(skill_md_path.to_string_lossy().to_string());
27 }
28
29 Ok(())
30}
31
32fn resolve_skills(plugin: &PluginConfig, skills_path: &Path) -> Result<Vec<(String, PathBuf)>> {
33 let mut resolved = Vec::new();
34
35 if plugin.skills.source == ComponentSource::Explicit {
36 for skill_id in &plugin.skills.include {
37 let skill_dir = skills_path.join(skill_id);
38 if skill_dir.exists() {
39 resolved.push((skill_id.clone(), skill_dir));
40 }
41 }
42 return Ok(resolved);
43 }
44
45 if !skills_path.exists() {
46 return Ok(resolved);
47 }
48
49 for entry in std::fs::read_dir(skills_path)? {
50 let entry = entry?;
51 let path = entry.path();
52 if !path.is_dir() {
53 continue;
54 }
55
56 let skill_id = entry.file_name().to_string_lossy().to_string();
57
58 if plugin.skills.exclude.contains(&skill_id) {
59 continue;
60 }
61
62 if plugin.skills.filter == Some(ComponentFilter::Enabled) {
63 let config_path = path.join("config.yaml");
64 if config_path.exists() {
65 let cfg_text = std::fs::read_to_string(&config_path)?;
66 let cfg: serde_yaml::Value = serde_yaml::from_str(&cfg_text)?;
67 let enabled = cfg
68 .get("enabled")
69 .and_then(serde_yaml::Value::as_bool)
70 .unwrap_or(true);
71 if !enabled {
72 continue;
73 }
74 }
75 }
76
77 resolved.push((skill_id, path));
78 }
79
80 resolved.sort_by(|a, b| a.0.cmp(&b.0));
81 Ok(resolved)
82}
83
84fn build_skill_md(skill: &str, skill_dir: &Path) -> Result<String> {
85 let index_md = skill_dir.join("index.md");
86 let skill_md_path = skill_dir.join("SKILL.md");
87
88 let config_path = skill_dir.join("config.yaml");
89 let (name, description) = if config_path.exists() {
90 let cfg_text = std::fs::read_to_string(&config_path)?;
91 let cfg: serde_yaml::Value = serde_yaml::from_str(&cfg_text)?;
92 let name = cfg
93 .get("name")
94 .and_then(|v| v.as_str())
95 .unwrap_or(skill)
96 .to_owned();
97 let desc = cfg
98 .get("description")
99 .and_then(|v| v.as_str())
100 .unwrap_or("")
101 .to_owned();
102 (name, desc)
103 } else {
104 (skill.to_owned(), String::new())
105 };
106
107 let body = if index_md.exists() {
108 let content = std::fs::read_to_string(&index_md)?;
109 strip_frontmatter(&content)
110 } else if skill_md_path.exists() {
111 let content = std::fs::read_to_string(&skill_md_path)?;
112 strip_frontmatter(&content)
113 } else {
114 format!(
115 "$(systemprompt core skills show {} --raw 2>/dev/null || echo \"Skill not available\")",
116 skill
117 )
118 };
119
120 Ok(format!(
121 "---\nname: \"{}\"\ndescription: \"{}\"\n---\n\n{}\n",
122 name,
123 description.replace('"', "\\\""),
124 body.trim()
125 ))
126}