mecha10-cli 0.1.47

Mecha10 CLI tool
Documentation
// Generated files from build.rs
include!(concat!(env!("OUT_DIR"), "/node_registry.rs"));
include!(concat!(env!("OUT_DIR"), "/embedded.rs"));

use anyhow::Result;
use mecha10_core::prelude::*;
use mecha10_node_resolver::NodeResolver;
use serde_json::Value;
use std::collections::HashMap;
use std::fs;

#[tokio::main]
async fn main() -> Result<()> {
    init_logging();

    info!("🤖 {{project_name}} starting...");

    // Initialize node resolver for framework nodes
    let resolver = NodeResolver::new()?;

    // Parse command
    match std::env::args().nth(1).as_deref() {
        Some("run") => run_all_nodes(&resolver).await,
        Some("node") => {
            let name = std::env::args()
                .nth(2)
                .ok_or_else(|| anyhow::anyhow!("Node name required"))?;
            run_single_node(&resolver, &name).await
        }
        Some("list") => list_nodes(),
        Some("info") => show_info(&resolver),
        _ => print_help(),
    }
}

/// Load mecha10.json config from current directory
fn load_config() -> Result<Value> {
    let config_str = fs::read_to_string("mecha10.json")
        .map_err(|e| anyhow::anyhow!("Failed to load mecha10.json: {}. Make sure mecha10.json is in the working directory.", e))?;
    let config: Value = serde_json::from_str(&config_str)
        .map_err(|e| anyhow::anyhow!("Failed to parse mecha10.json: {}", e))?;
    Ok(config)
}

/// Get enabled nodes from config based on target and lifecycle mode
fn get_enabled_nodes(config: &Value) -> Vec<String> {
    // Get nodes for current target (robot/remote/dev)
    let target_nodes: Vec<String> = config["targets"][RUN_TARGET]
        .as_array()
        .map(|arr| {
            arr.iter()
                .filter_map(|v| v.as_str().map(|s| s.to_string()))
                .collect()
        })
        .unwrap_or_default();

    if target_nodes.is_empty() {
        return vec![];
    }

    // Get lifecycle mode from env or use default
    let mode = std::env::var("MECHA10_MODE").unwrap_or_else(|_| {
        config["lifecycle"]["default_mode"]
            .as_str()
            .unwrap_or("dev")
            .to_string()
    });

    // Get nodes for the lifecycle mode
    let mode_nodes: Vec<String> = config["lifecycle"]["modes"][&mode]["nodes"]
        .as_array()
        .map(|arr| {
            arr.iter()
                .filter_map(|v| v.as_str().map(|s| s.to_string()))
                .collect()
        })
        .unwrap_or_default();

    // If no mode nodes defined, use all target nodes
    if mode_nodes.is_empty() {
        return target_nodes;
    }

    // Return intersection: nodes that are in both target AND mode
    target_nodes
        .into_iter()
        .filter(|n| mode_nodes.contains(n))
        .collect()
}

/// Build environment variables to pass to nodes
fn build_node_env() -> HashMap<String, String> {
    std::env::vars().collect()
}

async fn run_all_nodes(resolver: &NodeResolver) -> Result<()> {
    let config = load_config()?;
    let enabled_nodes = get_enabled_nodes(&config);

    let mode = std::env::var("MECHA10_MODE").unwrap_or_else(|_| {
        config["lifecycle"]["default_mode"]
            .as_str()
            .unwrap_or("dev")
            .to_string()
    });

    info!("Starting nodes (target: {}, mode: {})...", RUN_TARGET, mode);

    if enabled_nodes.is_empty() {
        info!("No nodes enabled for target '{}' in mode '{}'.", RUN_TARGET, mode);
        info!("Check mecha10.json targets.{} and lifecycle.modes.{}.nodes", RUN_TARGET, mode);
        return Ok(());
    }

    // Separate framework and custom nodes
    let framework_nodes: Vec<_> = enabled_nodes
        .iter()
        .filter(|n| is_framework_node(n))
        .cloned()
        .collect();
    let custom_nodes: Vec<_> = enabled_nodes
        .iter()
        .filter(|n| !is_framework_node(n))
        .cloned()
        .collect();

    // Pre-download framework nodes (parallel download)
    if !framework_nodes.is_empty() {
        info!("📦 Ensuring {} framework nodes are available...", framework_nodes.len());
        resolver.resolve_all(&framework_nodes).await?;
        info!("✅ Framework nodes ready");
    }

    for node_name in &enabled_nodes {
        let node_type = if is_framework_node(node_name) { "framework" } else { "custom" };
        info!("▶️  Starting {} ({})", node_name, node_type);
    }

    info!("✅ {} nodes starting", enabled_nodes.len());

    let env = build_node_env();

    // Spawn framework nodes as subprocesses
    let mut framework_handles = Vec::new();
    for node_name in framework_nodes {
        let short_name = framework_short_name(&node_name).unwrap().to_string();
        let resolver = resolver.clone();
        let env = env.clone();

        framework_handles.push(tokio::spawn(async move {
            if let Err(e) = resolver.spawn_and_wait(&short_name, env).await {
                tracing::error!("Framework node {} failed: {}", node_name, e);
            }
        }));
    }

    // Run custom nodes in-process
    let custom_futures: Vec<_> = custom_nodes
        .iter()
        .map(|name| {
            let name = name.clone();
            async move {
                if let Err(e) = run_custom_node(&name).await {
                    tracing::error!("Custom node {} failed: {}", name, e);
                }
            }
        })
        .collect();

    // Wait for Ctrl+C or all nodes to complete
    tokio::select! {
        _ = tokio::signal::ctrl_c() => {
            info!("Received Ctrl+C, shutting down...");
        }
        _ = async {
            // Wait for both framework and custom nodes
            let framework_future = futures::future::join_all(framework_handles);
            let custom_future = futures::future::join_all(custom_futures);
            tokio::join!(framework_future, custom_future);
        } => {
            info!("All nodes completed");
        }
    }

    info!("Shutdown complete");
    Ok(())
}

async fn run_single_node(resolver: &NodeResolver, name: &str) -> Result<()> {
    info!("Starting node: {}", name);

    if is_framework_node(name) {
        // Framework node: spawn via resolver
        let short_name = framework_short_name(name)
            .ok_or_else(|| anyhow::anyhow!("Invalid framework node name: {}", name))?;
        let env = build_node_env();
        resolver.spawn_and_wait(short_name, env).await
    } else {
        // Custom node: run in-process
        run_custom_node(name).await
    }
}

fn list_nodes() -> Result<()> {
    let config = load_config()?;
    let enabled_nodes = get_enabled_nodes(&config);

    let mode = std::env::var("MECHA10_MODE").unwrap_or_else(|_| {
        config["lifecycle"]["default_mode"]
            .as_str()
            .unwrap_or("dev")
            .to_string()
    });

    println!("Target: {}", RUN_TARGET);
    println!("Mode: {}", mode);
    println!();
    println!("Enabled nodes:");
    if enabled_nodes.is_empty() {
        println!("  (none for this target/mode combination)");
    } else {
        for node in &enabled_nodes {
            let node_type = if is_framework_node(node) { "framework" } else { "custom" };
            println!("  - {} ({})", node, node_type);
        }
    }
    Ok(())
}

fn show_info(resolver: &NodeResolver) -> Result<()> {
    println!("{{project_name}} v0.1.0\n");

    println!("Run Target: {}", RUN_TARGET);
    println!("Node Resolver:");
    println!("  Version: {}", resolver.version());
    println!("  Target:  {}", resolver.target());
    println!("  Pre-built available: {}", resolver.is_prebuilt_available());

    match load_config() {
        Ok(config) => {
            let enabled_nodes = get_enabled_nodes(&config);
            let mode = std::env::var("MECHA10_MODE").unwrap_or_else(|_| {
                config["lifecycle"]["default_mode"]
                    .as_str()
                    .unwrap_or("dev")
                    .to_string()
            });

            println!("Mode: {}", mode);
            println!();

            let framework_count = enabled_nodes.iter().filter(|n| is_framework_node(n)).count();
            let custom_count = enabled_nodes.len() - framework_count;

            println!("Enabled Nodes ({} framework, {} custom):", framework_count, custom_count);
            if enabled_nodes.is_empty() {
                println!("  (none)");
            } else {
                for node in enabled_nodes {
                    let node_type = if is_framework_node(&node) { "framework" } else { "custom" };
                    println!("  - {} ({})", node, node_type);
                }
            }
        }
        Err(e) => {
            println!("\nWarning: {}", e);
        }
    }

    println!("\nEmbedded Configs:");
    let configs = Embedded::list_configs();
    if configs.is_empty() {
        println!("  (none)");
    } else {
        for config in configs {
            println!("  - {}", config);
        }
    }

    println!("\nEmbedded Models:");
    let models = Embedded::list_models();
    if models.is_empty() {
        println!("  (none)");
    } else {
        for model in models {
            println!("  - {}", model);
        }
    }

    Ok(())
}

fn print_help() -> Result<()> {
    eprintln!("{{project_name}} v0.1.0\n");
    eprintln!("Usage:");
    eprintln!("  {{project_name}} run              Start all enabled nodes");
    eprintln!("  {{project_name}} node <name>      Start single node");
    eprintln!("  {{project_name}} list             List enabled nodes");
    eprintln!("  {{project_name}} info             Show project info");
    eprintln!("  {{project_name}} --help           Show this help");
    eprintln!();
    eprintln!("Environment:");
    eprintln!("  MECHA10_MODE=<mode>    Override lifecycle mode (default from mecha10.json)");
    eprintln!();
    eprintln!("Note: Framework nodes (@mecha10/*) are downloaded at runtime.");
    eprintln!("      Custom nodes are compiled into this binary.");
    Ok(())
}