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