mecha10_cli/services/
init_service.rs1use anyhow::Result;
9use std::path::Path;
10
11pub struct InitService;
27
28impl InitService {
29 pub fn new() -> Self {
31 Self
32 }
33
34 pub async fn create_project_directories(&self, path: &Path) -> Result<()> {
49 let dirs = vec![
50 "nodes",
51 "drivers",
52 "types",
53 "behaviors",
54 "logs",
55 "simulation",
56 "simulation/models",
57 "embedded/models",
58 "assets",
59 "assets/images",
60 "configs/dev/nodes",
62 "configs/dev/simulation",
63 "configs/production/nodes",
64 "configs/production/simulation",
65 ];
66
67 for dir in dirs {
68 let dir_path = path.join(dir);
69 tokio::fs::create_dir_all(&dir_path).await?;
70
71 tokio::fs::write(dir_path.join(".gitkeep"), "").await?;
73 }
74
75 Ok(())
76 }
77
78 pub async fn add_example_node(&self, project_root: &Path, component_name: &str) -> Result<()> {
91 use crate::component_catalog::ComponentCatalog;
92
93 let catalog = ComponentCatalog::new();
95 let components = catalog.search(component_name);
96
97 if components.is_empty() {
98 return Err(anyhow::anyhow!("Component '{}' not found in catalog", component_name));
99 }
100
101 let component = &components[0];
102
103 for file_template in &component.files {
106 let file_path = project_root.join(&file_template.path);
107
108 if let Some(parent) = file_path.parent() {
110 tokio::fs::create_dir_all(parent).await?;
111 }
112
113 tokio::fs::write(&file_path, &file_template.content).await?;
115 }
116
117 if !component.files.is_empty() {
120 self.add_workspace_member(project_root, component_name).await?;
121 }
122
123 let is_monorepo_node = component.files.is_empty();
125 if is_monorepo_node {
126 self.add_dependency(project_root, component).await?;
127 }
128
129 self.add_node_to_config(project_root, component_name, component, is_monorepo_node)
131 .await?;
132
133 self.copy_node_configs(project_root, component_name).await?;
135
136 Ok(())
137 }
138
139 async fn add_workspace_member(&self, project_root: &Path, component_name: &str) -> Result<()> {
141 let cargo_toml_path = project_root.join("Cargo.toml");
142 let content = tokio::fs::read_to_string(&cargo_toml_path).await?;
143
144 let new_member = format!("nodes/{}", component_name);
145 let updated_content = if content.contains("members = []") {
146 content.replace("members = []", &format!("members = [\n \"{}\",\n]", new_member))
148 } else if let Some(start) = content.find("members = [") {
149 if let Some(end) = content[start..].find(']') {
151 let insert_pos = start + end;
152 let members_section = &content[start..insert_pos];
154 if members_section.contains('\"') {
155 format!(
157 "{}\n \"{}\",{}",
158 &content[..insert_pos],
159 new_member,
160 &content[insert_pos..]
161 )
162 } else {
163 format!(
165 "{}\n \"{}\",\n{}",
166 &content[..insert_pos],
167 new_member,
168 &content[insert_pos..]
169 )
170 }
171 } else {
172 content
173 }
174 } else {
175 content
176 };
177
178 tokio::fs::write(&cargo_toml_path, updated_content).await?;
179 Ok(())
180 }
181
182 async fn add_dependency(&self, project_root: &Path, component: &crate::component_catalog::Component) -> Result<()> {
184 let cargo_toml_path = project_root.join("Cargo.toml");
185 let content = tokio::fs::read_to_string(&cargo_toml_path).await?;
186
187 let package_name = if let Some(dep) = component.cargo_dependencies.first() {
189 dep.name.clone()
190 } else {
191 format!("mecha10-nodes-{}", component.id)
193 };
194
195 if !content.contains(&package_name) {
197 let updated_content = if let Some(pos) = content.find("mecha10-core = \"0.1\"") {
198 if let Some(newline_pos) = content[pos..].find('\n') {
200 let insert_pos = pos + newline_pos + 1;
201 format!(
202 "{}{} = \"0.1\"\n{}",
203 &content[..insert_pos],
204 package_name,
205 &content[insert_pos..]
206 )
207 } else {
208 content
209 }
210 } else {
211 content
212 };
213
214 tokio::fs::write(&cargo_toml_path, updated_content).await?;
215 }
216
217 Ok(())
218 }
219
220 async fn add_node_to_config(
222 &self,
223 project_root: &Path,
224 component_name: &str,
225 component: &crate::component_catalog::Component,
226 is_monorepo_node: bool,
227 ) -> Result<()> {
228 use crate::services::ConfigService;
229 use crate::types::NodeEntry;
230
231 let config_path = project_root.join("mecha10.json");
232 let mut config = ConfigService::load_from(&config_path).await?;
233
234 let node_path = if is_monorepo_node {
236 component
238 .mecha10_config
239 .custom_node
240 .as_ref()
241 .map(|n| n.path.clone())
242 .unwrap_or_else(|| format!("mecha10-nodes-{}", component_name))
243 } else {
244 format!("nodes/{}", component_name)
246 };
247
248 let node_entry = NodeEntry {
249 name: component_name.to_string(),
250 path: node_path,
251 config: None,
252 description: Some(component.description.clone()),
253 run_target: None,
254 enabled: true,
255 };
256 config.nodes.custom.push(node_entry);
257
258 let config_json = serde_json::to_string_pretty(&config)?;
260 tokio::fs::write(&config_path, config_json).await?;
261
262 Ok(())
263 }
264
265 pub async fn copy_behavior_templates(&self, project_root: &Path) -> Result<()> {
274 let framework_path = self.detect_framework_path()?;
276 let source_templates_path = std::path::PathBuf::from(&framework_path)
277 .join("packages")
278 .join("behavior-runtime")
279 .join("seeds");
280
281 if !source_templates_path.exists() {
283 return Ok(());
285 }
286
287 let dest_templates_path = project_root.join("behaviors");
289 tokio::fs::create_dir_all(&dest_templates_path).await?;
290
291 let mut entries = tokio::fs::read_dir(&source_templates_path).await?;
293 while let Some(entry) = entries.next_entry().await? {
294 let path = entry.path();
295 if path.extension().and_then(|s| s.to_str()) == Some("json") {
296 if let Some(filename) = path.file_name() {
297 let dest_file = dest_templates_path.join(filename);
298 tokio::fs::copy(&path, &dest_file).await?;
299 }
300 }
301 }
302
303 Ok(())
304 }
305
306 pub async fn copy_simulation_configs(&self, project_root: &Path) -> Result<()> {
315 let framework_path = self.detect_framework_path()?;
317 let source_configs_path = std::path::PathBuf::from(&framework_path)
318 .join("packages")
319 .join("simulation")
320 .join("configs");
321
322 if !source_configs_path.exists() {
324 return Ok(());
326 }
327
328 let dev_src = source_configs_path.join("dev/config.json");
330 if dev_src.exists() {
331 let dev_dest = project_root.join("configs/dev/simulation/config.json");
332 if let Some(parent) = dev_dest.parent() {
333 tokio::fs::create_dir_all(parent).await?;
334 }
335 tokio::fs::copy(&dev_src, &dev_dest).await?;
336 }
337
338 let prod_src = source_configs_path.join("production/config.json");
340 if prod_src.exists() {
341 let prod_dest = project_root.join("configs/production/simulation/config.json");
342 if let Some(parent) = prod_dest.parent() {
343 tokio::fs::create_dir_all(parent).await?;
344 }
345 tokio::fs::copy(&prod_src, &prod_dest).await?;
346 }
347
348 Ok(())
349 }
350
351 pub async fn copy_simulation_docker_files(&self, project_root: &Path) -> Result<()> {
360 let framework_path = self.detect_framework_path()?;
362 let source_docker_path = std::path::PathBuf::from(&framework_path)
363 .join("packages")
364 .join("simulation")
365 .join("docker");
366
367 if !source_docker_path.exists() {
369 return Ok(());
371 }
372
373 let dest_dir = project_root.join("simulation");
374 tokio::fs::create_dir_all(&dest_dir).await?;
375
376 let dockerfile_src = source_docker_path.join("Dockerfile");
378 if dockerfile_src.exists() {
379 tokio::fs::copy(&dockerfile_src, dest_dir.join("Dockerfile")).await?;
380 }
381
382 let entrypoint_src = source_docker_path.join("entrypoint.sh");
384 if entrypoint_src.exists() {
385 tokio::fs::copy(&entrypoint_src, dest_dir.join("entrypoint.sh")).await?;
386
387 #[cfg(unix)]
389 {
390 use std::os::unix::fs::PermissionsExt;
391 let mut perms = tokio::fs::metadata(dest_dir.join("entrypoint.sh")).await?.permissions();
392 perms.set_mode(0o755);
393 tokio::fs::set_permissions(dest_dir.join("entrypoint.sh"), perms).await?;
394 }
395 }
396
397 let dockerignore_src = source_docker_path.join(".dockerignore");
399 if dockerignore_src.exists() {
400 tokio::fs::copy(&dockerignore_src, dest_dir.join(".dockerignore")).await?;
401 }
402
403 Ok(())
404 }
405
406 pub async fn copy_simulation_assets(&self, project_root: &Path) -> Result<()> {
417 let framework_path = self.detect_framework_path()?;
419 let source_assets_path = std::path::PathBuf::from(&framework_path)
420 .join("packages")
421 .join("simulation")
422 .join("environments")
423 .join("basic_arena")
424 .join("assets")
425 .join("images");
426
427 if !source_assets_path.exists() {
429 return Ok(());
431 }
432
433 let dest_assets_path = project_root.join("assets").join("images");
435 tokio::fs::create_dir_all(&dest_assets_path).await?;
436
437 let images = vec!["aiko.jpg", "phoebe.jpg"];
439 for image in images {
440 let source_file = source_assets_path.join(image);
441 if source_file.exists() {
442 let dest_file = dest_assets_path.join(image);
443 tokio::fs::copy(&source_file, &dest_file).await?;
444 }
445 }
446
447 Ok(())
448 }
449
450 async fn copy_node_configs(&self, project_root: &Path, component_name: &str) -> Result<()> {
460 let framework_path = self.detect_framework_path()?;
462 let source_configs_path = std::path::PathBuf::from(&framework_path)
463 .join("packages")
464 .join("nodes")
465 .join(component_name)
466 .join("configs");
467
468 if !source_configs_path.exists() {
470 return Ok(());
472 }
473
474 for env in &["dev", "production", "common"] {
476 let source_env_path = source_configs_path.join(env).join("config.json");
477
478 if source_env_path.exists() {
479 let dest_env_path = project_root
480 .join("configs")
481 .join(env)
482 .join("nodes")
483 .join(component_name);
484
485 tokio::fs::create_dir_all(&dest_env_path).await?;
487
488 let dest_file = dest_env_path.join("config.json");
490 tokio::fs::copy(&source_env_path, &dest_file).await?;
491 }
492 }
493
494 Ok(())
495 }
496
497 pub fn detect_framework_path(&self) -> Result<String> {
510 if let Ok(path) = std::env::var("MECHA10_FRAMEWORK_PATH") {
512 let path_buf = std::path::PathBuf::from(&path);
513
514 let expanded_path = if let Some(stripped) = path.strip_prefix("~/") {
516 if let Ok(home) = std::env::var("HOME") {
517 std::path::PathBuf::from(home).join(stripped)
518 } else {
519 path_buf.clone()
520 }
521 } else {
522 path_buf.clone()
523 };
524
525 let core_path = expanded_path.join("packages").join("core");
527 if core_path.exists() {
528 return Ok(expanded_path.to_string_lossy().to_string());
529 } else {
530 return Err(anyhow::anyhow!(
531 "MECHA10_FRAMEWORK_PATH is set to '{}' but this doesn't appear to be the framework root.\n\
532 Expected to find packages/core directory at that location.",
533 path
534 ));
535 }
536 }
537
538 let mut current = std::env::current_dir()?;
540
541 loop {
542 let core_path = current.join("packages").join("core");
544 if core_path.exists() {
545 return Ok(current.to_string_lossy().to_string());
547 }
548
549 if !current.pop() {
551 return Err(anyhow::anyhow!(
552 "Could not detect framework path. Either:\n\
553 1. Set MECHA10_FRAMEWORK_PATH environment variable, or\n\
554 2. Run from within the mecha10-monorepo directory\n\n\
555 Example: export MECHA10_FRAMEWORK_PATH=~/src/laboratory-one/mecha10"
556 ));
557 }
558 }
559 }
560}
561
562impl Default for InitService {
563 fn default() -> Self {
564 Self::new()
565 }
566}