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");
26// Templates removed - we copy directly from examples instead
27
28/// Service for generating project template files
29///
30/// # Examples
31///
32/// ```rust,ignore
33/// use mecha10_cli::services::ProjectTemplateService;
34///
35/// let service = ProjectTemplateService::new();
36///
37/// // Generate all project files
38/// service.create_readme(&project_path, "my-robot").await?;
39/// service.create_cargo_toml(&project_path, "my-robot", false).await?;
40/// ```
41pub struct ProjectTemplateService {
42    rust: RustTemplates,
43    infra: InfraTemplates,
44    meta: MetaTemplates,
45    embedded: EmbeddedTemplates,
46}
47
48impl ProjectTemplateService {
49    /// Create a new ProjectTemplateService
50    pub fn new() -> Self {
51        Self {
52            rust: RustTemplates::new(),
53            infra: InfraTemplates::new(),
54            meta: MetaTemplates::new(),
55            embedded: EmbeddedTemplates::new(),
56        }
57    }
58
59    /// Create README.md for the project
60    pub async fn create_readme(&self, path: &Path, project_name: &str) -> Result<()> {
61        self.meta.create_readme(path, project_name).await
62    }
63
64    /// Create .gitignore for the project
65    pub async fn create_gitignore(&self, path: &Path) -> Result<()> {
66        self.meta.create_gitignore(path).await
67    }
68
69    /// Create .cargo/config.toml for framework development
70    ///
71    /// This patches all mecha10-* dependencies to use local paths instead of crates.io.
72    /// Requires a framework_path pointing to the mecha10-monorepo root.
73    pub async fn create_cargo_config(&self, path: &Path, framework_path: &str) -> Result<()> {
74        self.rust.create_cargo_config(path, framework_path).await
75    }
76
77    /// Create Cargo.toml for the project
78    ///
79    /// # Arguments
80    ///
81    /// * `path` - Project root path
82    /// * `project_name` - Name of the project
83    /// * `dev` - Whether this is for framework development (affects dependency resolution)
84    pub async fn create_cargo_toml(&self, path: &Path, project_name: &str, dev: bool) -> Result<()> {
85        self.rust.create_cargo_toml(path, project_name, dev).await
86    }
87
88    /// Create src/main.rs for the project
89    pub async fn create_main_rs(&self, path: &Path, project_name: &str) -> Result<()> {
90        self.rust.create_main_rs(path, project_name).await
91    }
92
93    /// Create build.rs for the project
94    ///
95    /// This build script generates:
96    /// 1. node_registry.rs - Node dispatcher based on mecha10.json
97    /// 2. embedded.rs - Embedded asset accessors
98    pub async fn create_build_rs(&self, path: &Path) -> Result<()> {
99        self.rust.create_build_rs(path).await
100    }
101
102    /// Create embedded directory structure
103    pub async fn create_embedded_structure(&self, path: &Path) -> Result<()> {
104        self.embedded.create_structure(path).await
105    }
106
107    /// Create .env.example for the project
108    ///
109    /// # Arguments
110    ///
111    /// * `path` - Project root path
112    /// * `project_name` - Name of the project
113    /// * `framework_path` - Optional framework path for development mode
114    pub async fn create_env_example(
115        &self,
116        path: &Path,
117        project_name: &str,
118        framework_path: Option<String>,
119    ) -> Result<()> {
120        self.infra.create_env_example(path, project_name, framework_path).await
121    }
122
123    /// Create rustfmt.toml for the project
124    pub async fn create_rustfmt_toml(&self, path: &Path) -> Result<()> {
125        self.rust.create_rustfmt_toml(path).await
126    }
127
128    /// Create docker-compose.yml for the project
129    ///
130    /// Generates a Docker Compose file with Redis and optional PostgreSQL services
131    /// for local development and deployment.
132    ///
133    /// # Arguments
134    ///
135    /// * `path` - Project root path
136    /// * `project_name` - Name of the project (used for container naming)
137    pub async fn create_docker_compose(&self, path: &Path, project_name: &str) -> Result<()> {
138        self.infra.create_docker_compose(path, project_name).await
139    }
140
141    /// Create package.json for npm dependencies
142    ///
143    /// Includes @mecha10/simulation-models and @mecha10/simulation-environments dependencies.
144    /// Resolution happens at runtime via MECHA10_FRAMEWORK_PATH env var:
145    /// - If set: Uses local monorepo packages
146    /// - If not set: Uses node_modules/@mecha10/* (requires npm install)
147    ///
148    /// # Arguments
149    ///
150    /// * `path` - Project root path
151    /// * `project_name` - Name of the project
152    pub async fn create_package_json(&self, path: &Path, project_name: &str) -> Result<()> {
153        self.meta.create_package_json(path, project_name).await
154    }
155
156    /// Create requirements.txt for Python dependencies
157    ///
158    /// Includes Python packages needed for AI nodes (onnx, onnxruntime).
159    /// Install with: `mecha10 setup` or `pip install -r requirements.txt`
160    ///
161    /// # Arguments
162    ///
163    /// * `path` - Project root path
164    pub async fn create_requirements_txt(&self, path: &Path) -> Result<()> {
165        self.meta.create_requirements_txt(path).await
166    }
167
168    /// Create mecha10.json configuration file
169    ///
170    /// Generates the main project configuration file with robot identity,
171    /// simulation settings, nodes, services, and Docker configuration.
172    ///
173    /// # Arguments
174    ///
175    /// * `path` - Project root directory
176    /// * `project_name` - Name of the project
177    /// * `template` - Robot template (rover, humanoid, etc.)
178    pub async fn create_mecha10_json(&self, path: &Path, project_name: &str, template: &Option<String>) -> Result<()> {
179        let platform = template.as_deref().unwrap_or("basic");
180        let project_id = project_name.replace('-', "_");
181
182        // Render template with handlebars
183        let config_content = MECHA10_JSON_TEMPLATE
184            .replace("{{project_name}}", project_name)
185            .replace("{{project_id}}", &project_id)
186            .replace("{{platform}}", platform);
187
188        tokio::fs::write(path.join("mecha10.json"), config_content).await?;
189        Ok(())
190    }
191
192    /// Create simulation/models/model.json
193    ///
194    /// Generates the rover robot physical model configuration for Godot simulation
195    /// from embedded template.
196    ///
197    /// # Arguments
198    ///
199    /// * `path` - Project root directory
200    pub async fn create_simulation_model_json(&self, path: &Path) -> Result<()> {
201        let model_dest = path.join("simulation/models/model.json");
202
203        // Ensure destination directory exists
204        if let Some(parent) = model_dest.parent() {
205            tokio::fs::create_dir_all(parent).await?;
206        }
207
208        // Detect framework path
209        let framework_path = std::env::var("MECHA10_FRAMEWORK_PATH").unwrap_or_else(|_| {
210            // Try to detect from current directory
211            std::env::current_dir()
212                .ok()
213                .and_then(|mut d| {
214                    loop {
215                        if d.join("packages/core").exists() {
216                            return Some(d.to_string_lossy().to_string());
217                        }
218                        if !d.pop() {
219                            break;
220                        }
221                    }
222                    None
223                })
224                .unwrap_or_else(|| ".".to_string())
225        });
226
227        // Copy from rover example
228        let source_model =
229            std::path::PathBuf::from(&framework_path).join("packages/simulation/models/rover/model.json");
230
231        tokio::fs::copy(&source_model, &model_dest)
232            .await
233            .context("Failed to copy model.json from rover example")?;
234
235        Ok(())
236    }
237
238    /// Create simulation/environments/basic_arena/environment.json
239    ///
240    /// Generates the basic arena environment configuration for Godot simulation
241    /// from embedded template.
242    ///
243    /// # Arguments
244    ///
245    /// * `path` - Project root directory
246    pub async fn create_simulation_environment_json(&self, path: &Path) -> Result<()> {
247        let env_dest = path.join("simulation/environments/basic_arena/environment.json");
248
249        // Ensure destination directory exists
250        if let Some(parent) = env_dest.parent() {
251            tokio::fs::create_dir_all(parent).await?;
252        }
253
254        // Detect framework path
255        let framework_path = std::env::var("MECHA10_FRAMEWORK_PATH").unwrap_or_else(|_| {
256            // Try to detect from current directory
257            std::env::current_dir()
258                .ok()
259                .and_then(|mut d| {
260                    loop {
261                        if d.join("packages/core").exists() {
262                            return Some(d.to_string_lossy().to_string());
263                        }
264                        if !d.pop() {
265                            break;
266                        }
267                    }
268                    None
269                })
270                .unwrap_or_else(|| ".".to_string())
271        });
272
273        // Copy from basic_arena example
274        let source_env = std::path::PathBuf::from(&framework_path)
275            .join("packages/simulation/environments/basic_arena/environment.json");
276
277        tokio::fs::copy(&source_env, &env_dest)
278            .await
279            .context("Failed to copy environment.json from basic_arena example")?;
280
281        Ok(())
282    }
283}
284
285impl Default for ProjectTemplateService {
286    fn default() -> Self {
287        Self::new()
288    }
289}