use crate::commands::InitArgs;
use crate::context::CliContext;
use crate::init::validation;
use crate::paths;
use crate::services::CredentialsService;
use crate::types::EnvironmentsConfig;
use crate::ui::init_wizard::run_wizard_tui;
use anyhow::Result;
use std::path::{Path, PathBuf};
type InitProcessResult = (String, PathBuf, Option<String>, Vec<String>, bool);
async fn cleanup_on_error(project_path: &Path) {
if project_path.exists() {
if let Err(e) = tokio::fs::remove_dir_all(project_path).await {
eprintln!("โ ๏ธ Failed to cleanup directory: {}", e);
} else {
println!("๐งน Cleaned up incomplete project directory");
}
}
}
async fn download_templates(ctx: &mut CliContext) -> Result<String> {
let template_download = ctx.template_download();
let version = template_download.get_latest_version().await?;
template_download.ensure_templates(&version).await?;
Ok(version)
}
async fn copy_downloaded_templates(ctx: &mut CliContext, project_path: &Path, version: &str) -> Result<()> {
let template_download = ctx.template_download();
template_download.copy_template_dir(
version,
"configs/nodes/@mecha10",
&project_path.join(paths::config::NODES_MECHA10_DIR),
)?;
template_download.copy_template_file(
version,
"configs/simulation/config.json",
&project_path.join(paths::config::SIMULATION_CONFIG),
)?;
template_download.copy_template_dir(version, "behaviors", &project_path.join(paths::project::BEHAVIORS_DIR))?;
template_download.copy_template_dir(version, "assets", &project_path.join(paths::project::ASSETS_DIR))?;
Ok(())
}
async fn copy_fallback_templates(ctx: &mut CliContext, project_path: &Path) -> Result<()> {
ctx.init_service().copy_simulation_configs(project_path).await?;
ctx.project_template_service()
.create_simulation_configs(project_path)
.await?;
ctx.init_service().copy_simulation_assets(project_path).await?;
ctx.project_template_service()
.create_simulation_assets(project_path)
.await?;
ctx.init_service().copy_behavior_templates(project_path).await?;
ctx.project_template_service()
.create_behavior_templates(project_path)
.await?;
ctx.init_service().copy_all_node_configs(project_path).await?;
ctx.project_template_service().create_node_configs(project_path).await?;
Ok(())
}
async fn create_mecha10_json_from_template(
ctx: &mut CliContext,
project_path: &Path,
project_name: &str,
template: &Option<String>,
template_version: Option<&str>,
) -> Result<()> {
let platform = template.as_deref().unwrap_or("basic");
let project_id = project_name.replace('-', "_");
let template_content = if let Some(version) = template_version {
ctx.template_download()
.read_template(version, "config/mecha10.json.template")
.ok()
} else {
None
};
let config_content = if let Some(content) = template_content {
let mut config_str = content
.replace("{{project_name}}", project_name)
.replace("{{project_id}}", &project_id)
.replace("{{platform}}", platform);
if let Some(version) = template_version {
let mut config: serde_json::Value = serde_json::from_str(&config_str)?;
config["template_version"] = serde_json::json!(version);
config_str = serde_json::to_string_pretty(&config)?;
}
config_str
} else {
const EMBEDDED_TEMPLATE: &str = include_str!("../../templates/config/mecha10.json.template");
let mut config_str = EMBEDDED_TEMPLATE
.to_string()
.replace("{{project_name}}", project_name)
.replace("{{project_id}}", &project_id)
.replace("{{platform}}", platform);
if let Some(version) = template_version {
let mut config: serde_json::Value = serde_json::from_str(&config_str)?;
config["template_version"] = serde_json::json!(version);
config_str = serde_json::to_string_pretty(&config)?;
}
config_str
};
tokio::fs::write(project_path.join(paths::PROJECT_CONFIG), config_content).await?;
Ok(())
}
fn process_non_interactive_args(
args: &InitArgs,
working_dir: &Path,
catalog: &crate::services::ComponentCatalogService,
) -> Result<InitProcessResult> {
use crate::init::get_template_defaults;
let name = args.name.clone().ok_or_else(|| {
anyhow::anyhow!(
"Project name required in non-interactive mode.\n\
Usage: mecha10 init <PROJECT_NAME> [OPTIONS]\n\
Example: mecha10 init my-robot"
)
})?;
let path_buf = PathBuf::from(&name);
let (project_name, project_path) = if path_buf.is_absolute() || name.contains('/') || name.contains('\\') {
let final_name = path_buf
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(&name)
.to_string();
(final_name, path_buf)
} else {
let final_path = working_dir.join(&name);
(name, final_path)
};
let template = args.template.clone();
let nodes = if args.no_examples {
Vec::new()
} else {
let mut node_components = catalog.get_by_category("nodes");
node_components.sort_by(|a, b| a.name.cmp(&b.name));
let node_ids: Vec<String> = node_components.iter().map(|c| c.id.clone()).collect();
let defaults = get_template_defaults(&template, &node_ids);
node_ids
.iter()
.enumerate()
.filter(|(i, _)| defaults[*i])
.map(|(_, id)| id.clone())
.collect()
};
let dev = args.dev;
Ok((project_name, project_path, template, nodes, dev))
}
pub async fn handle_init(ctx: &mut CliContext, args: &InitArgs) -> Result<()> {
if args.update {
return handle_update_existing(ctx, args).await;
}
print!("\x1B[2J\x1B[1;1H");
let credentials_service = CredentialsService::new();
if !credentials_service.is_logged_in() {
println!();
println!("You must be logged in to create a project.");
println!();
println!("Run `mecha10 auth login` to authenticate.");
println!();
return Ok(());
}
println!();
println!("๐ค Initialize Mecha10 Project");
println!();
let working_dir = ctx.working_dir.clone();
let catalog = ctx.component_catalog();
let is_tty = atty::is(atty::Stream::Stdin) && atty::is(atty::Stream::Stdout);
let use_wizard = is_tty && !args.non_interactive;
let (project_name, project_path, template, nodes, dev) = if use_wizard {
let default_name = args.name.clone();
match run_wizard_tui(default_name.as_deref(), catalog) {
Ok((name, template, selected_nodes)) => {
let path_buf = std::path::PathBuf::from(&name);
let (final_name, path) = if path_buf.is_absolute() || name.contains('/') || name.contains('\\') {
let final_name = path_buf
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(&name)
.to_string();
(final_name, path_buf)
} else {
let final_path = ctx.working_dir.join(&name);
(name, final_path)
};
(final_name, path, template, selected_nodes, args.dev)
}
Err(e) => {
println!("โ ๏ธ Wizard failed: {}", e);
println!("Using command-line arguments...\n");
process_non_interactive_args(args, &working_dir, catalog)?
}
}
} else {
if !is_tty {
println!("โน๏ธ Non-interactive mode\n");
}
process_non_interactive_args(args, &working_dir, catalog)?
};
validation::check_directory_not_exists(&project_path)?;
let template_version = match download_templates(ctx).await {
Ok(version) => {
println!("๐ฆ Using templates v{}", version);
Some(version)
}
Err(e) => {
println!("โ ๏ธ Could not download templates: {}", e);
println!(" Using embedded fallback templates");
None
}
};
let result = perform_init(
ctx,
&project_path,
&project_name,
&template,
&nodes,
dev,
template_version.as_deref(),
)
.await;
if let Err(e) = result {
cleanup_on_error(&project_path).await;
return Err(e);
}
print_success_message(&project_name);
Ok(())
}
async fn handle_update_existing(ctx: &mut CliContext, args: &InitArgs) -> Result<()> {
use crate::services::ConfigService;
println!();
println!("๐ Updating existing project");
println!();
let project_path = if let Some(name) = &args.name {
let path = std::path::PathBuf::from(name);
if path.is_absolute() {
path
} else {
ctx.working_dir.join(name)
}
} else {
ctx.working_dir.clone()
};
let config_path = project_path.join(crate::paths::PROJECT_CONFIG);
if !config_path.exists() {
anyhow::bail!(
"No mecha10.json found at {}. Use 'mecha10 init' to create a new project.",
project_path.display()
);
}
let config = ConfigService::load_from(&config_path).await?;
let nodes: Vec<String> = config.nodes.0.iter().map(|s| s.to_string()).collect();
println!("๐ฆ Found {} nodes in project", nodes.len());
let init_service = ctx.init_service();
let mut synced = 0;
for node_id in &nodes {
let node_name = node_id
.strip_prefix("@mecha10/")
.or_else(|| node_id.strip_prefix("@local/"))
.unwrap_or(node_id);
let v2_dev_exists = project_path
.join("configs")
.join("dev")
.join("nodes")
.join(node_name)
.join("config.json")
.exists();
let legacy_exists = project_path
.join("configs")
.join("nodes")
.join("@mecha10")
.join(node_name)
.join("config.json")
.exists();
if !v2_dev_exists && !legacy_exists {
if let Err(e) = init_service.add_example_node(&project_path, node_name).await {
tracing::debug!("Could not add config for {}: {}", node_name, e);
} else {
println!("โ
Added config for: {}", node_name);
synced += 1;
}
}
}
if synced > 0 {
println!();
println!("โจ Synced {} node configs", synced);
} else {
println!();
println!("โ
All node configs already present");
}
Ok(())
}
async fn perform_init(
ctx: &mut CliContext,
project_path: &Path,
project_name: &str,
template: &Option<String>,
nodes: &[String],
dev: bool,
template_version: Option<&str>,
) -> Result<()> {
ctx.init_service().create_project_directories(project_path).await?;
if let Some(version) = template_version {
copy_downloaded_templates(ctx, project_path, version).await?;
} else {
copy_fallback_templates(ctx, project_path).await?;
}
create_mecha10_json_from_template(ctx, project_path, project_name, template, template_version).await?;
ctx.project_template_service()
.create_simulation_model_json(project_path)
.await?;
ctx.project_template_service()
.create_simulation_environment_json(project_path)
.await?;
println!("โ
Simulation assets");
println!("โ
Node configurations");
ctx.project_template_service()
.create_readme(project_path, project_name)
.await?;
ctx.project_template_service().create_gitignore(project_path).await?;
ctx.project_template_service()
.create_package_json(project_path, project_name)
.await?;
ctx.project_template_service()
.create_requirements_txt(project_path)
.await?;
if dev {
let framework_path = ctx.init_service().detect_framework_path()?;
ctx.project_template_service()
.create_cargo_config(project_path, &framework_path)
.await?;
println!("๐ง Framework dev mode: Created .cargo/config.toml with source patches");
}
ctx.project_template_service()
.create_cargo_toml(project_path, project_name, dev)
.await?;
ctx.project_template_service()
.create_main_rs(project_path, project_name)
.await?;
ctx.project_template_service().create_build_rs(project_path).await?;
let framework_path = if dev {
ctx.init_service().detect_framework_path().ok()
} else {
None
};
ctx.project_template_service()
.create_env_example(project_path, project_name, framework_path)
.await?;
ctx.project_template_service().create_rustfmt_toml(project_path).await?;
ctx.project_template_service()
.create_docker_compose(project_path, project_name)
.await?;
ctx.project_template_service()
.create_remote_docker_files(project_path)
.await?;
ctx.project_template_service()
.create_dockerfile_robot_builder(project_path, project_name)
.await?;
println!("โ
Project files");
if !nodes.is_empty() {
for node_name in nodes {
if let Err(e) = ctx.init_service().add_example_node(project_path, node_name).await {
println!("โ ๏ธ Failed to add {}: {}", node_name, e);
} else {
println!("โ
Node: {}", node_name);
}
}
println!();
}
Ok(())
}
fn print_success_message(project_name: &str) {
println!("โจ Project {} created successfully!", project_name);
println!();
println!("๐ Next steps:");
println!(" 1. cd {}", project_name);
println!(" 2. Set OpenAI API key in .env (for AI features):");
println!(" OPENAI_API_KEY=your-key-here");
println!(" 3. Start development:");
println!(" mecha10 dev");
println!();
println!("Useful commands:");
println!(" mecha10 topics list # View message topics");
println!(" mecha10 logs [node] # Monitor node logs");
println!(" mecha10 topology # Visualize system topology");
println!(" mecha10 diagnostics watch # Monitor diagnostics in real-time");
println!(" mecha10 models # Manage AI models");
println!();
println!("In dev mode:");
println!(" 's' โ Start simulation 'wasd' โ Teleop control");
println!(" Dashboard: {}", EnvironmentsConfig::default().dashboard_url());
println!();
}