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 node configuration templates
30const BEHAVIOR_EXECUTOR_CONFIG: &str = include_str!("../../templates/config/nodes/behavior-executor/config.json");
31const IMAGE_CLASSIFIER_CONFIG: &str = include_str!("../../templates/config/nodes/image-classifier/config.json");
32const LLM_COMMAND_CONFIG: &str = include_str!("../../templates/config/nodes/llm-command/config.json");
33const OBJECT_DETECTOR_CONFIG: &str = include_str!("../../templates/config/nodes/object-detector/config.json");
34const SIMULATION_BRIDGE_CONFIG: &str = include_str!("../../templates/config/nodes/simulation-bridge/config.json");
35const WEBSOCKET_BRIDGE_CONFIG: &str = include_str!("../../templates/config/nodes/websocket-bridge/config.json");
36
37// Embedded simulation image assets (for standalone mode)
38const AIKO_IMAGE: &[u8] = include_bytes!("../../templates/assets/images/aiko.jpg");
39const PHOEBE_IMAGE: &[u8] = include_bytes!("../../templates/assets/images/phoebe.jpg");
40const CAT_TOWER_IMAGE: &[u8] = include_bytes!("../../templates/assets/cat_tower.jpg");
41
42// Embedded behavior tree templates (for standalone mode)
43const BEHAVIOR_BASIC_NAVIGATION: &str = include_str!("../../templates/behaviors/basic_navigation.json");
44const BEHAVIOR_IDLE_WANDER: &str = include_str!("../../templates/behaviors/idle_wander.json");
45const BEHAVIOR_OBSTACLE_AVOIDANCE: &str = include_str!("../../templates/behaviors/obstacle_avoidance.json");
46const BEHAVIOR_PATROL_SIMPLE: &str = include_str!("../../templates/behaviors/patrol_simple.json");
47
48// Embedded simulation configs (for standalone mode)
49const SIMULATION_CONFIG_DEV: &str = include_str!("../../templates/config/simulation/dev/config.json");
50const SIMULATION_CONFIG_PRODUCTION: &str = include_str!("../../templates/config/simulation/production/config.json");
51
52// Embedded simulation docker files (for standalone mode)
53const SIMULATION_DOCKERFILE: &str = include_str!("../../templates/docker/simulation/Dockerfile");
54const SIMULATION_ENTRYPOINT: &str = include_str!("../../templates/docker/simulation/entrypoint.sh");
55const SIMULATION_DOCKERIGNORE: &str = include_str!("../../templates/docker/simulation/.dockerignore");
56
57/// Service for generating project template files
58///
59/// # Examples
60///
61/// ```rust,ignore
62/// use mecha10_cli::services::ProjectTemplateService;
63///
64/// let service = ProjectTemplateService::new();
65///
66/// // Generate all project files
67/// service.create_readme(&project_path, "my-robot").await?;
68/// service.create_cargo_toml(&project_path, "my-robot", false).await?;
69/// ```
70pub struct ProjectTemplateService {
71    rust: RustTemplates,
72    infra: InfraTemplates,
73    meta: MetaTemplates,
74    embedded: EmbeddedTemplates,
75}
76
77impl ProjectTemplateService {
78    /// Create a new ProjectTemplateService
79    pub fn new() -> Self {
80        Self {
81            rust: RustTemplates::new(),
82            infra: InfraTemplates::new(),
83            meta: MetaTemplates::new(),
84            embedded: EmbeddedTemplates::new(),
85        }
86    }
87
88    /// Create README.md for the project
89    pub async fn create_readme(&self, path: &Path, project_name: &str) -> Result<()> {
90        self.meta.create_readme(path, project_name).await
91    }
92
93    /// Create .gitignore for the project
94    pub async fn create_gitignore(&self, path: &Path) -> Result<()> {
95        self.meta.create_gitignore(path).await
96    }
97
98    /// Create .cargo/config.toml for framework development
99    ///
100    /// This patches all mecha10-* dependencies to use local paths instead of crates.io.
101    /// Requires a framework_path pointing to the mecha10-monorepo root.
102    pub async fn create_cargo_config(&self, path: &Path, framework_path: &str) -> Result<()> {
103        self.rust.create_cargo_config(path, framework_path).await
104    }
105
106    /// Create Cargo.toml for the project
107    ///
108    /// # Arguments
109    ///
110    /// * `path` - Project root path
111    /// * `project_name` - Name of the project
112    /// * `dev` - Whether this is for framework development (affects dependency resolution)
113    pub async fn create_cargo_toml(&self, path: &Path, project_name: &str, dev: bool) -> Result<()> {
114        self.rust.create_cargo_toml(path, project_name, dev).await
115    }
116
117    /// Create src/main.rs for the project
118    pub async fn create_main_rs(&self, path: &Path, project_name: &str) -> Result<()> {
119        self.rust.create_main_rs(path, project_name).await
120    }
121
122    /// Create build.rs for the project
123    ///
124    /// This build script generates:
125    /// 1. node_registry.rs - Node dispatcher based on mecha10.json
126    /// 2. embedded.rs - Embedded asset accessors
127    pub async fn create_build_rs(&self, path: &Path) -> Result<()> {
128        self.rust.create_build_rs(path).await
129    }
130
131    /// Create embedded directory structure
132    pub async fn create_embedded_structure(&self, path: &Path) -> Result<()> {
133        self.embedded.create_structure(path).await
134    }
135
136    /// Create .env.example for the project
137    ///
138    /// # Arguments
139    ///
140    /// * `path` - Project root path
141    /// * `project_name` - Name of the project
142    /// * `framework_path` - Optional framework path for development mode
143    pub async fn create_env_example(
144        &self,
145        path: &Path,
146        project_name: &str,
147        framework_path: Option<String>,
148    ) -> Result<()> {
149        self.infra.create_env_example(path, project_name, framework_path).await
150    }
151
152    /// Create rustfmt.toml for the project
153    pub async fn create_rustfmt_toml(&self, path: &Path) -> Result<()> {
154        self.rust.create_rustfmt_toml(path).await
155    }
156
157    /// Create docker-compose.yml for the project
158    ///
159    /// Generates a Docker Compose file with Redis and optional PostgreSQL services
160    /// for local development and deployment.
161    ///
162    /// # Arguments
163    ///
164    /// * `path` - Project root path
165    /// * `project_name` - Name of the project (used for container naming)
166    pub async fn create_docker_compose(&self, path: &Path, project_name: &str) -> Result<()> {
167        self.infra.create_docker_compose(path, project_name).await
168    }
169
170    /// Create package.json for npm dependencies
171    ///
172    /// Includes @mecha10/simulation-models and @mecha10/simulation-environments dependencies.
173    /// Resolution happens at runtime via MECHA10_FRAMEWORK_PATH env var:
174    /// - If set: Uses local monorepo packages
175    /// - If not set: Uses node_modules/@mecha10/* (requires npm install)
176    ///
177    /// # Arguments
178    ///
179    /// * `path` - Project root path
180    /// * `project_name` - Name of the project
181    pub async fn create_package_json(&self, path: &Path, project_name: &str) -> Result<()> {
182        self.meta.create_package_json(path, project_name).await
183    }
184
185    /// Create requirements.txt for Python dependencies
186    ///
187    /// Includes Python packages needed for AI nodes (onnx, onnxruntime).
188    /// Install with: `mecha10 setup` or `pip install -r requirements.txt`
189    ///
190    /// # Arguments
191    ///
192    /// * `path` - Project root path
193    pub async fn create_requirements_txt(&self, path: &Path) -> Result<()> {
194        self.meta.create_requirements_txt(path).await
195    }
196
197    /// Create mecha10.json configuration file
198    ///
199    /// Generates the main project configuration file with robot identity,
200    /// simulation settings, nodes, services, and Docker configuration.
201    ///
202    /// # Arguments
203    ///
204    /// * `path` - Project root directory
205    /// * `project_name` - Name of the project
206    /// * `template` - Robot template (rover, humanoid, etc.)
207    pub async fn create_mecha10_json(&self, path: &Path, project_name: &str, template: &Option<String>) -> Result<()> {
208        let platform = template.as_deref().unwrap_or("basic");
209        let project_id = project_name.replace('-', "_");
210
211        // Render template with handlebars
212        let config_content = MECHA10_JSON_TEMPLATE
213            .replace("{{project_name}}", project_name)
214            .replace("{{project_id}}", &project_id)
215            .replace("{{platform}}", platform);
216
217        tokio::fs::write(path.join("mecha10.json"), config_content).await?;
218        Ok(())
219    }
220
221    /// Create simulation/models/model.json
222    ///
223    /// Generates the rover robot physical model configuration for Godot simulation
224    /// from embedded template.
225    ///
226    /// # Arguments
227    ///
228    /// * `path` - Project root directory
229    pub async fn create_simulation_model_json(&self, path: &Path) -> Result<()> {
230        let model_dest = path.join("simulation/models/rover/model.json");
231
232        // Ensure destination directory exists
233        if let Some(parent) = model_dest.parent() {
234            tokio::fs::create_dir_all(parent).await?;
235        }
236
237        // Write embedded model.json template
238        tokio::fs::write(&model_dest, MODEL_JSON_TEMPLATE)
239            .await
240            .context("Failed to write model.json")?;
241
242        Ok(())
243    }
244
245    /// Create simulation/environments/basic_arena/environment.json
246    ///
247    /// Generates the basic arena environment configuration for Godot simulation
248    /// from embedded template.
249    ///
250    /// # Arguments
251    ///
252    /// * `path` - Project root directory
253    pub async fn create_simulation_environment_json(&self, path: &Path) -> Result<()> {
254        let env_dest = path.join("simulation/environments/basic_arena/environment.json");
255
256        // Ensure destination directory exists
257        if let Some(parent) = env_dest.parent() {
258            tokio::fs::create_dir_all(parent).await?;
259        }
260
261        // Write embedded environment.json template
262        tokio::fs::write(&env_dest, ENVIRONMENT_JSON_TEMPLATE)
263            .await
264            .context("Failed to write environment.json")?;
265
266        Ok(())
267    }
268
269    /// Create node configuration files from embedded templates
270    ///
271    /// Writes node configs to configs/dev/nodes/{node}/config.json
272    /// This is used in standalone mode when framework path is not available.
273    ///
274    /// Node configs use environment variable placeholders (${CONTROL_PLANE_URL}, ${ROBOT_ID}, etc.)
275    /// that are resolved at runtime based on the mecha10.json environments configuration.
276    ///
277    /// # Arguments
278    ///
279    /// * `path` - Project root directory
280    pub async fn create_node_configs(&self, path: &Path) -> Result<()> {
281        // List of nodes with embedded configs
282        // Templates use ${CONTROL_PLANE_URL} which is resolved at runtime from mecha10.json
283        let node_configs: &[(&str, &str)] = &[
284            ("behavior-executor", BEHAVIOR_EXECUTOR_CONFIG),
285            ("image-classifier", IMAGE_CLASSIFIER_CONFIG),
286            ("llm-command", LLM_COMMAND_CONFIG),
287            ("object-detector", OBJECT_DETECTOR_CONFIG),
288            ("simulation-bridge", SIMULATION_BRIDGE_CONFIG),
289            ("websocket-bridge", WEBSOCKET_BRIDGE_CONFIG),
290        ];
291
292        for (node_name, config_content) in node_configs {
293            // Write to dev environment
294            let dev_dest = path.join("configs/dev/nodes").join(node_name).join("config.json");
295
296            if let Some(parent) = dev_dest.parent() {
297                tokio::fs::create_dir_all(parent).await?;
298            }
299
300            tokio::fs::write(&dev_dest, *config_content)
301                .await
302                .with_context(|| format!("Failed to write {}/config.json", node_name))?;
303
304            // Also write to common (shared across environments)
305            let common_dest = path.join("configs/common/nodes").join(node_name).join("config.json");
306
307            if let Some(parent) = common_dest.parent() {
308                tokio::fs::create_dir_all(parent).await?;
309            }
310
311            tokio::fs::write(&common_dest, *config_content)
312                .await
313                .with_context(|| format!("Failed to write common/{}/config.json", node_name))?;
314        }
315
316        Ok(())
317    }
318
319    /// Create simulation image assets from embedded templates
320    ///
321    /// Writes image assets to assets/images/ directory.
322    /// This is used in standalone mode when framework path is not available.
323    /// In framework dev mode, these are copied from the simulation package instead.
324    ///
325    /// # Arguments
326    ///
327    /// * `path` - Project root directory
328    pub async fn create_simulation_assets(&self, path: &Path) -> Result<()> {
329        // Create assets/images directory
330        let images_dest = path.join("assets/images");
331        tokio::fs::create_dir_all(&images_dest).await?;
332
333        // Write image files
334        let image_assets: &[(&str, &[u8])] = &[("aiko.jpg", AIKO_IMAGE), ("phoebe.jpg", PHOEBE_IMAGE)];
335
336        for (filename, content) in image_assets {
337            let dest_file = images_dest.join(filename);
338            tokio::fs::write(&dest_file, content)
339                .await
340                .with_context(|| format!("Failed to write assets/images/{}", filename))?;
341        }
342
343        // Write cat_tower.jpg to assets/ directory (not images subdirectory)
344        let assets_dest = path.join("assets");
345        tokio::fs::write(assets_dest.join("cat_tower.jpg"), CAT_TOWER_IMAGE)
346            .await
347            .context("Failed to write assets/cat_tower.jpg")?;
348
349        Ok(())
350    }
351
352    /// Create behavior tree templates from embedded files
353    ///
354    /// Writes behavior tree JSON files to behaviors/ directory.
355    /// This is used in standalone mode when framework path is not available.
356    /// In framework dev mode, these are copied from behavior-runtime/seeds/ instead.
357    ///
358    /// # Arguments
359    ///
360    /// * `path` - Project root directory
361    pub async fn create_behavior_templates(&self, path: &Path) -> Result<()> {
362        let behaviors_dest = path.join("behaviors");
363        tokio::fs::create_dir_all(&behaviors_dest).await?;
364
365        let behavior_templates: &[(&str, &str)] = &[
366            ("basic_navigation.json", BEHAVIOR_BASIC_NAVIGATION),
367            ("idle_wander.json", BEHAVIOR_IDLE_WANDER),
368            ("obstacle_avoidance.json", BEHAVIOR_OBSTACLE_AVOIDANCE),
369            ("patrol_simple.json", BEHAVIOR_PATROL_SIMPLE),
370        ];
371
372        for (filename, content) in behavior_templates {
373            let dest_file = behaviors_dest.join(filename);
374            tokio::fs::write(&dest_file, *content)
375                .await
376                .with_context(|| format!("Failed to write behaviors/{}", filename))?;
377        }
378
379        Ok(())
380    }
381
382    /// Create simulation config files from embedded templates
383    ///
384    /// Writes simulation configs to configs/{env}/simulation/config.json.
385    /// This is used in standalone mode when framework path is not available.
386    /// In framework dev mode, these are copied from packages/simulation/configs/ instead.
387    ///
388    /// # Arguments
389    ///
390    /// * `path` - Project root directory
391    pub async fn create_simulation_configs(&self, path: &Path) -> Result<()> {
392        // Write dev config
393        let dev_dest = path.join("configs/dev/simulation/config.json");
394        if let Some(parent) = dev_dest.parent() {
395            tokio::fs::create_dir_all(parent).await?;
396        }
397        tokio::fs::write(&dev_dest, SIMULATION_CONFIG_DEV)
398            .await
399            .context("Failed to write configs/dev/simulation/config.json")?;
400
401        // Write production config
402        let prod_dest = path.join("configs/production/simulation/config.json");
403        if let Some(parent) = prod_dest.parent() {
404            tokio::fs::create_dir_all(parent).await?;
405        }
406        tokio::fs::write(&prod_dest, SIMULATION_CONFIG_PRODUCTION)
407            .await
408            .context("Failed to write configs/production/simulation/config.json")?;
409
410        Ok(())
411    }
412
413    /// Create simulation Docker files from embedded templates
414    ///
415    /// Writes Dockerfile, entrypoint.sh, and .dockerignore to simulation/ directory.
416    /// This is used in standalone mode when framework path is not available.
417    /// In framework dev mode, these are copied from packages/simulation/docker/ instead.
418    ///
419    /// # Arguments
420    ///
421    /// * `path` - Project root directory
422    pub async fn create_simulation_docker_files(&self, path: &Path) -> Result<()> {
423        let simulation_dest = path.join("simulation");
424        tokio::fs::create_dir_all(&simulation_dest).await?;
425
426        // Write Dockerfile
427        tokio::fs::write(simulation_dest.join("Dockerfile"), SIMULATION_DOCKERFILE)
428            .await
429            .context("Failed to write simulation/Dockerfile")?;
430
431        // Write entrypoint.sh and make it executable
432        let entrypoint_path = simulation_dest.join("entrypoint.sh");
433        tokio::fs::write(&entrypoint_path, SIMULATION_ENTRYPOINT)
434            .await
435            .context("Failed to write simulation/entrypoint.sh")?;
436
437        #[cfg(unix)]
438        {
439            use std::os::unix::fs::PermissionsExt;
440            let mut perms = tokio::fs::metadata(&entrypoint_path).await?.permissions();
441            perms.set_mode(0o755);
442            tokio::fs::set_permissions(&entrypoint_path, perms).await?;
443        }
444
445        // Write .dockerignore
446        tokio::fs::write(simulation_dest.join(".dockerignore"), SIMULATION_DOCKERIGNORE)
447            .await
448            .context("Failed to write simulation/.dockerignore")?;
449
450        Ok(())
451    }
452}
453
454impl Default for ProjectTemplateService {
455    fn default() -> Self {
456        Self::new()
457    }
458}