zenith-tool 0.0.7

The Zenith command-line interface (the `zenith` binary) for the design-document toolchain.
Documentation
// Build script: embed the Zenith agent skill into the binary.
//
// The skill is the canonical, hand-edited source under `assets/skill` (the
// folder tree) and `assets/plugin-commands` (slash-commands). This script walks
// those directories and generates an `include_str!`-based manifest in `OUT_DIR`,
// so the content is compiled into the binary and ships in the crates.io tarball.

use std::collections::BTreeMap;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};

fn main() {
    let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap_or_default());
    let skill_dir = manifest_dir.join("assets").join("skill");
    let cmd_dir = manifest_dir.join("assets").join("plugin-commands");

    rerun_if_changed(&skill_dir);
    rerun_if_changed(&cmd_dir);

    let skill_files = collect_files(&skill_dir);
    let cmd_files = collect_files(&cmd_dir);

    let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap_or_default());
    let dest = out_dir.join("skill_assets.rs");
    let generated = render_manifest(&skill_files, &cmd_files);
    fs::write(&dest, generated).expect("write skill_assets.rs");
}

/// Collect text files under `root`, keyed by forward-slash relative path, sorted.
fn collect_files(root: &Path) -> BTreeMap<String, PathBuf> {
    let mut out = BTreeMap::new();
    walk(root, root, &mut out);
    out
}

fn walk(root: &Path, dir: &Path, out: &mut BTreeMap<String, PathBuf>) {
    let entries = match fs::read_dir(dir) {
        Ok(e) => e,
        Err(_) => return,
    };
    for entry in entries.flatten() {
        let path = entry.path();
        if path.is_dir() {
            walk(root, &path, out);
        } else if let Ok(rel) = path.strip_prefix(root) {
            let key = rel.to_string_lossy().replace('\\', "/");
            out.insert(key, path.clone());
        }
    }
}

fn render_manifest(skill: &BTreeMap<String, PathBuf>, cmds: &BTreeMap<String, PathBuf>) -> String {
    let mut s = String::new();
    s.push_str("// @generated by build.rs — do not edit.\n");
    s.push_str("/// Skill tree files: (relative path, contents).\n");
    s.push_str("pub static SKILL_FILES: &[(&str, &str)] = &[\n");
    for (rel, path) in skill {
        s.push_str(&format!(
            "    ({:?}, include_str!({:?})),\n",
            rel,
            path.display().to_string()
        ));
    }
    s.push_str("];\n\n");
    s.push_str("/// Slash-command files: (file name, contents).\n");
    s.push_str("pub static COMMAND_FILES: &[(&str, &str)] = &[\n");
    for (rel, path) in cmds {
        s.push_str(&format!(
            "    ({:?}, include_str!({:?})),\n",
            rel,
            path.display().to_string()
        ));
    }
    s.push_str("];\n");
    s
}

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