1#![allow(dead_code)]
2
3use crate::services::{
9 ComponentCatalogService, ConfigService, DeploymentService, DevService, DiscoveryService, DockerService,
10 InitService, PackageService, ProcessService, ProjectService, ProjectTemplateService, RedisService,
11 SimulationService, TemplateDownloadService, TemplateService, TopologyService,
12};
13use crate::types::ProjectConfig;
14use std::path::PathBuf;
15use tracing::Level;
16
17pub struct CliContext {
39 pub config_path: PathBuf,
41
42 pub log_level: Level,
44
45 pub working_dir: PathBuf,
47
48 redis_url: String,
50
51 postgres_url: Option<String>,
53
54 verbose: bool,
56
57 dev_mode: bool,
59
60 redis_service: Option<RedisService>,
62 project_service: Option<ProjectService>,
63 template_service: Option<TemplateService>,
64 simulation_service: Option<SimulationService>,
65 process_service: Option<ProcessService>,
66 shared_process_service: Option<std::sync::Arc<std::sync::Mutex<ProcessService>>>,
68 docker_service: Option<DockerService>,
69 package_service: Option<PackageService>,
70 deployment_service: Option<DeploymentService>,
71 component_catalog_service: Option<ComponentCatalogService>,
72 discovery_service: Option<DiscoveryService>,
73 init_service: Option<InitService>,
74 project_template_service: Option<ProjectTemplateService>,
75 dev_service: Option<DevService>,
76 topology_service: Option<TopologyService>,
77 template_download_service: Option<TemplateDownloadService>,
78
79 _phantom: std::marker::PhantomData<ProjectConfig>,
83}
84
85impl CliContext {
86 pub async fn load_project_config(&self) -> anyhow::Result<ProjectConfig> {
92 ConfigService::load_from(&self.config_path).await
93 }
94
95 pub fn is_project_initialized(&self) -> bool {
97 ConfigService::is_initialized(&self.working_dir)
98 }
99
100 pub fn project_path(&self, path: &str) -> PathBuf {
102 self.working_dir.join(path)
103 }
104
105 pub fn is_verbose(&self) -> bool {
109 self.verbose
110 }
111
112 pub fn is_dev_mode(&self) -> bool {
114 self.dev_mode
115 }
116
117 pub fn redis_url(&self) -> &str {
119 &self.redis_url
120 }
121
122 pub fn postgres_url(&self) -> Option<&str> {
124 self.postgres_url.as_deref()
125 }
126
127 pub fn redis(&mut self) -> anyhow::Result<&RedisService> {
131 if self.redis_service.is_none() {
132 self.redis_service = Some(RedisService::new(&self.redis_url)?);
133 }
134 Ok(self.redis_service.as_ref().unwrap())
135 }
136
137 pub fn project(&mut self) -> anyhow::Result<&ProjectService> {
139 if self.project_service.is_none() {
140 self.project_service = Some(ProjectService::detect(&self.working_dir)?);
141 }
142 Ok(self.project_service.as_ref().unwrap())
143 }
144
145 pub fn template(&mut self) -> &TemplateService {
147 if self.template_service.is_none() {
148 self.template_service = Some(TemplateService::new());
149 }
150 self.template_service.as_ref().unwrap()
151 }
152
153 pub fn simulation(&mut self) -> &SimulationService {
155 if self.simulation_service.is_none() {
156 self.simulation_service = Some(SimulationService::new());
157 }
158 self.simulation_service.as_ref().unwrap()
159 }
160
161 pub fn process(&mut self) -> &mut ProcessService {
163 if self.process_service.is_none() {
164 self.process_service = Some(ProcessService::new());
165 }
166 self.process_service.as_mut().unwrap()
167 }
168
169 pub fn shared_process(&mut self) -> std::sync::Arc<std::sync::Mutex<ProcessService>> {
175 if self.shared_process_service.is_none() {
176 self.shared_process_service = Some(std::sync::Arc::new(std::sync::Mutex::new(ProcessService::new())));
177 }
178 self.shared_process_service.as_ref().unwrap().clone()
179 }
180
181 pub fn docker(&mut self) -> &DockerService {
183 if self.docker_service.is_none() {
184 self.docker_service = Some(DockerService::new());
185 }
186 self.docker_service.as_ref().unwrap()
187 }
188
189 pub fn package(&mut self) -> anyhow::Result<&PackageService> {
193 if self.package_service.is_none() {
194 let project = self.project()?;
195 let name = project.name()?;
196 let version = project.version()?;
197 self.package_service = Some(PackageService::new(name, version, self.working_dir.clone())?);
198 }
199 Ok(self.package_service.as_ref().unwrap())
200 }
201
202 pub fn deployment(&mut self) -> &DeploymentService {
204 if self.deployment_service.is_none() {
205 self.deployment_service = Some(DeploymentService::new());
206 }
207 self.deployment_service.as_ref().unwrap()
208 }
209
210 pub fn component_catalog(&mut self) -> &ComponentCatalogService {
212 if self.component_catalog_service.is_none() {
213 self.component_catalog_service = Some(ComponentCatalogService::new());
214 }
215 self.component_catalog_service.as_ref().unwrap()
216 }
217
218 pub fn discovery(&mut self) -> &mut DiscoveryService {
220 if self.discovery_service.is_none() {
221 self.discovery_service = Some(DiscoveryService::new());
222 }
223 self.discovery_service.as_mut().unwrap()
224 }
225
226 pub fn init_service(&mut self) -> &InitService {
228 if self.init_service.is_none() {
229 self.init_service = Some(InitService::new());
230 }
231 self.init_service.as_ref().unwrap()
232 }
233
234 pub fn project_template_service(&mut self) -> &ProjectTemplateService {
236 if self.project_template_service.is_none() {
237 self.project_template_service = Some(ProjectTemplateService::new());
238 }
239 self.project_template_service.as_ref().unwrap()
240 }
241
242 pub fn dev(&mut self) -> &DevService {
244 if self.dev_service.is_none() {
245 self.dev_service = Some(DevService::new(self.redis_url.clone()));
246 }
247 self.dev_service.as_ref().unwrap()
248 }
249
250 pub fn topology(&mut self) -> &TopologyService {
255 if self.topology_service.is_none() {
256 self.topology_service = Some(TopologyService::new(self.working_dir.clone()));
257 }
258 self.topology_service.as_ref().unwrap()
259 }
260
261 pub fn template_download(&mut self) -> &TemplateDownloadService {
266 if self.template_download_service.is_none() {
267 self.template_download_service = Some(TemplateDownloadService::new());
268 }
269 self.template_download_service.as_ref().unwrap()
270 }
271
272 pub fn validate_docker(&mut self) -> anyhow::Result<()> {
276 let docker = self.docker();
277 docker.check_installation()?;
278 docker.check_daemon()?;
279 Ok(())
280 }
281
282 pub async fn validate_redis(&mut self) -> anyhow::Result<()> {
284 let redis = self.redis()?;
285 let _ = redis;
288 Ok(())
289 }
290
291 pub fn validate_project_structure(&self) -> anyhow::Result<()> {
296 if !self.is_project_initialized() {
297 return Err(anyhow::anyhow!("Project not initialized. Run 'mecha10 init' first."));
298 }
299
300 let required_dirs = vec!["nodes", "drivers"];
302 for dir in required_dirs {
303 let path = self.project_path(dir);
304 if !path.exists() {
305 return Err(anyhow::anyhow!(
306 "Required directory missing: {}. Expected at: {}",
307 dir,
308 path.display()
309 ));
310 }
311 }
312
313 Ok(())
314 }
315
316 pub fn validate_godot(&mut self) -> anyhow::Result<()> {
318 let sim = self.simulation();
319 sim.validate_godot()?;
320 Ok(())
321 }
322
323 pub fn is_ci(&self) -> bool {
327 std::env::var("CI").is_ok()
328 || std::env::var("GITHUB_ACTIONS").is_ok()
329 || std::env::var("GITLAB_CI").is_ok()
330 || std::env::var("CIRCLECI").is_ok()
331 || std::env::var("JENKINS_HOME").is_ok()
332 }
333
334 pub fn is_interactive(&self) -> bool {
336 atty::is(atty::Stream::Stdout) && !self.is_ci()
337 }
338
339 pub fn logs_dir(&self) -> PathBuf {
341 self.project_path("logs")
342 }
343
344 pub fn data_dir(&self) -> PathBuf {
346 self.project_path("data")
347 }
348
349 pub fn recordings_dir(&self) -> PathBuf {
351 self.project_path("data/recordings")
352 }
353
354 pub fn maps_dir(&self) -> PathBuf {
356 self.project_path("data/maps")
357 }
358
359 pub fn telemetry_dir(&self) -> PathBuf {
361 self.project_path("data/telemetry")
362 }
363
364 pub fn simulation_dir(&self) -> PathBuf {
366 self.project_path("simulation")
367 }
368
369 pub fn target_debug_dir(&self) -> PathBuf {
371 self.project_path("target/debug")
372 }
373
374 pub fn target_release_dir(&self) -> PathBuf {
376 self.project_path("target/release")
377 }
378
379 pub fn packages_dir(&self) -> PathBuf {
381 self.project_path("target/packages")
382 }
383
384 pub fn ensure_dir(&self, path: &PathBuf) -> anyhow::Result<()> {
386 if !path.exists() {
387 std::fs::create_dir_all(path)?;
388 }
389 Ok(())
390 }
391}
392
393impl Default for CliContext {
394 fn default() -> Self {
395 CliContextBuilder::new()
396 .log_level(Level::INFO)
397 .build()
398 .expect("Failed to build default CliContext")
399 }
400}
401
402pub struct CliContextBuilder {
426 config_path: Option<PathBuf>,
427 log_level: Option<Level>,
428 working_dir: Option<PathBuf>,
429 redis_url: Option<String>,
430 postgres_url: Option<String>,
431 verbose: Option<bool>,
432 dev_mode: Option<bool>,
433}
434
435impl CliContextBuilder {
436 pub fn new() -> Self {
438 Self {
439 config_path: None,
440 log_level: None,
441 working_dir: None,
442 redis_url: None,
443 postgres_url: None,
444 verbose: None,
445 dev_mode: None,
446 }
447 }
448
449 pub fn config_path(mut self, path: Option<PathBuf>) -> Self {
451 self.config_path = path;
452 self
453 }
454
455 pub fn log_level(mut self, level: Level) -> Self {
457 self.log_level = Some(level);
458 self
459 }
460
461 pub fn working_dir(mut self, dir: PathBuf) -> Self {
463 self.working_dir = Some(dir);
464 self
465 }
466
467 pub fn redis_url(mut self, url: String) -> Self {
469 self.redis_url = Some(url);
470 self
471 }
472
473 pub fn postgres_url(mut self, url: Option<String>) -> Self {
475 self.postgres_url = url;
476 self
477 }
478
479 pub fn verbose(mut self, verbose: bool) -> Self {
481 self.verbose = Some(verbose);
482 self
483 }
484
485 pub fn dev_mode(mut self, dev: bool) -> Self {
487 self.dev_mode = Some(dev);
488 self
489 }
490
491 pub fn build(self) -> anyhow::Result<CliContext> {
493 let working_dir = self
495 .working_dir
496 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
497
498 let config_path = self.config_path.unwrap_or_else(|| working_dir.join("mecha10.json"));
500
501 let config_redis_url = if config_path.exists() {
503 std::fs::read_to_string(&config_path)
504 .ok()
505 .and_then(|content| serde_json::from_str::<serde_json::Value>(&content).ok())
506 .and_then(|json| json.get("redis")?.get("url")?.as_str().map(String::from))
507 } else {
508 None
509 };
510
511 let redis_url = self
513 .redis_url
514 .or_else(|| std::env::var("MECHA10_REDIS_URL").ok())
515 .or_else(|| std::env::var("REDIS_URL").ok())
516 .or(config_redis_url)
517 .unwrap_or_else(|| "redis://localhost:6380".to_string());
518
519 let postgres_url = self
521 .postgres_url
522 .or_else(|| std::env::var("DATABASE_URL").ok())
523 .or_else(|| std::env::var("POSTGRES_URL").ok());
524
525 let log_level = self.log_level.unwrap_or(Level::INFO);
527
528 let verbose = self.verbose.unwrap_or(false);
530
531 let dev_mode = self.dev_mode.unwrap_or_else(|| {
533 std::env::var("MECHA10_DEV_MODE")
534 .or_else(|_| std::env::var("DEV_MODE"))
535 .map(|v| v == "1" || v.to_lowercase() == "true")
536 .unwrap_or(false)
537 });
538
539 Ok(CliContext {
540 config_path,
541 log_level,
542 working_dir,
543 redis_url,
544 postgres_url,
545 verbose,
546 dev_mode,
547 redis_service: None,
548 project_service: None,
549 template_service: None,
550 simulation_service: None,
551 process_service: None,
552 shared_process_service: None,
553 docker_service: None,
554 package_service: None,
555 deployment_service: None,
556 component_catalog_service: None,
557 discovery_service: None,
558 init_service: None,
559 project_template_service: None,
560 dev_service: None,
561 topology_service: None,
562 template_download_service: None,
563 _phantom: std::marker::PhantomData,
564 })
565 }
566}
567
568impl Default for CliContextBuilder {
569 fn default() -> Self {
570 Self::new()
571 }
572}