mecha10-cli 0.1.47

Mecha10 CLI tool
Documentation
use std::env;
use std::fs;
use std::path::Path;
use serde_json::Value;

fn main() {
    println!("cargo:rerun-if-changed=mecha10.json");
    println!("cargo:rerun-if-changed=configs");
    println!("cargo:rerun-if-changed=models");
    println!("cargo:rerun-if-changed=nodes");

    // Generate node registry from mecha10.json
    generate_node_registry();

    // Generate embedded resources
    generate_embedded();
}

/// Check if a node is a framework node (@mecha10/*)
fn is_framework_node(name: &str) -> bool {
    name.starts_with("@mecha10/")
}

/// Extract short name from framework node ("@mecha10/speaker" -> "speaker")
fn framework_node_short_name(name: &str) -> Option<&str> {
    name.strip_prefix("@mecha10/")
}

/// Map a custom node name to its module name
/// For custom nodes in ./nodes/my-node/, the module would be my_node
fn custom_node_to_module(name: &str) -> String {
    name.replace('-', "_")
}

fn generate_node_registry() {
    let manifest_str = fs::read_to_string("mecha10.json")
        .unwrap_or_else(|_| r#"{"nodes":[]}"#.to_string());
    let manifest: Value = serde_json::from_str(&manifest_str)
        .expect("Failed to parse mecha10.json");

    let mut code = String::from("// Auto-generated from mecha10.json\n");
    code.push_str("// Framework nodes are spawned via NodeResolver (pre-compiled binaries)\n");
    code.push_str("// Custom nodes are compiled into this binary\n\n");

    // Detect run target from features
    let is_robot = env::var("CARGO_FEATURE_TARGET_ROBOT").is_ok();
    let is_remote = env::var("CARGO_FEATURE_TARGET_REMOTE").is_ok();
    let is_dev = env::var("CARGO_FEATURE_TARGET_DEV").is_ok();

    let run_target = if is_dev {
        "dev"
    } else if is_robot {
        "robot"
    } else if is_remote {
        "remote"
    } else {
        "dev"
    };

    code.push_str(&format!("pub const RUN_TARGET: &str = \"{}\";\n\n", run_target));

    // Collect all node names from the flat "nodes" array
    let mut all_nodes: Vec<String> = Vec::new();

    // New format: flat array of node names
    if let Some(nodes) = manifest["nodes"].as_array() {
        for node in nodes {
            if let Some(name) = node.as_str() {
                all_nodes.push(name.to_string());
            }
        }
    }

    // Legacy format: nodes.drivers and nodes.custom arrays
    if let Some(drivers) = manifest["nodes"]["drivers"].as_array() {
        for driver in drivers {
            if driver["enabled"].as_bool().unwrap_or(false) {
                if let Some(name) = driver["name"].as_str() {
                    all_nodes.push(name.to_string());
                }
            }
        }
    }
    if let Some(custom) = manifest["nodes"]["custom"].as_array() {
        for node in custom {
            if node["enabled"].as_bool().unwrap_or(false) {
                if let Some(name) = node["name"].as_str() {
                    all_nodes.push(name.to_string());
                }
            }
        }
    }

    // Categorize nodes
    let framework_nodes: Vec<_> = all_nodes.iter()
        .filter(|n| is_framework_node(n))
        .cloned()
        .collect();
    let custom_nodes: Vec<_> = all_nodes.iter()
        .filter(|n| !is_framework_node(n))
        .cloned()
        .collect();

    // Generate ENABLED_NODES const
    code.push_str("pub const ENABLED_NODES: &[&str] = &[\n");
    for name in &all_nodes {
        code.push_str(&format!("    \"{}\",\n", name));
    }
    code.push_str("];\n\n");

    // Generate FRAMEWORK_NODES const
    code.push_str("pub const FRAMEWORK_NODES: &[&str] = &[\n");
    for name in &framework_nodes {
        code.push_str(&format!("    \"{}\",\n", name));
    }
    code.push_str("];\n\n");

    // Generate CUSTOM_NODES const
    code.push_str("pub const CUSTOM_NODES: &[&str] = &[\n");
    for name in &custom_nodes {
        code.push_str(&format!("    \"{}\",\n", name));
    }
    code.push_str("];\n\n");

    // Generate function to check if a node is a framework node
    code.push_str("pub fn is_framework_node(name: &str) -> bool {\n");
    code.push_str("    name.starts_with(\"@mecha10/\")\n");
    code.push_str("}\n\n");

    // Generate function to get framework node short name
    code.push_str("pub fn framework_short_name(name: &str) -> Option<&str> {\n");
    code.push_str("    name.strip_prefix(\"@mecha10/\")\n");
    code.push_str("}\n\n");

    // Generate custom node runner (only for custom nodes)
    if !custom_nodes.is_empty() {
        code.push_str("/// Run a custom node (compiled into this binary)\n");
        code.push_str("#[allow(unused_variables)]\n");
        code.push_str("pub async fn run_custom_node(name: &str) -> anyhow::Result<()> {\n");
        code.push_str("    match name {\n");

        for name in &custom_nodes {
            let module_name = custom_node_to_module(name);
            code.push_str(&format!(
                "        \"{}\" => {}::run().await.map_err(|e| anyhow::anyhow!(\"{{:?}}\", e)),\n",
                name, module_name
            ));
        }

        code.push_str("        _ => Err(anyhow::anyhow!(\"Unknown custom node: {}\", name)),\n");
        code.push_str("    }\n");
        code.push_str("}\n");
    } else {
        code.push_str("/// Run a custom node (none defined in this project)\n");
        code.push_str("#[allow(unused_variables)]\n");
        code.push_str("pub async fn run_custom_node(name: &str) -> anyhow::Result<()> {\n");
        code.push_str("    Err(anyhow::anyhow!(\"No custom nodes defined. Node '{}' not found.\", name))\n");
        code.push_str("}\n");
    }

    let out_dir = env::var("OUT_DIR").unwrap();
    let dest_path = Path::new(&out_dir).join("node_registry.rs");
    fs::write(&dest_path, code).expect("Failed to write node_registry.rs");
}

fn generate_embedded() {
    let mut code = String::from("// Auto-generated embedded resources\n\n");

    // Collect config files from configs/ directory
    let mut configs: Vec<String> = Vec::new();
    if Path::new("configs").exists() {
        if let Ok(entries) = fs::read_dir("configs") {
            for entry in entries.flatten() {
                if let Some(name) = entry.file_name().to_str() {
                    if name.ends_with(".json") || name.ends_with(".yaml") || name.ends_with(".toml") {
                        configs.push(name.to_string());
                    }
                }
            }
        }
    }
    configs.sort();

    // Collect model files from models/ directory
    let mut models: Vec<String> = Vec::new();
    if Path::new("models").exists() {
        if let Ok(entries) = fs::read_dir("models") {
            for entry in entries.flatten() {
                if let Some(name) = entry.file_name().to_str() {
                    if name.ends_with(".onnx") || name.ends_with(".pt") || name.ends_with(".safetensors") {
                        models.push(name.to_string());
                    }
                }
            }
        }
    }
    models.sort();

    // Generate Embedded struct
    code.push_str("pub struct Embedded;\n\n");

    code.push_str("impl Embedded {\n");

    // Generate list_configs method
    code.push_str("    pub fn list_configs() -> Vec<&'static str> {\n");
    code.push_str("        vec![\n");
    for config in &configs {
        code.push_str(&format!("            \"{}\",\n", config));
    }
    code.push_str("        ]\n");
    code.push_str("    }\n\n");

    // Generate list_models method
    code.push_str("    pub fn list_models() -> Vec<&'static str> {\n");
    code.push_str("        vec![\n");
    for model in &models {
        code.push_str(&format!("            \"{}\",\n", model));
    }
    code.push_str("        ]\n");
    code.push_str("    }\n");

    code.push_str("}\n");

    let out_dir = env::var("OUT_DIR").unwrap();
    let dest_path = Path::new(&out_dir).join("embedded.rs");
    fs::write(&dest_path, code).expect("Failed to write embedded.rs");
}