mecha10-cli 0.1.47

Mecha10 CLI tool
Documentation
//! Simulation runner
//!
//! Functions for launching Godot simulations.

mod validation;

#[allow(unused_imports)]
pub use validation::{validate_godot_version, validate_symlink};

use crate::paths;
use crate::sim::EnvironmentCatalog;
use anyhow::{Context, Result};
use std::path::Path;
use std::process::Command;

/// Run simulation with Godot
pub fn run_simulation(
    robot: &str,
    env: &str,
    project_path: &Path,
    headless: bool,
    godot_exe: &str,
    validate_only: bool,
) -> Result<()> {
    println!("🚀 Mecha10 Simulation Runner\n");
    println!("Robot: {}", robot);
    println!("Environment: {}", env);
    println!("Project: {}", project_path.display());
    println!("Headless: {}", headless);
    if validate_only {
        println!("Mode: Validation Only (dry-run)");
    }
    println!();

    // Step 1: Validate environment exists in catalog
    println!("🔍 Validating environment...");
    let catalog_path = std::path::PathBuf::from(paths::framework::ROBOT_TASKS_CATALOG);
    let catalog = EnvironmentCatalog::load(&catalog_path)?;

    let env_entry = catalog.environments.iter().find(|e| e.id == env).ok_or_else(|| {
        anyhow::anyhow!(
            "Environment '{}' not found in catalog.\n\
                 Run 'mecha10 sim list-envs' to see available environments.",
            env
        )
    })?;

    println!("  ✓ Found: {} ({})", env_entry.name, env_entry.id);

    // Step 2: Validate environment is implemented
    let base_path = std::path::PathBuf::from(paths::framework::ROBOT_TASKS_DIR);
    let env_path = base_path.join(&env_entry.path);

    if !env_path.exists() {
        anyhow::bail!(
            "Environment '{}' is defined in catalog but not implemented.\n\
             Expected path: {}\n\
             Run 'mecha10 sim validate-catalog' to see missing environments.",
            env,
            env_path.display()
        );
    }

    println!("  ✓ Implementation: {}", env_path.display());
    println!();

    // Step 3: Validate robot scene exists (optional warning)
    println!("🤖 Checking robot scene...");
    let robot_scene_path = project_path.join("robots").join(robot).join("robot.tscn");

    if !robot_scene_path.exists() {
        println!("  ⚠️  WARNING: Robot scene not found: {}", robot_scene_path.display());
        println!("  Generate it with: mecha10 sim generate-robot");
        println!("  Continuing anyway (environment may have default robot)...");
    } else {
        println!("  ✓ Robot scene: {}", robot_scene_path.display());
    }
    println!();

    // Step 4: Validate project path exists
    println!("📁 Validating Godot project...");
    if !project_path.exists() {
        anyhow::bail!(
            "Godot project not found: {}\n\
             Make sure you're in the monorepo root directory.",
            project_path.display()
        );
    }

    // Validate project.godot exists
    let project_file = project_path.join("project.godot");
    if !project_file.exists() {
        anyhow::bail!(
            "project.godot not found: {}\n\
             Expected path: {}",
            project_path.display(),
            project_file.display()
        );
    }

    println!("  ✓ Project: {}", project_file.display());

    // Validate required symlinks exist and are valid
    println!("\n🔗 Validating simulation symlinks...");
    validation::validate_symlink(
        project_path,
        "components",
        "../godot-components",
        "Godot components (sensors, actuators, etc.)",
    )?;
    validation::validate_symlink(
        project_path,
        "environments",
        "../../simulation/environments/robot-tasks",
        "Simulation environments",
    )?;

    // Validate robot symlink if specified
    let robot_symlink = project_path.join("robots").join(robot);
    if robot_symlink.exists() {
        if robot_symlink.is_symlink() {
            // Check if symlink target exists
            match std::fs::read_link(&robot_symlink) {
                Ok(target) => {
                    let target_path = if target.is_absolute() {
                        target.clone()
                    } else {
                        robot_symlink.parent().unwrap().join(&target)
                    };

                    if !target_path.exists() {
                        anyhow::bail!(
                            "Robot symlink is broken:\n\
                             Symlink: {}\n\
                             Points to: {} (does not exist)\n\
                             \n\
                             Fix this by recreating the symlink:\n\
                             ln -sf <robot-path> {}",
                            robot_symlink.display(),
                            target.display(),
                            robot_symlink.display()
                        );
                    }
                    println!("  ✓ Robot symlink: {} -> {}", robot, target.display());
                }
                Err(e) => {
                    anyhow::bail!(
                        "Failed to read robot symlink: {}\n\
                         Error: {}",
                        robot_symlink.display(),
                        e
                    );
                }
            }
        } else {
            println!("  ✓ Robot directory: {}", robot_symlink.display());
        }
    } else {
        println!(
            "  ⚠️  Robot path not found: {}\n\
             The environment may provide a default robot.",
            robot_symlink.display()
        );
    }

    println!();

    // Step 5: Validate Godot version
    println!("🔍 Checking Godot version...");
    validation::validate_godot_version(godot_exe)?;
    println!();

    // If validation only, stop here
    if validate_only {
        println!("✅ Validation completed successfully!");
        println!("\nAll checks passed:");
        println!("  ✓ Environment '{}' exists in catalog", env);
        println!("  ✓ Environment implementation found");
        println!("  ✓ Godot project structure validated");
        println!("  ✓ Required symlinks verified");
        println!("  ✓ Godot version check passed");
        println!("\nReady to run simulation with:");
        println!("  mecha10 sim run --robot {} --env {}", robot, env);
        return Ok(());
    }

    // Step 6: Build Godot command
    let mut cmd = Command::new(godot_exe);

    if headless {
        cmd.arg("--headless");
    }

    cmd.arg("--path");
    cmd.arg(project_path);

    // Pass custom arguments to Godot scene
    // Note: These are passed as Godot command-line arguments, not script args
    cmd.arg(format!("--env={}", env));
    cmd.arg(format!("--robot={}", robot));

    println!("🎮 Launching Godot...\n");
    println!("Command: {:?}\n", cmd);

    // Step 7: Execute Godot
    let status = cmd
        .status()
        .context("Failed to execute Godot. Is Godot installed and in PATH?")?;

    if status.success() {
        println!("\n✅ Simulation completed successfully!");
    } else {
        anyhow::bail!("Godot exited with error code: {:?}", status.code());
    }

    Ok(())
}