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