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