mecha10_cli/services/
project_template_service.rs

1//! Project template service for generating project files
2//!
3//! This service coordinates template generation across specialized modules:
4//! - Rust templates (Cargo.toml, main.rs, build.rs)
5//! - Infrastructure templates (docker-compose.yml, .env)
6//! - Meta templates (README.md, .gitignore, package.json)
7//!
8//! # Architecture
9//!
10//! Templates are organized into focused modules under `crate::templates`:
11//! - **RustTemplates**: Rust-specific files
12//! - **InfraTemplates**: Infrastructure files
13//! - **MetaTemplates**: Project metadata files
14//!
15//! Template content is stored in external files at `packages/cli/templates/`
16//! and embedded at compile time using `include_str!()`.
17
18use crate::paths;
19use crate::templates::{InfraTemplates, MetaTemplates, RustTemplates};
20use anyhow::{Context, Result};
21use std::path::Path;
22
23// Embedded configuration templates
24#[allow(dead_code)]
25const MECHA10_JSON_TEMPLATE: &str = include_str!("../../templates/config/mecha10.json.template");
26const MODEL_JSON_TEMPLATE: &str = include_str!("../../templates/config/model.json.template");
27const ENVIRONMENT_JSON_TEMPLATE: &str = include_str!("../../templates/config/environment.json.template");
28
29// Note: Node configs and simulation configs are NOT embedded here.
30// They are sourced from:
31// 1. GitHub release templates (downloaded on demand)
32// 2. Framework packages/nodes/*/configs/ (merged at init time)
33// This ensures the source of truth is packages/nodes, not templates.
34
35// Embedded simulation image assets (for standalone mode)
36const AIKO_IMAGE: &[u8] = include_bytes!("../../templates/assets/images/aiko.jpg");
37const PHOEBE_IMAGE: &[u8] = include_bytes!("../../templates/assets/images/phoebe.jpg");
38
39// Embedded behavior tree templates (for standalone mode)
40const BEHAVIOR_IDLE_WANDER: &str = include_str!("../../templates/behaviors/idle_wander.json");
41const BEHAVIOR_PATROL_SIMPLE: &str = include_str!("../../templates/behaviors/patrol_simple.json");
42
43/// Service for generating project template files
44///
45/// # Examples
46///
47/// ```rust,ignore
48/// use mecha10_cli::services::ProjectTemplateService;
49///
50/// let service = ProjectTemplateService::new();
51///
52/// // Generate all project files
53/// service.create_readme(&project_path, "my-robot").await?;
54/// service.create_cargo_toml(&project_path, "my-robot", false).await?;
55/// ```
56pub struct ProjectTemplateService {
57    rust: RustTemplates,
58    infra: InfraTemplates,
59    meta: MetaTemplates,
60}
61
62impl ProjectTemplateService {
63    /// Create a new ProjectTemplateService
64    pub fn new() -> Self {
65        Self {
66            rust: RustTemplates::new(),
67            infra: InfraTemplates::new(),
68            meta: MetaTemplates::new(),
69        }
70    }
71
72    /// Create README.md for the project
73    pub async fn create_readme(&self, path: &Path, project_name: &str) -> Result<()> {
74        self.meta.create_readme(path, project_name).await
75    }
76
77    /// Create .gitignore for the project
78    pub async fn create_gitignore(&self, path: &Path) -> Result<()> {
79        self.meta.create_gitignore(path).await
80    }
81
82    /// Create .cargo/config.toml for framework development
83    ///
84    /// This patches all mecha10-* dependencies to use local paths instead of crates.io.
85    /// Requires a framework_path pointing to the mecha10-monorepo root.
86    pub async fn create_cargo_config(&self, path: &Path, framework_path: &str) -> Result<()> {
87        self.rust.create_cargo_config(path, framework_path).await
88    }
89
90    /// Create Cargo.toml for the project
91    ///
92    /// # Arguments
93    ///
94    /// * `path` - Project root path
95    /// * `project_name` - Name of the project
96    /// * `dev` - Whether this is for framework development (affects dependency resolution)
97    pub async fn create_cargo_toml(&self, path: &Path, project_name: &str, dev: bool) -> Result<()> {
98        self.rust.create_cargo_toml(path, project_name, dev).await
99    }
100
101    /// Create src/main.rs for the project
102    pub async fn create_main_rs(&self, path: &Path, project_name: &str) -> Result<()> {
103        self.rust.create_main_rs(path, project_name).await
104    }
105
106    /// Create build.rs for the project
107    ///
108    /// This build script generates:
109    /// 1. node_registry.rs - Node dispatcher based on mecha10.json
110    pub async fn create_build_rs(&self, path: &Path) -> Result<()> {
111        self.rust.create_build_rs(path).await
112    }
113
114    /// Create .env.example for the project
115    ///
116    /// # Arguments
117    ///
118    /// * `path` - Project root path
119    /// * `project_name` - Name of the project
120    /// * `framework_path` - Optional framework path for development mode
121    pub async fn create_env_example(
122        &self,
123        path: &Path,
124        project_name: &str,
125        framework_path: Option<String>,
126    ) -> Result<()> {
127        self.infra.create_env_example(path, project_name, framework_path).await
128    }
129
130    /// Create rustfmt.toml for the project
131    pub async fn create_rustfmt_toml(&self, path: &Path) -> Result<()> {
132        self.rust.create_rustfmt_toml(path).await
133    }
134
135    /// Create docker-compose.yml for the project
136    ///
137    /// Generates a Docker Compose file with Redis and optional PostgreSQL services
138    /// for local development and deployment.
139    ///
140    /// # Arguments
141    ///
142    /// * `path` - Project root path
143    /// * `project_name` - Name of the project (used for container naming)
144    pub async fn create_docker_compose(&self, path: &Path, project_name: &str) -> Result<()> {
145        self.infra.create_docker_compose(path, project_name).await
146    }
147
148    /// Create remote node Docker files
149    ///
150    /// Creates docker-compose.remote.yml and Dockerfile.remote for running
151    /// AI/ML nodes in containers. These are used when nodes are configured
152    /// in targets.remote in mecha10.json.
153    ///
154    /// # Arguments
155    ///
156    /// * `path` - Project root path
157    pub async fn create_remote_docker_files(&self, path: &Path) -> Result<()> {
158        self.infra.create_remote_docker_files(path).await
159    }
160
161    /// Create robot-builder.Dockerfile for cross-compilation
162    ///
163    /// Creates a Dockerfile that enables building robot binaries for different
164    /// architectures (x86_64, aarch64) using Docker. Used by `mecha10 build robot --docker`.
165    ///
166    /// # Arguments
167    ///
168    /// * `path` - Project root path
169    /// * `project_name` - Name of the project
170    pub async fn create_dockerfile_robot_builder(&self, path: &Path, project_name: &str) -> Result<()> {
171        self.infra.create_dockerfile_robot_builder(path, project_name).await
172    }
173
174    /// Create package.json for npm dependencies
175    ///
176    /// Includes @mecha10/simulation-models and @mecha10/simulation-environments dependencies.
177    /// Resolution happens at runtime via MECHA10_FRAMEWORK_PATH env var:
178    /// - If set: Uses local monorepo packages
179    /// - If not set: Uses node_modules/@mecha10/* (requires npm install)
180    ///
181    /// # Arguments
182    ///
183    /// * `path` - Project root path
184    /// * `project_name` - Name of the project
185    pub async fn create_package_json(&self, path: &Path, project_name: &str) -> Result<()> {
186        self.meta.create_package_json(path, project_name).await
187    }
188
189    /// Create requirements.txt for Python dependencies
190    ///
191    /// Includes Python packages needed for AI nodes (onnx, onnxruntime).
192    /// Install with: `mecha10 setup` or `pip install -r requirements.txt`
193    ///
194    /// # Arguments
195    ///
196    /// * `path` - Project root path
197    pub async fn create_requirements_txt(&self, path: &Path) -> Result<()> {
198        self.meta.create_requirements_txt(path).await
199    }
200
201    /// Create mecha10.json configuration file
202    ///
203    /// Generates the main project configuration file with robot identity,
204    /// simulation settings, nodes, services, and Docker configuration.
205    ///
206    /// # Arguments
207    ///
208    /// * `path` - Project root directory
209    /// * `project_name` - Name of the project
210    /// * `template` - Robot template (rover, humanoid, etc.)
211    #[allow(dead_code)]
212    pub async fn create_mecha10_json(&self, path: &Path, project_name: &str, template: &Option<String>) -> Result<()> {
213        let platform = template.as_deref().unwrap_or("basic");
214        let project_id = project_name.replace('-', "_");
215
216        // Render template with handlebars
217        let config_content = MECHA10_JSON_TEMPLATE
218            .replace("{{project_name}}", project_name)
219            .replace("{{project_id}}", &project_id)
220            .replace("{{platform}}", platform);
221
222        tokio::fs::write(path.join(paths::PROJECT_CONFIG), config_content).await?;
223        Ok(())
224    }
225
226    /// Create simulation/models/model.json
227    ///
228    /// Generates the rover robot physical model configuration for Godot simulation
229    /// from embedded template.
230    ///
231    /// # Arguments
232    ///
233    /// * `path` - Project root directory
234    pub async fn create_simulation_model_json(&self, path: &Path) -> Result<()> {
235        let model_dest = path.join(paths::project::model_config("rover"));
236
237        // Ensure destination directory exists
238        if let Some(parent) = model_dest.parent() {
239            tokio::fs::create_dir_all(parent).await?;
240        }
241
242        // Write embedded model.json template
243        tokio::fs::write(&model_dest, MODEL_JSON_TEMPLATE)
244            .await
245            .context("Failed to write model.json")?;
246
247        Ok(())
248    }
249
250    /// Create simulation/environments/basic_arena/environment.json
251    ///
252    /// Generates the basic arena environment configuration for Godot simulation
253    /// from embedded template.
254    ///
255    /// # Arguments
256    ///
257    /// * `path` - Project root directory
258    pub async fn create_simulation_environment_json(&self, path: &Path) -> Result<()> {
259        let env_dest = path.join(paths::project::environment_config("basic_arena"));
260
261        // Ensure destination directory exists
262        if let Some(parent) = env_dest.parent() {
263            tokio::fs::create_dir_all(parent).await?;
264        }
265
266        // Write embedded environment.json template
267        tokio::fs::write(&env_dest, ENVIRONMENT_JSON_TEMPLATE)
268            .await
269            .context("Failed to write environment.json")?;
270
271        Ok(())
272    }
273
274    /// Create node configuration files (no-op)
275    ///
276    /// Node configs are sourced from:
277    /// 1. GitHub release templates (downloaded on demand)
278    /// 2. Framework packages/nodes/*/configs/ (merged by init_service)
279    ///
280    /// This function is kept for API compatibility but does nothing.
281    /// Use init_service.copy_all_node_configs() instead.
282    ///
283    /// # Arguments
284    ///
285    /// * `_path` - Project root directory (unused)
286    #[allow(unused_variables)]
287    pub async fn create_node_configs(&self, _path: &Path) -> Result<()> {
288        // No-op: configs come from GitHub templates or packages/nodes/
289        Ok(())
290    }
291
292    /// Create simulation config (no-op)
293    ///
294    /// Simulation configs are sourced from:
295    /// 1. GitHub release templates (downloaded on demand)
296    /// 2. Framework packages/simulation/configs/ (merged by init_service)
297    ///
298    /// This function is kept for API compatibility but does nothing.
299    /// Use init_service.copy_simulation_configs() instead.
300    ///
301    /// # Arguments
302    ///
303    /// * `_path` - Project root directory (unused)
304    #[allow(unused_variables)]
305    pub async fn create_simulation_configs(&self, _path: &Path) -> Result<()> {
306        // No-op: configs come from GitHub templates or packages/simulation/
307        Ok(())
308    }
309
310    /// Create simulation image assets from embedded templates
311    ///
312    /// Writes image assets to assets/images/ directory.
313    /// This is used in standalone mode when framework path is not available.
314    /// In framework dev mode, these are copied from the simulation package instead.
315    ///
316    /// # Arguments
317    ///
318    /// * `path` - Project root directory
319    pub async fn create_simulation_assets(&self, path: &Path) -> Result<()> {
320        // Create assets/images directory
321        let images_dest = path.join(paths::project::ASSETS_IMAGES_DIR);
322        tokio::fs::create_dir_all(&images_dest).await?;
323
324        // Write image files
325        let image_assets: &[(&str, &[u8])] = &[("aiko.jpg", AIKO_IMAGE), ("phoebe.jpg", PHOEBE_IMAGE)];
326
327        for (filename, content) in image_assets {
328            let dest_file = images_dest.join(filename);
329            tokio::fs::write(&dest_file, content)
330                .await
331                .with_context(|| format!("Failed to write assets/images/{}", filename))?;
332        }
333
334        Ok(())
335    }
336
337    /// Create behavior tree templates from embedded files
338    ///
339    /// Writes behavior tree JSON files to behaviors/ directory.
340    /// This is used in standalone mode when framework path is not available.
341    /// In framework dev mode, these are copied from behavior-runtime/seeds/ instead.
342    ///
343    /// # Arguments
344    ///
345    /// * `path` - Project root directory
346    pub async fn create_behavior_templates(&self, path: &Path) -> Result<()> {
347        let behaviors_dest = path.join(paths::project::BEHAVIORS_DIR);
348        tokio::fs::create_dir_all(&behaviors_dest).await?;
349
350        let behavior_templates: &[(&str, &str)] = &[
351            ("idle_wander.json", BEHAVIOR_IDLE_WANDER),
352            ("patrol_simple.json", BEHAVIOR_PATROL_SIMPLE),
353        ];
354
355        for (filename, content) in behavior_templates {
356            let dest_file = behaviors_dest.join(filename);
357            tokio::fs::write(&dest_file, *content)
358                .await
359                .with_context(|| format!("Failed to write behaviors/{}", filename))?;
360        }
361
362        Ok(())
363    }
364}
365
366impl Default for ProjectTemplateService {
367    fn default() -> Self {
368        Self::new()
369    }
370}