use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
fn main() {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set"));
let asset_root = find_asset_root(&manifest_dir);
let out_dir = env::var("OUT_DIR").expect("OUT_DIR not set");
for dir in &["commands", "agents", "hooks", "get-shit-done"] {
println!(
"cargo:rerun-if-changed={}",
asset_root.join(dir).display()
);
}
println!(
"cargo:rerun-if-changed={}",
manifest_dir.join("build.rs").display()
);
let source_dirs = ["commands", "agents", "hooks", "get-shit-done"];
let mut entries: Vec<(String, PathBuf)> = Vec::new();
for dir in &source_dirs {
let abs_dir = asset_root.join(dir);
if !abs_dir.exists() {
panic!(
"build.rs: required source directory not found: {}",
abs_dir.display()
);
}
for entry in WalkDir::new(&abs_dir).follow_links(false) {
let entry = entry.expect("Failed to read directory entry");
if !entry.file_type().is_file() {
continue;
}
let abs_path = entry.path();
let rel_path = abs_path
.strip_prefix(&asset_root)
.expect("File not under asset root");
if !is_eligible(abs_path, rel_path) {
continue;
}
let rel_str = rel_path.to_string_lossy().replace('\\', "/");
entries.push((rel_str, abs_path.to_path_buf()));
}
}
if entries.is_empty() {
panic!("build.rs: no assets found — check source directory paths and filter rules");
}
entries.sort_by(|a, b| a.0.cmp(&b.0));
let mut code = String::from("// Generated by build.rs — do not edit\n\n");
code.push_str("#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n");
code.push_str("pub enum Category {\n");
code.push_str(" Command,\n");
code.push_str(" Workflow,\n");
code.push_str(" Agent,\n");
code.push_str(" Hook,\n");
code.push_str(" Other,\n");
code.push_str("}\n\n");
code.push_str("pub struct Asset {\n");
code.push_str(" pub path: &'static str,\n");
code.push_str(" pub content: &'static [u8],\n");
code.push_str(" pub category: Category,\n");
code.push_str("}\n\n");
code.push_str("pub static ASSETS: &[Asset] = &[\n");
for (rel_str, abs_path) in &entries {
let abs_str = abs_path.to_string_lossy().replace('\\', "/");
let category = classify(rel_str);
code.push_str(" Asset {\n");
code.push_str(&format!(" path: {:?},\n", rel_str));
code.push_str(&format!(
" content: include_bytes!({:?}),\n",
abs_str
));
code.push_str(&format!(" category: {},\n", category));
code.push_str(" },\n");
}
code.push_str("];\n");
let dest = Path::new(&out_dir).join("assets.rs");
fs::write(&dest, code).expect("Failed to write assets.rs");
}
fn find_asset_root(manifest_dir: &Path) -> PathBuf {
if manifest_dir.join("agents").exists() {
return manifest_dir.to_path_buf();
}
match manifest_dir.join("../..").canonicalize() {
Ok(root) if root.join("agents").exists() => root,
_ => panic!(
"build.rs: cannot find asset directories. Checked:\n 1. {}/agents\n 2. {}/../../agents",
manifest_dir.display(),
manifest_dir.display()
),
}
}
fn is_eligible(abs_path: &Path, rel_path: &Path) -> bool {
for component in rel_path.components() {
let s = component.as_os_str().to_string_lossy();
if s.starts_with('.') {
return false;
}
}
let rel_str = rel_path.to_string_lossy();
if rel_str.contains("node_modules/")
|| rel_str.contains("/dist/")
|| rel_str.contains("/tests/")
{
return false;
}
let file_name = abs_path.file_name().unwrap_or_default().to_string_lossy();
if file_name == "README.md" || file_name == "package.json" || file_name == "package-lock.json" {
return false;
}
matches!(
abs_path.extension().and_then(|e| e.to_str()),
Some("md") | Some("cjs") | Some("js") | Some("json")
)
}
fn classify(rel_path: &str) -> &'static str {
if rel_path.starts_with("commands/") {
"Category::Command"
} else if rel_path.starts_with("get-shit-done/workflows/") {
"Category::Workflow"
} else if rel_path.starts_with("agents/") {
"Category::Agent"
} else if rel_path.starts_with("hooks/") {
"Category::Hook"
} else {
"Category::Other"
}
}