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