// 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(())
}