use std::collections::BTreeMap;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug)]
struct SkillData {
name: String,
files: Vec<(String, String)>,
}
fn main() {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR"));
let workspace_root = manifest_dir.join("../..");
let bundled_root = workspace_root.join("skills");
let optional_root = workspace_root.join("optional-skills");
let out_path = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR")).join("embedded_skills.rs");
println!("cargo:rerun-if-changed={}", bundled_root.display());
println!("cargo:rerun-if-changed={}", optional_root.display());
println!("cargo:rerun-if-env-changed=PROFILE");
let generated = if should_embed_skills() {
let bundled = collect_skills(&bundled_root);
let optional = collect_skills(&optional_root);
render_embedded_skills(&bundled, &optional)
} else {
render_empty_embedded_skills()
};
fs::write(&out_path, generated).expect("write embedded skills");
}
fn should_embed_skills() -> bool {
env::var("PROFILE").is_ok_and(|p| p == "release")
}
fn collect_skills(root: &Path) -> Vec<SkillData> {
let mut skills = Vec::new();
if !root.is_dir() {
return skills;
}
let mut stack = vec![root.to_path_buf()];
while let Some(dir) = stack.pop() {
let entries = match fs::read_dir(&dir) {
Ok(entries) => entries,
Err(_) => continue,
};
for entry in entries.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
if should_skip_dir(&path) {
continue;
}
let skill_md = path.join("SKILL.md");
if skill_md.is_file() {
let name = path
.strip_prefix(root)
.unwrap_or(&path)
.to_string_lossy()
.replace('\\', "/");
let files = collect_skill_files(&path);
skills.push(SkillData { name, files });
} else {
stack.push(path);
}
}
}
skills.sort_by(|a, b| a.name.cmp(&b.name));
skills
}
fn should_skip_dir(path: &Path) -> bool {
path.file_name()
.and_then(|name| name.to_str())
.map(|name| name == "__pycache__")
.unwrap_or(false)
}
fn should_skip_file(path: &Path) -> bool {
path.file_name()
.and_then(|name| name.to_str())
.map(|name| name.ends_with(".pyc"))
.unwrap_or(false)
}
fn collect_skill_files(root: &Path) -> Vec<(String, String)> {
let mut stack = vec![root.to_path_buf()];
let mut files = BTreeMap::new();
while let Some(dir) = stack.pop() {
let entries = match fs::read_dir(&dir) {
Ok(entries) => entries,
Err(_) => continue,
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if !should_skip_dir(&path) {
stack.push(path);
}
continue;
}
if !path.is_file() || should_skip_file(&path) {
continue;
}
let rel = path
.strip_prefix(root)
.unwrap_or(&path)
.to_string_lossy()
.replace('\\', "/");
if let Ok(content) = fs::read_to_string(&path) {
files.insert(rel, content);
}
}
}
files.into_iter().collect()
}
fn render_empty_embedded_skills() -> String {
"// @generated by build.rs; do not edit manually.\n\
// Debug builds: empty — skills loaded from repo `skills/` at runtime.\n\
const EMBEDDED_BUNDLED_SKILLS: &[EmbeddedSkill] = &[];\n\
const EMBEDDED_OPTIONAL_SKILLS: &[EmbeddedSkill] = &[];\n"
.to_string()
}
fn render_embedded_skills(bundled: &[SkillData], optional: &[SkillData]) -> String {
let mut out = String::new();
out.push_str("// @generated by build.rs; do not edit manually.\n");
out.push_str("const EMBEDDED_BUNDLED_SKILLS: &[EmbeddedSkill] = &[\n");
for skill in bundled {
render_skill(&mut out, skill);
}
out.push_str("];\n\n");
out.push_str("const EMBEDDED_OPTIONAL_SKILLS: &[EmbeddedSkill] = &[\n");
for skill in optional {
render_skill(&mut out, skill);
}
out.push_str("];\n");
out
}
fn render_skill(out: &mut String, skill: &SkillData) {
out.push_str(" EmbeddedSkill {\n");
out.push_str(&format!(" name: {:?},\n", skill.name));
out.push_str(" files: &[\n");
for (rel, content) in &skill.files {
out.push_str(" EmbeddedSkillFile {\n");
out.push_str(&format!(" relative_path: {:?},\n", rel));
out.push_str(&format!(" content: {:?},\n", content));
out.push_str(" },\n");
}
out.push_str(" ],\n");
out.push_str(" },\n");
}