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