netsky 0.1.6

netsky CLI: the viable system launcher and subcommand dispatcher
Documentation
use std::env;
use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};

fn main() {
    let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").expect("manifest dir"));
    let repo_root = manifest_dir
        .ancestors()
        .nth(3)
        .expect("repo root")
        .to_path_buf();
    let source_root = if repo_root.join(".mcp.json").is_file() {
        repo_root
    } else {
        manifest_dir.join("assets/root")
    };

    let mut entries = Vec::new();

    rerun_if_changed(&source_root.join(".mcp.json"));
    rerun_if_changed(&source_root.join("AGENTS.md"));
    rerun_if_changed(&source_root.join("CLAUDE.md"));
    rerun_if_changed(&source_root.join(".agents/settings.json"));
    rerun_if_changed(&source_root.join("prompts"));
    rerun_if_changed(&source_root.join(".agents/skills"));
    rerun_if_changed(&source_root.join("scripts"));

    entries.push((
        ".mcp.json".to_string(),
        fs::read_to_string(source_root.join(".mcp.json")).expect("read .mcp.json"),
    ));
    entries.push((
        "AGENTS.md".to_string(),
        fs::read_to_string(source_root.join("AGENTS.md")).expect("read AGENTS.md"),
    ));
    entries.push((
        "CLAUDE.md".to_string(),
        fs::read_to_string(source_root.join("CLAUDE.md")).expect("read CLAUDE.md"),
    ));
    entries.push((
        ".agents/settings.json".to_string(),
        fs::read_to_string(source_root.join(".agents/settings.json"))
            .expect("read .agents/settings.json"),
    ));

    entries.extend(collect_prompts(&source_root));
    entries.extend(collect_skills(&source_root));
    entries.extend(collect_scripts(&source_root));

    entries.sort_by(|a, b| a.0.cmp(&b.0));

    let out_dir = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR"));
    let out_file = out_dir.join("embedded_files.rs");
    let mut rendered = String::new();
    rendered.push_str("pub const EMBEDDED_FILES: &[(&str, &str)] = &[\n");
    for (path, content) in entries {
        rendered.push_str("    (");
        rendered.push_str(&rust_string(&path));
        rendered.push_str(", ");
        rendered.push_str(&rust_string(&content));
        rendered.push_str("),\n");
    }
    rendered.push_str("];\n");
    fs::write(out_file, rendered).expect("write embedded registry");
}

fn collect_prompts(repo_root: &Path) -> Vec<(String, String)> {
    let dir = repo_root.join("prompts");
    let mut entries = Vec::new();
    if let Ok(read_dir) = fs::read_dir(&dir) {
        for entry in read_dir.flatten() {
            let path = entry.path();
            if path.extension() != Some(OsStr::new("md")) {
                continue;
            }
            if !path.is_file() {
                continue;
            }
            let rel = path
                .strip_prefix(repo_root)
                .expect("prompt under repo root")
                .to_string_lossy()
                .replace('\\', "/");
            entries.push((rel, fs::read_to_string(&path).expect("read prompt")));
        }
    }
    entries.sort_by(|a, b| a.0.cmp(&b.0));
    entries
}

fn collect_skills(repo_root: &Path) -> Vec<(String, String)> {
    let dir = repo_root.join(".agents/skills");
    let mut entries = Vec::new();
    walk_skills(&dir, repo_root, &mut entries);
    entries.sort_by(|a, b| a.0.cmp(&b.0));
    entries
}

fn collect_scripts(repo_root: &Path) -> Vec<(String, String)> {
    let dir = repo_root.join("scripts");
    let mut entries = Vec::new();
    let Ok(read_dir) = fs::read_dir(&dir) else {
        return entries;
    };
    for entry in read_dir.flatten() {
        let path = entry.path();
        if !path.is_file() {
            continue;
        }
        let rel = path
            .strip_prefix(repo_root)
            .expect("script under repo root")
            .to_string_lossy()
            .replace('\\', "/");
        entries.push((rel, fs::read_to_string(&path).expect("read script")));
    }
    entries.sort_by(|a, b| a.0.cmp(&b.0));
    entries
}

fn walk_skills(dir: &Path, repo_root: &Path, entries: &mut Vec<(String, String)>) {
    let Ok(read_dir) = fs::read_dir(dir) else {
        return;
    };
    for entry in read_dir.flatten() {
        let path = entry.path();
        if path.is_dir() {
            walk_skills(&path, repo_root, entries);
            continue;
        }
        if path.file_name() != Some(OsStr::new("SKILL.md")) {
            continue;
        }
        let rel = path
            .strip_prefix(repo_root)
            .expect("skill under repo root")
            .to_string_lossy()
            .replace('\\', "/");
        entries.push((rel, fs::read_to_string(&path).expect("read skill")));
    }
}

fn rerun_if_changed(path: &Path) {
    println!("cargo:rerun-if-changed={}", path.display());
}

fn rust_string(s: &str) -> String {
    let mut out = String::with_capacity(s.len() + 2);
    out.push('"');
    for ch in s.chars() {
        for esc in ch.escape_default() {
            out.push(esc);
        }
    }
    out.push('"');
    out
}