mecha10-cli 0.1.47

Mecha10 CLI tool
Documentation
//! Remote command handlers
//!
//! Orchestrates the mecha10-remote container which runs AI/ML nodes
//! in Docker containers with platform-specific dependencies.

use crate::commands::remote::{RemoteLogsArgs, RemoteUpArgs};
use crate::context::CliContext;
use crate::paths;
use crate::services::{DockerService, RemoteImageService};
use crate::types::project::load_project_config;
use anyhow::{Context, Result};

/// Handle the `remote up` command
///
/// Starts the mecha10-remote container which runs all nodes configured
/// in targets.remote. Automatically detects whether to use pre-built
/// image or build locally.
pub async fn handle_remote_up(ctx: &mut CliContext, args: &RemoteUpArgs) -> Result<()> {
    // Load project config
    let config_path = ctx.working_dir.join(paths::PROJECT_CONFIG);
    let config = load_project_config(&config_path).await.context(format!(
        "Failed to load {}. Are you in a mecha10 project directory?",
        paths::PROJECT_CONFIG
    ))?;

    // Check if remote nodes are configured
    let targets = config.targets.as_ref();
    let has_remote_nodes = targets.is_some_and(|t| t.has_remote_nodes());

    if !has_remote_nodes {
        println!();
        println!("No remote nodes configured in mecha10.json.");
        println!();
        println!("To configure remote nodes, add a targets section:");
        println!();
        println!("  \"targets\": {{");
        println!("    \"remote\": [\"@mecha10/object-detector\", \"@mecha10/image-classifier\"]");
        println!("  }}");
        println!();
        return Ok(());
    }

    let targets = targets.unwrap();
    let remote_nodes = targets.remote_nodes();

    println!();
    println!("Starting remote nodes: {}", remote_nodes.join(", "));
    println!();

    // Check if compose file exists
    let compose_path = ctx.working_dir.join(paths::docker::COMPOSE_REMOTE_FILE);
    if !compose_path.exists() {
        return Err(anyhow::anyhow!(
            "Remote compose file not found: {}\n\
             Run `mecha10 init` to create project files, or create {} manually.",
            compose_path.display(),
            paths::docker::COMPOSE_REMOTE_FILE
        ));
    }

    // Create DockerService with remote compose file
    let compose_file_path = compose_path.to_string_lossy().to_string();
    let docker = DockerService::with_compose_file(&compose_file_path);

    // Check Docker is available
    docker.check_installation()?;
    docker.check_daemon()?;

    // Set platform for docker-compose
    let platform = args.arch.docker_platform();
    std::env::set_var("MECHA10_PLATFORM", platform);
    println!("Platform: {} ({:?})", platform, args.arch);

    // Determine if we should use pre-built or build locally
    let remote_image_service = RemoteImageService::new();
    let detection = if args.build {
        println!("Forcing local build (--build flag)");
        None
    } else {
        let result = remote_image_service.detect_prebuilt(targets, &ctx.working_dir).await?;
        if result.can_use_prebuilt {
            Some(result)
        } else {
            if let Some(reason) = &result.reason {
                println!("Building locally: {}", reason);
            }
            None
        }
    };

    if let Some(prebuilt) = detection {
        // Use pre-built image
        if let Some(image_tag) = prebuilt.image_tag {
            println!("Using pre-built image: {}", image_tag);
            // Set environment variable for docker-compose
            std::env::set_var(RemoteImageService::image_env_var(), &image_tag);
        }
        // Start without building (image will be pulled) - always detached
        docker.compose_up(true).await?;
    } else {
        // Build locally - always detached
        build_and_start(&docker, true).await?;
    }

    println!();
    println!("Remote nodes started.");
    println!();
    println!("Useful commands:");
    println!("  mecha10 remote logs -f  # View logs (follow)");
    println!("  mecha10 remote ps       # List running nodes");
    println!("  mecha10 remote down     # Stop container");
    println!();

    Ok(())
}

/// Build and start the remote container
async fn build_and_start(docker: &DockerService, detach: bool) -> Result<()> {
    // Build the container
    docker.compose_build(None)?;

    // Start the container
    docker.compose_up(detach).await?;

    Ok(())
}

/// Handle the `remote down` command
///
/// Stops and removes the mecha10-remote container.
pub async fn handle_remote_down(ctx: &mut CliContext) -> Result<()> {
    let compose_path = ctx.working_dir.join(paths::docker::COMPOSE_REMOTE_FILE);
    if !compose_path.exists() {
        println!("No remote compose file found.");
        return Ok(());
    }

    let compose_file_path = compose_path.to_string_lossy().to_string();
    let docker = DockerService::with_compose_file(&compose_file_path);
    docker.check_installation()?;

    println!("Stopping remote nodes...");
    docker.compose_down().await?;

    println!();
    println!("Remote nodes stopped.");
    println!();

    Ok(())
}

/// Handle the `remote logs` command
///
/// Shows logs from the mecha10-remote container.
pub async fn handle_remote_logs(ctx: &mut CliContext, args: &RemoteLogsArgs) -> Result<()> {
    let compose_path = ctx.working_dir.join(paths::docker::COMPOSE_REMOTE_FILE);
    if !compose_path.exists() {
        return Err(anyhow::anyhow!("No remote compose file found."));
    }

    let compose_file_path = compose_path.to_string_lossy().to_string();
    let docker = DockerService::with_compose_file(&compose_file_path);
    docker.check_installation()?;

    // If specific node requested, show a note
    if let Some(node) = &args.node {
        println!("Showing logs for node: {}", node);
        println!("(Note: All nodes run in single container, filtering by log content)");
        println!();
    }

    // Use the compose_logs method
    let tail = if args.tail > 0 { Some(args.tail as usize) } else { None };

    docker.compose_logs(Some("mecha10-remote"), args.follow, tail)?;

    Ok(())
}

/// Handle the `remote ps` command
///
/// Lists running remote nodes.
pub async fn handle_remote_ps(ctx: &mut CliContext) -> Result<()> {
    let compose_path = ctx.working_dir.join(paths::docker::COMPOSE_REMOTE_FILE);
    if !compose_path.exists() {
        println!("No remote compose file found.");
        return Ok(());
    }

    let compose_file_path = compose_path.to_string_lossy().to_string();
    let docker = DockerService::with_compose_file(&compose_file_path);
    docker.check_installation()?;

    // Run docker-compose ps manually since DockerService doesn't have a ps method
    let status = std::process::Command::new("docker")
        .arg("compose")
        .arg("-f")
        .arg(&compose_file_path)
        .arg("ps")
        .status()
        .context("Failed to run docker compose ps")?;

    if !status.success() {
        return Err(anyhow::anyhow!("docker compose ps failed"));
    }

    Ok(())
}

/// Handle the `remote build` command
///
/// Forces a rebuild of the remote container.
pub async fn handle_remote_build(ctx: &mut CliContext) -> Result<()> {
    let compose_path = ctx.working_dir.join(paths::docker::COMPOSE_REMOTE_FILE);
    if !compose_path.exists() {
        return Err(anyhow::anyhow!(
            "Remote compose file not found: {}\n\
             Run `mecha10 init` to create project files.",
            compose_path.display()
        ));
    }

    let compose_file_path = compose_path.to_string_lossy().to_string();
    let docker = DockerService::with_compose_file(&compose_file_path);
    docker.check_installation()?;
    docker.check_daemon()?;

    println!("Building remote container...");
    docker.compose_build(None)?;

    println!();
    println!("Remote container built successfully.");
    println!();
    println!("Run `mecha10 remote up` to start.");
    println!();

    Ok(())
}