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 robot-builder.Dockerfile for cross-compilation
162 ///
163 /// Creates a Dockerfile that enables building robot binaries for different
164 /// architectures (x86_64, aarch64) using Docker. Used by `mecha10 build robot --docker`.
165 ///
166 /// # Arguments
167 ///
168 /// * `path` - Project root path
169 /// * `project_name` - Name of the project
170 pub async fn create_dockerfile_robot_builder(&self, path: &Path, project_name: &str) -> Result<()> {
171 self.infra.create_dockerfile_robot_builder(path, project_name).await
172 }
173
174 /// Create package.json for npm dependencies
175 ///
176 /// Includes @mecha10/simulation-models and @mecha10/simulation-environments dependencies.
177 /// Resolution happens at runtime via MECHA10_FRAMEWORK_PATH env var:
178 /// - If set: Uses local monorepo packages
179 /// - If not set: Uses node_modules/@mecha10/* (requires npm install)
180 ///
181 /// # Arguments
182 ///
183 /// * `path` - Project root path
184 /// * `project_name` - Name of the project
185 pub async fn create_package_json(&self, path: &Path, project_name: &str) -> Result<()> {
186 self.meta.create_package_json(path, project_name).await
187 }
188
189 /// Create requirements.txt for Python dependencies
190 ///
191 /// Includes Python packages needed for AI nodes (onnx, onnxruntime).
192 /// Install with: `mecha10 setup` or `pip install -r requirements.txt`
193 ///
194 /// # Arguments
195 ///
196 /// * `path` - Project root path
197 pub async fn create_requirements_txt(&self, path: &Path) -> Result<()> {
198 self.meta.create_requirements_txt(path).await
199 }
200
201 /// Create mecha10.json configuration file
202 ///
203 /// Generates the main project configuration file with robot identity,
204 /// simulation settings, nodes, services, and Docker configuration.
205 ///
206 /// # Arguments
207 ///
208 /// * `path` - Project root directory
209 /// * `project_name` - Name of the project
210 /// * `template` - Robot template (rover, humanoid, etc.)
211 #[allow(dead_code)]
212 pub async fn create_mecha10_json(&self, path: &Path, project_name: &str, template: &Option<String>) -> Result<()> {
213 let platform = template.as_deref().unwrap_or("basic");
214 let project_id = project_name.replace('-', "_");
215
216 // Render template with handlebars
217 let config_content = MECHA10_JSON_TEMPLATE
218 .replace("{{project_name}}", project_name)
219 .replace("{{project_id}}", &project_id)
220 .replace("{{platform}}", platform);
221
222 tokio::fs::write(path.join(paths::PROJECT_CONFIG), config_content).await?;
223 Ok(())
224 }
225
226 /// Create simulation/models/model.json
227 ///
228 /// Generates the rover robot physical model configuration for Godot simulation
229 /// from embedded template.
230 ///
231 /// # Arguments
232 ///
233 /// * `path` - Project root directory
234 pub async fn create_simulation_model_json(&self, path: &Path) -> Result<()> {
235 let model_dest = path.join(paths::project::model_config("rover"));
236
237 // Ensure destination directory exists
238 if let Some(parent) = model_dest.parent() {
239 tokio::fs::create_dir_all(parent).await?;
240 }
241
242 // Write embedded model.json template
243 tokio::fs::write(&model_dest, MODEL_JSON_TEMPLATE)
244 .await
245 .context("Failed to write model.json")?;
246
247 Ok(())
248 }
249
250 /// Create simulation/environments/basic_arena/environment.json
251 ///
252 /// Generates the basic arena environment configuration for Godot simulation
253 /// from embedded template.
254 ///
255 /// # Arguments
256 ///
257 /// * `path` - Project root directory
258 pub async fn create_simulation_environment_json(&self, path: &Path) -> Result<()> {
259 let env_dest = path.join(paths::project::environment_config("basic_arena"));
260
261 // Ensure destination directory exists
262 if let Some(parent) = env_dest.parent() {
263 tokio::fs::create_dir_all(parent).await?;
264 }
265
266 // Write embedded environment.json template
267 tokio::fs::write(&env_dest, ENVIRONMENT_JSON_TEMPLATE)
268 .await
269 .context("Failed to write environment.json")?;
270
271 Ok(())
272 }
273
274 /// Create node configuration files (no-op)
275 ///
276 /// Node configs are sourced from:
277 /// 1. GitHub release templates (downloaded on demand)
278 /// 2. Framework packages/nodes/*/configs/ (merged by init_service)
279 ///
280 /// This function is kept for API compatibility but does nothing.
281 /// Use init_service.copy_all_node_configs() instead.
282 ///
283 /// # Arguments
284 ///
285 /// * `_path` - Project root directory (unused)
286 #[allow(unused_variables)]
287 pub async fn create_node_configs(&self, _path: &Path) -> Result<()> {
288 // No-op: configs come from GitHub templates or packages/nodes/
289 Ok(())
290 }
291
292 /// Create simulation config (no-op)
293 ///
294 /// Simulation configs are sourced from:
295 /// 1. GitHub release templates (downloaded on demand)
296 /// 2. Framework packages/simulation/configs/ (merged by init_service)
297 ///
298 /// This function is kept for API compatibility but does nothing.
299 /// Use init_service.copy_simulation_configs() instead.
300 ///
301 /// # Arguments
302 ///
303 /// * `_path` - Project root directory (unused)
304 #[allow(unused_variables)]
305 pub async fn create_simulation_configs(&self, _path: &Path) -> Result<()> {
306 // No-op: configs come from GitHub templates or packages/simulation/
307 Ok(())
308 }
309
310 /// Create simulation image assets from embedded templates
311 ///
312 /// Writes image assets to assets/images/ directory.
313 /// This is used in standalone mode when framework path is not available.
314 /// In framework dev mode, these are copied from the simulation package instead.
315 ///
316 /// # Arguments
317 ///
318 /// * `path` - Project root directory
319 pub async fn create_simulation_assets(&self, path: &Path) -> Result<()> {
320 // Create assets/images directory
321 let images_dest = path.join(paths::project::ASSETS_IMAGES_DIR);
322 tokio::fs::create_dir_all(&images_dest).await?;
323
324 // Write image files
325 let image_assets: &[(&str, &[u8])] = &[("aiko.jpg", AIKO_IMAGE), ("phoebe.jpg", PHOEBE_IMAGE)];
326
327 for (filename, content) in image_assets {
328 let dest_file = images_dest.join(filename);
329 tokio::fs::write(&dest_file, content)
330 .await
331 .with_context(|| format!("Failed to write assets/images/{}", filename))?;
332 }
333
334 Ok(())
335 }
336
337 /// Create behavior tree templates from embedded files
338 ///
339 /// Writes behavior tree JSON files to behaviors/ directory.
340 /// This is used in standalone mode when framework path is not available.
341 /// In framework dev mode, these are copied from behavior-runtime/seeds/ instead.
342 ///
343 /// # Arguments
344 ///
345 /// * `path` - Project root directory
346 pub async fn create_behavior_templates(&self, path: &Path) -> Result<()> {
347 let behaviors_dest = path.join(paths::project::BEHAVIORS_DIR);
348 tokio::fs::create_dir_all(&behaviors_dest).await?;
349
350 let behavior_templates: &[(&str, &str)] = &[
351 ("idle_wander.json", BEHAVIOR_IDLE_WANDER),
352 ("patrol_simple.json", BEHAVIOR_PATROL_SIMPLE),
353 ];
354
355 for (filename, content) in behavior_templates {
356 let dest_file = behaviors_dest.join(filename);
357 tokio::fs::write(&dest_file, *content)
358 .await
359 .with_context(|| format!("Failed to write behaviors/{}", filename))?;
360 }
361
362 Ok(())
363 }
364}
365
366impl Default for ProjectTemplateService {
367 fn default() -> Self {
368 Self::new()
369 }
370}