mecha10-cli 0.1.47

Mecha10 CLI tool
Documentation
//! Project template service for generating project files
//!
//! This service coordinates template generation across specialized modules:
//! - Rust templates (Cargo.toml, main.rs, build.rs)
//! - Infrastructure templates (docker-compose.yml, .env)
//! - Meta templates (README.md, .gitignore, package.json)
//!
//! # Architecture
//!
//! Templates are organized into focused modules under `crate::templates`:
//! - **RustTemplates**: Rust-specific files
//! - **InfraTemplates**: Infrastructure files
//! - **MetaTemplates**: Project metadata files
//!
//! Template content is stored in external files at `packages/cli/templates/`
//! and embedded at compile time using `include_str!()`.

use crate::paths;
use crate::templates::{InfraTemplates, MetaTemplates, RustTemplates};
use anyhow::{Context, Result};
use std::path::Path;

// Embedded configuration templates
#[allow(dead_code)]
const MECHA10_JSON_TEMPLATE: &str = include_str!("../../templates/config/mecha10.json.template");
const MODEL_JSON_TEMPLATE: &str = include_str!("../../templates/config/model.json.template");
const ENVIRONMENT_JSON_TEMPLATE: &str = include_str!("../../templates/config/environment.json.template");

// Note: Node configs and simulation configs are NOT embedded here.
// They are sourced from:
// 1. GitHub release templates (downloaded on demand)
// 2. Framework packages/nodes/*/configs/ (merged at init time)
// This ensures the source of truth is packages/nodes, not templates.

// Embedded simulation image assets (for standalone mode)
const AIKO_IMAGE: &[u8] = include_bytes!("../../templates/assets/images/aiko.jpg");
const PHOEBE_IMAGE: &[u8] = include_bytes!("../../templates/assets/images/phoebe.jpg");

// Embedded behavior tree templates (for standalone mode)
const BEHAVIOR_IDLE_WANDER: &str = include_str!("../../templates/behaviors/idle_wander.json");
const BEHAVIOR_PATROL_SIMPLE: &str = include_str!("../../templates/behaviors/patrol_simple.json");

/// Service for generating project template files
///
/// # Examples
///
/// ```rust,ignore
/// use mecha10_cli::services::ProjectTemplateService;
///
/// let service = ProjectTemplateService::new();
///
/// // Generate all project files
/// service.create_readme(&project_path, "my-robot").await?;
/// service.create_cargo_toml(&project_path, "my-robot", false).await?;
/// ```
pub struct ProjectTemplateService {
    rust: RustTemplates,
    infra: InfraTemplates,
    meta: MetaTemplates,
}

impl ProjectTemplateService {
    /// Create a new ProjectTemplateService
    pub fn new() -> Self {
        Self {
            rust: RustTemplates::new(),
            infra: InfraTemplates::new(),
            meta: MetaTemplates::new(),
        }
    }

    /// Create README.md for the project
    pub async fn create_readme(&self, path: &Path, project_name: &str) -> Result<()> {
        self.meta.create_readme(path, project_name).await
    }

    /// Create .gitignore for the project
    pub async fn create_gitignore(&self, path: &Path) -> Result<()> {
        self.meta.create_gitignore(path).await
    }

    /// Create .cargo/config.toml for framework development
    ///
    /// This patches all mecha10-* dependencies to use local paths instead of crates.io.
    /// Requires a framework_path pointing to the mecha10-monorepo root.
    pub async fn create_cargo_config(&self, path: &Path, framework_path: &str) -> Result<()> {
        self.rust.create_cargo_config(path, framework_path).await
    }

    /// Create Cargo.toml for the project
    ///
    /// # Arguments
    ///
    /// * `path` - Project root path
    /// * `project_name` - Name of the project
    /// * `dev` - Whether this is for framework development (affects dependency resolution)
    pub async fn create_cargo_toml(&self, path: &Path, project_name: &str, dev: bool) -> Result<()> {
        self.rust.create_cargo_toml(path, project_name, dev).await
    }

    /// Create src/main.rs for the project
    pub async fn create_main_rs(&self, path: &Path, project_name: &str) -> Result<()> {
        self.rust.create_main_rs(path, project_name).await
    }

    /// Create build.rs for the project
    ///
    /// This build script generates:
    /// 1. node_registry.rs - Node dispatcher based on mecha10.json
    pub async fn create_build_rs(&self, path: &Path) -> Result<()> {
        self.rust.create_build_rs(path).await
    }

    /// Create .env.example for the project
    ///
    /// # Arguments
    ///
    /// * `path` - Project root path
    /// * `project_name` - Name of the project
    /// * `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<()> {
        self.infra.create_env_example(path, project_name, framework_path).await
    }

    /// Create rustfmt.toml for the project
    pub async fn create_rustfmt_toml(&self, path: &Path) -> Result<()> {
        self.rust.create_rustfmt_toml(path).await
    }

    /// Create docker-compose.yml for the project
    ///
    /// Generates a Docker Compose file with Redis and optional PostgreSQL services
    /// for local development and deployment.
    ///
    /// # Arguments
    ///
    /// * `path` - Project root path
    /// * `project_name` - Name of the project (used for container naming)
    pub async fn create_docker_compose(&self, path: &Path, project_name: &str) -> Result<()> {
        self.infra.create_docker_compose(path, project_name).await
    }

    /// Create remote node Docker files
    ///
    /// Creates docker-compose.remote.yml and Dockerfile.remote for running
    /// AI/ML nodes in containers. These are used when nodes are configured
    /// in targets.remote in mecha10.json.
    ///
    /// # Arguments
    ///
    /// * `path` - Project root path
    pub async fn create_remote_docker_files(&self, path: &Path) -> Result<()> {
        self.infra.create_remote_docker_files(path).await
    }

    /// Create robot-builder.Dockerfile for cross-compilation
    ///
    /// Creates a Dockerfile that enables building robot binaries for different
    /// architectures (x86_64, aarch64) using Docker. Used by `mecha10 build robot --docker`.
    ///
    /// # Arguments
    ///
    /// * `path` - Project root path
    /// * `project_name` - Name of the project
    pub async fn create_dockerfile_robot_builder(&self, path: &Path, project_name: &str) -> Result<()> {
        self.infra.create_dockerfile_robot_builder(path, project_name).await
    }

    /// Create package.json for npm dependencies
    ///
    /// Includes @mecha10/simulation-models and @mecha10/simulation-environments dependencies.
    /// Resolution happens at runtime via MECHA10_FRAMEWORK_PATH env var:
    /// - If set: Uses local monorepo packages
    /// - If not set: Uses node_modules/@mecha10/* (requires npm install)
    ///
    /// # Arguments
    ///
    /// * `path` - Project root path
    /// * `project_name` - Name of the project
    pub async fn create_package_json(&self, path: &Path, project_name: &str) -> Result<()> {
        self.meta.create_package_json(path, project_name).await
    }

    /// Create requirements.txt for Python dependencies
    ///
    /// Includes Python packages needed for AI nodes (onnx, onnxruntime).
    /// Install with: `mecha10 setup` or `pip install -r requirements.txt`
    ///
    /// # Arguments
    ///
    /// * `path` - Project root path
    pub async fn create_requirements_txt(&self, path: &Path) -> Result<()> {
        self.meta.create_requirements_txt(path).await
    }

    /// Create mecha10.json configuration file
    ///
    /// Generates the main project configuration file with robot identity,
    /// simulation settings, nodes, services, and Docker configuration.
    ///
    /// # Arguments
    ///
    /// * `path` - Project root directory
    /// * `project_name` - Name of the project
    /// * `template` - Robot template (rover, humanoid, etc.)
    #[allow(dead_code)]
    pub async fn create_mecha10_json(&self, path: &Path, project_name: &str, template: &Option<String>) -> Result<()> {
        let platform = template.as_deref().unwrap_or("basic");
        let project_id = project_name.replace('-', "_");

        // Render template with handlebars
        let config_content = MECHA10_JSON_TEMPLATE
            .replace("{{project_name}}", project_name)
            .replace("{{project_id}}", &project_id)
            .replace("{{platform}}", platform);

        tokio::fs::write(path.join(paths::PROJECT_CONFIG), config_content).await?;
        Ok(())
    }

    /// Create simulation/models/model.json
    ///
    /// Generates the rover robot physical model configuration for Godot simulation
    /// from embedded template.
    ///
    /// # Arguments
    ///
    /// * `path` - Project root directory
    pub async fn create_simulation_model_json(&self, path: &Path) -> Result<()> {
        let model_dest = path.join(paths::project::model_config("rover"));

        // Ensure destination directory exists
        if let Some(parent) = model_dest.parent() {
            tokio::fs::create_dir_all(parent).await?;
        }

        // Write embedded model.json template
        tokio::fs::write(&model_dest, MODEL_JSON_TEMPLATE)
            .await
            .context("Failed to write model.json")?;

        Ok(())
    }

    /// Create simulation/environments/basic_arena/environment.json
    ///
    /// Generates the basic arena environment configuration for Godot simulation
    /// from embedded template.
    ///
    /// # Arguments
    ///
    /// * `path` - Project root directory
    pub async fn create_simulation_environment_json(&self, path: &Path) -> Result<()> {
        let env_dest = path.join(paths::project::environment_config("basic_arena"));

        // Ensure destination directory exists
        if let Some(parent) = env_dest.parent() {
            tokio::fs::create_dir_all(parent).await?;
        }

        // Write embedded environment.json template
        tokio::fs::write(&env_dest, ENVIRONMENT_JSON_TEMPLATE)
            .await
            .context("Failed to write environment.json")?;

        Ok(())
    }

    /// Create node configuration files (no-op)
    ///
    /// Node configs are sourced from:
    /// 1. GitHub release templates (downloaded on demand)
    /// 2. Framework packages/nodes/*/configs/ (merged by init_service)
    ///
    /// This function is kept for API compatibility but does nothing.
    /// Use init_service.copy_all_node_configs() instead.
    ///
    /// # Arguments
    ///
    /// * `_path` - Project root directory (unused)
    #[allow(unused_variables)]
    pub async fn create_node_configs(&self, _path: &Path) -> Result<()> {
        // No-op: configs come from GitHub templates or packages/nodes/
        Ok(())
    }

    /// Create simulation config (no-op)
    ///
    /// Simulation configs are sourced from:
    /// 1. GitHub release templates (downloaded on demand)
    /// 2. Framework packages/simulation/configs/ (merged by init_service)
    ///
    /// This function is kept for API compatibility but does nothing.
    /// Use init_service.copy_simulation_configs() instead.
    ///
    /// # Arguments
    ///
    /// * `_path` - Project root directory (unused)
    #[allow(unused_variables)]
    pub async fn create_simulation_configs(&self, _path: &Path) -> Result<()> {
        // No-op: configs come from GitHub templates or packages/simulation/
        Ok(())
    }

    /// Create simulation image assets from embedded templates
    ///
    /// Writes image assets to assets/images/ directory.
    /// This is used in standalone mode when framework path is not available.
    /// In framework dev mode, these are copied from the simulation package instead.
    ///
    /// # Arguments
    ///
    /// * `path` - Project root directory
    pub async fn create_simulation_assets(&self, path: &Path) -> Result<()> {
        // Create assets/images directory
        let images_dest = path.join(paths::project::ASSETS_IMAGES_DIR);
        tokio::fs::create_dir_all(&images_dest).await?;

        // Write image files
        let image_assets: &[(&str, &[u8])] = &[("aiko.jpg", AIKO_IMAGE), ("phoebe.jpg", PHOEBE_IMAGE)];

        for (filename, content) in image_assets {
            let dest_file = images_dest.join(filename);
            tokio::fs::write(&dest_file, content)
                .await
                .with_context(|| format!("Failed to write assets/images/{}", filename))?;
        }

        Ok(())
    }

    /// Create behavior tree templates from embedded files
    ///
    /// Writes behavior tree JSON files to behaviors/ directory.
    /// This is used in standalone mode when framework path is not available.
    /// In framework dev mode, these are copied from behavior-runtime/seeds/ instead.
    ///
    /// # Arguments
    ///
    /// * `path` - Project root directory
    pub async fn create_behavior_templates(&self, path: &Path) -> Result<()> {
        let behaviors_dest = path.join(paths::project::BEHAVIORS_DIR);
        tokio::fs::create_dir_all(&behaviors_dest).await?;

        let behavior_templates: &[(&str, &str)] = &[
            ("idle_wander.json", BEHAVIOR_IDLE_WANDER),
            ("patrol_simple.json", BEHAVIOR_PATROL_SIMPLE),
        ];

        for (filename, content) in behavior_templates {
            let dest_file = behaviors_dest.join(filename);
            tokio::fs::write(&dest_file, *content)
                .await
                .with_context(|| format!("Failed to write behaviors/{}", filename))?;
        }

        Ok(())
    }
}

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