mecha10-cli 0.1.47

Mecha10 CLI tool
Documentation
//! Run command handler
//!
//! Runs the built project binary for a specific target.

use crate::commands::{RunCommand, RunTargetArgs};
use crate::context::CliContext;
use crate::services::credentials::CredentialsService;
use anyhow::Result;
use std::process::Command;

/// Run target type
#[derive(Debug, Clone, Copy)]
pub enum RunTarget {
    /// Robot nodes (edge/on-robot)
    Robot,
    /// Remote nodes (cloud/remote server)
    Remote,
}

impl RunTarget {
    /// Get display name for this target
    fn display_name(&self) -> &'static str {
        match self {
            RunTarget::Robot => "Robot",
            RunTarget::Remote => "Remote",
        }
    }

    /// Get description for this target
    fn description(&self) -> &'static str {
        match self {
            RunTarget::Robot => "edge/on-robot nodes",
            RunTarget::Remote => "cloud/remote server nodes",
        }
    }
}

/// Handle the run command
pub async fn handle_run(ctx: &mut CliContext, command: &RunCommand) -> Result<()> {
    match command {
        RunCommand::Robot(args) => run_robot_target(ctx, args).await,
        RunCommand::Remote(args) => run_remote_target(ctx, args).await,
    }
}

/// Run the robot target (binary execution)
async fn run_robot_target(ctx: &mut CliContext, args: &RunTargetArgs) -> Result<()> {
    let target = RunTarget::Robot;

    println!();
    println!("🚀 Running Mecha10 Project - {} Target", target.display_name());
    println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
    println!();

    // Verify we're in a project
    if !ctx.is_project_initialized() {
        println!("⚠️  Not in a Mecha10 project directory");
        println!();
        return Err(anyhow::anyhow!("Not in a Mecha10 project"));
    }

    // Detect project to ensure it's valid
    let project = ctx.project()?;
    let project_name = project.name()?;
    println!("Project: {}", project_name);
    println!("Target:  {} ({})", target.display_name(), target.description());

    let build_profile = if args.release { "release" } else { "debug" };
    println!("Profile: {}", build_profile);
    println!();

    // Get Redis URL from context
    let redis_url = ctx.redis_url().to_string();
    println!("Redis:   {}", redis_url);
    println!();

    // Build binary path
    let binary_path = ctx.working_dir.join("target").join(build_profile).join(project_name);

    if !binary_path.exists() {
        println!("❌ Binary not found: {}", binary_path.display());
        println!();
        println!("Build first with:");
        println!("  mecha10 build robot {}", if args.release { "--release" } else { "" });
        return Err(anyhow::anyhow!("Binary not found"));
    }

    println!("Binary:  {}", binary_path.display());
    println!();

    // Get control plane URL and robot ID from config
    let config = ctx.load_project_config().await?;
    let control_plane_url = config.environments.control_plane_url();
    let relay_url = config.environments.relay_url();
    let robot_id = config.robot.id.clone();

    // Get API key from credentials (user must be logged in)
    let credentials_service = CredentialsService::new();
    let api_key = match credentials_service.get_api_key() {
        Ok(Some(key)) => {
            println!("Auth:    ✓ (logged in)");
            key
        }
        _ => {
            println!("Auth:    ⚠ Not logged in - run 'mecha10 auth login'");
            String::new()
        }
    };

    println!("Control: {}", control_plane_url);
    println!("Relay:   {}", relay_url);
    println!("Robot:   {}", robot_id);
    println!();

    // Run the binary with proper environment
    println!("Starting {}...", target.display_name());
    println!();

    let mut cmd = Command::new(&binary_path);
    cmd.arg("run")
        .current_dir(&ctx.working_dir)
        .env("MECHA10_REDIS_URL", &redis_url)
        .env("MECHA10_ENV", "development")
        .env("CONTROL_PLANE_URL", &control_plane_url)
        .env("WEBRTC_RELAY_URL", &relay_url)
        .env("ROBOT_ID", &robot_id)
        .env("ROBOT_API_KEY", &api_key);

    // Inherit stdio for interactive output
    cmd.stdin(std::process::Stdio::inherit())
        .stdout(std::process::Stdio::inherit())
        .stderr(std::process::Stdio::inherit());

    let status = cmd.status()?;

    println!();

    if status.success() {
        println!("{} target stopped gracefully", target.display_name());
    } else {
        println!(
            "{} target exited with error: {:?}",
            target.display_name(),
            status.code()
        );
    }

    Ok(())
}

/// Run the remote target (docker compose)
async fn run_remote_target(ctx: &mut CliContext, args: &RunTargetArgs) -> Result<()> {
    let target = RunTarget::Remote;

    println!();
    println!("🐳 Running Mecha10 Project - {} Target (Docker)", target.display_name());
    println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
    println!();

    // Verify we're in a project
    if !ctx.is_project_initialized() {
        println!("⚠️  Not in a Mecha10 project directory");
        println!();
        return Err(anyhow::anyhow!("Not in a Mecha10 project"));
    }

    // Detect project to ensure it's valid
    let project = ctx.project()?;
    let project_name = project.name()?;
    println!("Project: {}", project_name);
    println!("Target:  {} ({})", target.display_name(), target.description());
    println!();

    // Check for docker-compose.remote.yml
    let compose_file = ctx.working_dir.join("docker").join("docker-compose.remote.yml");
    if !compose_file.exists() {
        println!("❌ Docker compose file not found: {}", compose_file.display());
        println!();
        println!("Expected: docker/docker-compose.remote.yml");
        println!("Run 'mecha10 init' to create a new project with Docker support.");
        return Err(anyhow::anyhow!("docker-compose.remote.yml not found"));
    }

    println!("Compose: {}", compose_file.display());

    // Get control plane URL and robot ID from config
    let config = ctx.load_project_config().await?;
    let control_plane_url = config.environments.control_plane_url();
    let robot_id = config.robot.id.clone();

    // Get API key from credentials (user must be logged in)
    let credentials_service = CredentialsService::new();
    let api_key = match credentials_service.get_api_key() {
        Ok(Some(key)) => {
            println!("Auth:    ✓ (logged in)");
            key
        }
        _ => {
            println!("Auth:    ⚠ Not logged in - run 'mecha10 auth login'");
            String::new()
        }
    };

    println!("Control: {}", control_plane_url);
    println!("Robot:   {}", robot_id);
    println!();

    // Run docker compose
    println!("Starting Docker containers...");
    println!();

    // Build command args
    let mut compose_args = vec!["compose", "-f", "docker/docker-compose.remote.yml", "up", "--build"];

    // Add detach flag if release mode (run in background)
    if args.release {
        compose_args.push("-d");
        println!("Mode:    Detached (background)");
    } else {
        println!("Mode:    Attached (foreground, Ctrl+C to stop)");
    }
    println!();

    // Get GITHUB_TOKEN from environment for private repo builds
    let github_token = std::env::var("GITHUB_TOKEN").unwrap_or_default();
    if !github_token.is_empty() {
        println!("GitHub:  ✓ (token found)");
    } else {
        println!("GitHub:  ⚠ No GITHUB_TOKEN - will fail if repo is private");
    }

    let mut cmd = Command::new("docker");
    cmd.args(&compose_args)
        .current_dir(&ctx.working_dir)
        .env("ROBOT_ID", &robot_id)
        .env("ROBOT_API_KEY", &api_key)
        .env("CONTROL_PLANE_URL", &control_plane_url)
        .env("GITHUB_TOKEN", &github_token);

    // Inherit stdio for interactive output
    cmd.stdin(std::process::Stdio::inherit())
        .stdout(std::process::Stdio::inherit())
        .stderr(std::process::Stdio::inherit());

    let status = cmd.status()?;

    println!();

    if status.success() {
        if args.release {
            println!("✅ Remote containers started in background");
            println!();
            println!("View logs:  docker compose -f docker/docker-compose.remote.yml logs -f");
            println!("Stop:       docker compose -f docker/docker-compose.remote.yml down");
        } else {
            println!("✅ Remote containers stopped");
        }
    } else {
        println!("❌ Docker compose exited with error: {:?}", status.code());
    }

    Ok(())
}