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 docker_service: Option<DockerService>,
67 package_service: Option<PackageService>,
68 deployment_service: Option<DeploymentService>,
69 component_catalog_service: Option<ComponentCatalogService>,
70 discovery_service: Option<DiscoveryService>,
71 init_service: Option<InitService>,
72 project_template_service: Option<ProjectTemplateService>,
73 dev_service: Option<DevService>,
74 topology_service: Option<TopologyService>,
75 template_download_service: Option<TemplateDownloadService>,
76
77 _phantom: std::marker::PhantomData<ProjectConfig>,
81}
82
83impl CliContext {
84 pub async fn load_project_config(&self) -> anyhow::Result<ProjectConfig> {
90 ConfigService::load_from(&self.config_path).await
91 }
92
93 pub fn is_project_initialized(&self) -> bool {
95 ConfigService::is_initialized(&self.working_dir)
96 }
97
98 pub fn project_path(&self, path: &str) -> PathBuf {
100 self.working_dir.join(path)
101 }
102
103 pub fn is_verbose(&self) -> bool {
107 self.verbose
108 }
109
110 pub fn is_dev_mode(&self) -> bool {
112 self.dev_mode
113 }
114
115 pub fn redis_url(&self) -> &str {
117 &self.redis_url
118 }
119
120 pub fn postgres_url(&self) -> Option<&str> {
122 self.postgres_url.as_deref()
123 }
124
125 pub fn redis(&mut self) -> anyhow::Result<&RedisService> {
129 if self.redis_service.is_none() {
130 self.redis_service = Some(RedisService::new(&self.redis_url)?);
131 }
132 Ok(self.redis_service.as_ref().unwrap())
133 }
134
135 pub fn project(&mut self) -> anyhow::Result<&ProjectService> {
137 if self.project_service.is_none() {
138 self.project_service = Some(ProjectService::detect(&self.working_dir)?);
139 }
140 Ok(self.project_service.as_ref().unwrap())
141 }
142
143 pub fn template(&mut self) -> &TemplateService {
145 if self.template_service.is_none() {
146 self.template_service = Some(TemplateService::new());
147 }
148 self.template_service.as_ref().unwrap()
149 }
150
151 pub fn simulation(&mut self) -> &SimulationService {
153 if self.simulation_service.is_none() {
154 self.simulation_service = Some(SimulationService::new());
155 }
156 self.simulation_service.as_ref().unwrap()
157 }
158
159 pub fn process(&mut self) -> &mut ProcessService {
161 if self.process_service.is_none() {
162 self.process_service = Some(ProcessService::new());
163 }
164 self.process_service.as_mut().unwrap()
165 }
166
167 pub fn docker(&mut self) -> &DockerService {
169 if self.docker_service.is_none() {
170 self.docker_service = Some(DockerService::new());
171 }
172 self.docker_service.as_ref().unwrap()
173 }
174
175 pub fn package(&mut self) -> anyhow::Result<&PackageService> {
179 if self.package_service.is_none() {
180 let project = self.project()?;
181 let name = project.name()?;
182 let version = project.version()?;
183 self.package_service = Some(PackageService::new(name, version, self.working_dir.clone())?);
184 }
185 Ok(self.package_service.as_ref().unwrap())
186 }
187
188 pub fn deployment(&mut self) -> &DeploymentService {
190 if self.deployment_service.is_none() {
191 self.deployment_service = Some(DeploymentService::new());
192 }
193 self.deployment_service.as_ref().unwrap()
194 }
195
196 pub fn component_catalog(&mut self) -> &ComponentCatalogService {
198 if self.component_catalog_service.is_none() {
199 self.component_catalog_service = Some(ComponentCatalogService::new());
200 }
201 self.component_catalog_service.as_ref().unwrap()
202 }
203
204 pub fn discovery(&mut self) -> &mut DiscoveryService {
206 if self.discovery_service.is_none() {
207 self.discovery_service = Some(DiscoveryService::new());
208 }
209 self.discovery_service.as_mut().unwrap()
210 }
211
212 pub fn init_service(&mut self) -> &InitService {
214 if self.init_service.is_none() {
215 self.init_service = Some(InitService::new());
216 }
217 self.init_service.as_ref().unwrap()
218 }
219
220 pub fn project_template_service(&mut self) -> &ProjectTemplateService {
222 if self.project_template_service.is_none() {
223 self.project_template_service = Some(ProjectTemplateService::new());
224 }
225 self.project_template_service.as_ref().unwrap()
226 }
227
228 pub fn dev(&mut self) -> &DevService {
230 if self.dev_service.is_none() {
231 self.dev_service = Some(DevService::new(self.redis_url.clone()));
232 }
233 self.dev_service.as_ref().unwrap()
234 }
235
236 pub fn topology(&mut self) -> &TopologyService {
241 if self.topology_service.is_none() {
242 self.topology_service = Some(TopologyService::new(self.working_dir.clone()));
243 }
244 self.topology_service.as_ref().unwrap()
245 }
246
247 pub fn template_download(&mut self) -> &TemplateDownloadService {
252 if self.template_download_service.is_none() {
253 self.template_download_service = Some(TemplateDownloadService::new());
254 }
255 self.template_download_service.as_ref().unwrap()
256 }
257
258 pub fn validate_docker(&mut self) -> anyhow::Result<()> {
262 let docker = self.docker();
263 docker.check_installation()?;
264 docker.check_daemon()?;
265 Ok(())
266 }
267
268 pub async fn validate_redis(&mut self) -> anyhow::Result<()> {
270 let redis = self.redis()?;
271 let _ = redis;
274 Ok(())
275 }
276
277 pub fn validate_project_structure(&self) -> anyhow::Result<()> {
282 if !self.is_project_initialized() {
283 return Err(anyhow::anyhow!("Project not initialized. Run 'mecha10 init' first."));
284 }
285
286 let required_dirs = vec!["nodes", "drivers"];
288 for dir in required_dirs {
289 let path = self.project_path(dir);
290 if !path.exists() {
291 return Err(anyhow::anyhow!(
292 "Required directory missing: {}. Expected at: {}",
293 dir,
294 path.display()
295 ));
296 }
297 }
298
299 Ok(())
300 }
301
302 pub fn validate_godot(&mut self) -> anyhow::Result<()> {
304 let sim = self.simulation();
305 sim.validate_godot()?;
306 Ok(())
307 }
308
309 pub fn is_ci(&self) -> bool {
313 std::env::var("CI").is_ok()
314 || std::env::var("GITHUB_ACTIONS").is_ok()
315 || std::env::var("GITLAB_CI").is_ok()
316 || std::env::var("CIRCLECI").is_ok()
317 || std::env::var("JENKINS_HOME").is_ok()
318 }
319
320 pub fn is_interactive(&self) -> bool {
322 atty::is(atty::Stream::Stdout) && !self.is_ci()
323 }
324
325 pub fn logs_dir(&self) -> PathBuf {
327 self.project_path("logs")
328 }
329
330 pub fn data_dir(&self) -> PathBuf {
332 self.project_path("data")
333 }
334
335 pub fn recordings_dir(&self) -> PathBuf {
337 self.project_path("data/recordings")
338 }
339
340 pub fn maps_dir(&self) -> PathBuf {
342 self.project_path("data/maps")
343 }
344
345 pub fn telemetry_dir(&self) -> PathBuf {
347 self.project_path("data/telemetry")
348 }
349
350 pub fn simulation_dir(&self) -> PathBuf {
352 self.project_path("simulation")
353 }
354
355 pub fn target_debug_dir(&self) -> PathBuf {
357 self.project_path("target/debug")
358 }
359
360 pub fn target_release_dir(&self) -> PathBuf {
362 self.project_path("target/release")
363 }
364
365 pub fn packages_dir(&self) -> PathBuf {
367 self.project_path("target/packages")
368 }
369
370 pub fn ensure_dir(&self, path: &PathBuf) -> anyhow::Result<()> {
372 if !path.exists() {
373 std::fs::create_dir_all(path)?;
374 }
375 Ok(())
376 }
377}
378
379impl Default for CliContext {
380 fn default() -> Self {
381 CliContextBuilder::new()
382 .log_level(Level::INFO)
383 .build()
384 .expect("Failed to build default CliContext")
385 }
386}
387
388pub struct CliContextBuilder {
412 config_path: Option<PathBuf>,
413 log_level: Option<Level>,
414 working_dir: Option<PathBuf>,
415 redis_url: Option<String>,
416 postgres_url: Option<String>,
417 verbose: Option<bool>,
418 dev_mode: Option<bool>,
419}
420
421impl CliContextBuilder {
422 pub fn new() -> Self {
424 Self {
425 config_path: None,
426 log_level: None,
427 working_dir: None,
428 redis_url: None,
429 postgres_url: None,
430 verbose: None,
431 dev_mode: None,
432 }
433 }
434
435 pub fn config_path(mut self, path: Option<PathBuf>) -> Self {
437 self.config_path = path;
438 self
439 }
440
441 pub fn log_level(mut self, level: Level) -> Self {
443 self.log_level = Some(level);
444 self
445 }
446
447 pub fn working_dir(mut self, dir: PathBuf) -> Self {
449 self.working_dir = Some(dir);
450 self
451 }
452
453 pub fn redis_url(mut self, url: String) -> Self {
455 self.redis_url = Some(url);
456 self
457 }
458
459 pub fn postgres_url(mut self, url: Option<String>) -> Self {
461 self.postgres_url = url;
462 self
463 }
464
465 pub fn verbose(mut self, verbose: bool) -> Self {
467 self.verbose = Some(verbose);
468 self
469 }
470
471 pub fn dev_mode(mut self, dev: bool) -> Self {
473 self.dev_mode = Some(dev);
474 self
475 }
476
477 pub fn build(self) -> anyhow::Result<CliContext> {
479 let working_dir = self
481 .working_dir
482 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
483
484 let config_path = self.config_path.unwrap_or_else(|| working_dir.join("mecha10.json"));
486
487 let config_redis_url = if config_path.exists() {
489 std::fs::read_to_string(&config_path)
490 .ok()
491 .and_then(|content| serde_json::from_str::<serde_json::Value>(&content).ok())
492 .and_then(|json| json.get("redis")?.get("url")?.as_str().map(String::from))
493 } else {
494 None
495 };
496
497 let redis_url = self
499 .redis_url
500 .or_else(|| std::env::var("MECHA10_REDIS_URL").ok())
501 .or_else(|| std::env::var("REDIS_URL").ok())
502 .or(config_redis_url)
503 .unwrap_or_else(|| "redis://localhost:6380".to_string());
504
505 let postgres_url = self
507 .postgres_url
508 .or_else(|| std::env::var("DATABASE_URL").ok())
509 .or_else(|| std::env::var("POSTGRES_URL").ok());
510
511 let log_level = self.log_level.unwrap_or(Level::INFO);
513
514 let verbose = self.verbose.unwrap_or(false);
516
517 let dev_mode = self.dev_mode.unwrap_or_else(|| {
519 std::env::var("MECHA10_DEV_MODE")
520 .or_else(|_| std::env::var("DEV_MODE"))
521 .map(|v| v == "1" || v.to_lowercase() == "true")
522 .unwrap_or(false)
523 });
524
525 Ok(CliContext {
526 config_path,
527 log_level,
528 working_dir,
529 redis_url,
530 postgres_url,
531 verbose,
532 dev_mode,
533 redis_service: None,
534 project_service: None,
535 template_service: None,
536 simulation_service: None,
537 process_service: None,
538 docker_service: None,
539 package_service: None,
540 deployment_service: None,
541 component_catalog_service: None,
542 discovery_service: None,
543 init_service: None,
544 project_template_service: None,
545 dev_service: None,
546 topology_service: None,
547 template_download_service: None,
548 _phantom: std::marker::PhantomData,
549 })
550 }
551}
552
553impl Default for CliContextBuilder {
554 fn default() -> Self {
555 Self::new()
556 }
557}