edgecrab-tools 0.2.2

Tool registry, ToolHandler trait, and 50+ tool implementations
Documentation
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());

    let bundled = collect_skills(&bundled_root);
    let optional = collect_skills(&optional_root);
    let generated = render_embedded_skills(&bundled, &optional);

    fs::write(&out_path, generated).expect("write embedded skills");
}

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_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");
}