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