mecha10-cli 0.1.47

Mecha10 CLI tool
Documentation
//! Infrastructure template generation
//!
//! Handles generation of infrastructure files:
//! - docker-compose.yml
//! - docker-compose.remote.yml (for remote AI nodes)
//! - Dockerfile.remote (for remote AI nodes)
//! - robot-builder.Dockerfile (for cross-compilation)
//! - .env.example

use crate::paths;
use anyhow::Result;
use std::path::Path;

/// Template files embedded at compile time
const DOCKER_COMPOSE: &str = include_str!("../../templates/docker-compose.yml.template");
const DOCKER_COMPOSE_REMOTE: &str = include_str!("../../templates/docker/docker-compose.remote.yml.template");
const DOCKERFILE_REMOTE: &str = include_str!("../../templates/docker/Dockerfile.remote.template");
const DOCKERFILE_ROBOT_BUILDER: &str = include_str!("../../templates/docker/robot-builder.Dockerfile.template");
const ENV_EXAMPLE: &str = include_str!("../../templates/.env.example.template");

/// Infrastructure template generator
pub struct InfraTemplates;

impl InfraTemplates {
    /// Create a new InfraTemplates instance
    pub fn new() -> Self {
        Self
    }

    /// Create docker/ directory if it doesn't exist
    async fn ensure_docker_dir(&self, path: &Path) -> Result<()> {
        let docker_dir = path.join(paths::docker::DIR);
        if !docker_dir.exists() {
            tokio::fs::create_dir_all(&docker_dir).await?;
        }
        Ok(())
    }

    /// Create docker-compose.yml for the project
    ///
    /// # Arguments
    ///
    /// * `path` - Project root path
    /// * `project_name` - Name of the project
    pub async fn create_docker_compose(&self, path: &Path, project_name: &str) -> Result<()> {
        self.ensure_docker_dir(path).await?;
        let content = DOCKER_COMPOSE.replace("{{project_name}}", project_name);
        tokio::fs::write(path.join(paths::docker::COMPOSE_FILE), content).await?;
        Ok(())
    }

    /// Create docker-compose.remote.yml for remote AI nodes
    ///
    /// This compose file runs the mecha10-remote container which executes
    /// AI/ML nodes (object-detector, image-classifier, llm-command) that
    /// have platform-specific binary dependencies.
    ///
    /// # Arguments
    ///
    /// * `path` - Project root path
    pub async fn create_docker_compose_remote(&self, path: &Path) -> Result<()> {
        self.ensure_docker_dir(path).await?;
        tokio::fs::write(path.join(paths::docker::COMPOSE_REMOTE_FILE), DOCKER_COMPOSE_REMOTE).await?;
        Ok(())
    }

    /// Create Dockerfile.remote for building the mecha10-remote container
    ///
    /// This Dockerfile can be customized by users to add system dependencies
    /// for their AI nodes. Changes to this file trigger a rebuild (pre-built
    /// image won't be used).
    ///
    /// # Arguments
    ///
    /// * `path` - Project root path
    pub async fn create_dockerfile_remote(&self, path: &Path) -> Result<()> {
        self.ensure_docker_dir(path).await?;
        tokio::fs::write(path.join(paths::docker::DOCKERFILE_REMOTE), DOCKERFILE_REMOTE).await?;
        Ok(())
    }

    /// Create robot-builder.Dockerfile for cross-compilation
    ///
    /// This Dockerfile enables building robot binaries for different architectures
    /// (x86_64, aarch64) using Docker. Users can customize it to add system
    /// dependencies for their robot nodes.
    ///
    /// # Arguments
    ///
    /// * `path` - Project root path
    /// * `project_name` - Name of the project (used to name the binary)
    pub async fn create_dockerfile_robot_builder(&self, path: &Path, project_name: &str) -> Result<()> {
        self.ensure_docker_dir(path).await?;
        let content = DOCKERFILE_ROBOT_BUILDER.replace("{{project_name}}", project_name);
        tokio::fs::write(path.join(paths::docker::DOCKERFILE_ROBOT_BUILDER), content).await?;
        Ok(())
    }

    /// Create all remote node infrastructure files
    ///
    /// Creates both docker-compose.remote.yml and Dockerfile.remote.
    /// Called during `mecha10 init` to set up remote node support.
    ///
    /// # Arguments
    ///
    /// * `path` - Project root path
    pub async fn create_remote_docker_files(&self, path: &Path) -> Result<()> {
        self.ensure_docker_dir(path).await?;
        self.create_docker_compose_remote(path).await?;
        self.create_dockerfile_remote(path).await?;
        Ok(())
    }

    /// Create .env.example for the project
    ///
    /// # Arguments
    ///
    /// * `path` - Project root path
    /// * `project_name` - Name of the project (used for ROBOT_ID)
    /// * `framework_path` - Optional framework path for development mode
    pub async fn create_env_example(
        &self,
        path: &Path,
        project_name: &str,
        framework_path: Option<String>,
    ) -> Result<()> {
        let robot_id = project_name.replace('-', "_");

        // If framework path is provided, uncomment and set it
        let framework_line = if let Some(fw_path) = framework_path {
            format!("MECHA10_FRAMEWORK_PATH={}", fw_path)
        } else {
            "# MECHA10_FRAMEWORK_PATH=/path/to/mecha10-monorepo".to_string()
        };

        let content = ENV_EXAMPLE
            .replace("{{robot_id}}", &robot_id)
            .replace("# MECHA10_FRAMEWORK_PATH=/path/to/mecha10-monorepo", &framework_line);

        // Write .env.example
        tokio::fs::write(path.join(paths::env::EXAMPLE), &content).await?;

        // Also create .env from .env.example (users need this for the CLI to load variables)
        tokio::fs::write(path.join(paths::env::FILE), &content).await?;

        Ok(())
    }
}

impl Default for InfraTemplates {
    fn default() -> Self {
        Self::new()
    }
}