1#![allow(dead_code)]
2
3use crate::services::{
9 ComponentCatalogService, ConfigService, DeploymentService, DevService, DiscoveryService, DockerService,
10 InitService, PackageService, ProcessService, ProjectService, ProjectTemplateService, RedisService,
11 SimulationService, 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
76 _phantom: std::marker::PhantomData<ProjectConfig>,
80}
81
82impl CliContext {
83 pub async fn load_project_config(&self) -> anyhow::Result<ProjectConfig> {
89 ConfigService::load_from(&self.config_path).await
90 }
91
92 pub fn is_project_initialized(&self) -> bool {
94 ConfigService::is_initialized(&self.working_dir)
95 }
96
97 pub fn project_path(&self, path: &str) -> PathBuf {
99 self.working_dir.join(path)
100 }
101
102 pub fn is_verbose(&self) -> bool {
106 self.verbose
107 }
108
109 pub fn is_dev_mode(&self) -> bool {
111 self.dev_mode
112 }
113
114 pub fn redis_url(&self) -> &str {
116 &self.redis_url
117 }
118
119 pub fn postgres_url(&self) -> Option<&str> {
121 self.postgres_url.as_deref()
122 }
123
124 pub fn redis(&mut self) -> anyhow::Result<&RedisService> {
128 if self.redis_service.is_none() {
129 self.redis_service = Some(RedisService::new(&self.redis_url)?);
130 }
131 Ok(self.redis_service.as_ref().unwrap())
132 }
133
134 pub fn project(&mut self) -> anyhow::Result<&ProjectService> {
136 if self.project_service.is_none() {
137 self.project_service = Some(ProjectService::detect(&self.working_dir)?);
138 }
139 Ok(self.project_service.as_ref().unwrap())
140 }
141
142 pub fn template(&mut self) -> &TemplateService {
144 if self.template_service.is_none() {
145 self.template_service = Some(TemplateService::new());
146 }
147 self.template_service.as_ref().unwrap()
148 }
149
150 pub fn simulation(&mut self) -> &SimulationService {
152 if self.simulation_service.is_none() {
153 self.simulation_service = Some(SimulationService::new());
154 }
155 self.simulation_service.as_ref().unwrap()
156 }
157
158 pub fn process(&mut self) -> &mut ProcessService {
160 if self.process_service.is_none() {
161 self.process_service = Some(ProcessService::new());
162 }
163 self.process_service.as_mut().unwrap()
164 }
165
166 pub fn docker(&mut self) -> &DockerService {
168 if self.docker_service.is_none() {
169 self.docker_service = Some(DockerService::new());
170 }
171 self.docker_service.as_ref().unwrap()
172 }
173
174 pub fn package(&mut self) -> anyhow::Result<&PackageService> {
178 if self.package_service.is_none() {
179 let project = self.project()?;
180 let name = project.name()?;
181 let version = project.version()?;
182 self.package_service = Some(PackageService::new(name, version, self.working_dir.clone())?);
183 }
184 Ok(self.package_service.as_ref().unwrap())
185 }
186
187 pub fn deployment(&mut self) -> &DeploymentService {
189 if self.deployment_service.is_none() {
190 self.deployment_service = Some(DeploymentService::new());
191 }
192 self.deployment_service.as_ref().unwrap()
193 }
194
195 pub fn component_catalog(&mut self) -> &ComponentCatalogService {
197 if self.component_catalog_service.is_none() {
198 self.component_catalog_service = Some(ComponentCatalogService::new());
199 }
200 self.component_catalog_service.as_ref().unwrap()
201 }
202
203 pub fn discovery(&mut self) -> &mut DiscoveryService {
205 if self.discovery_service.is_none() {
206 self.discovery_service = Some(DiscoveryService::new());
207 }
208 self.discovery_service.as_mut().unwrap()
209 }
210
211 pub fn init_service(&mut self) -> &InitService {
213 if self.init_service.is_none() {
214 self.init_service = Some(InitService::new());
215 }
216 self.init_service.as_ref().unwrap()
217 }
218
219 pub fn project_template_service(&mut self) -> &ProjectTemplateService {
221 if self.project_template_service.is_none() {
222 self.project_template_service = Some(ProjectTemplateService::new());
223 }
224 self.project_template_service.as_ref().unwrap()
225 }
226
227 pub fn dev(&mut self) -> &DevService {
229 if self.dev_service.is_none() {
230 self.dev_service = Some(DevService::new(self.redis_url.clone()));
231 }
232 self.dev_service.as_ref().unwrap()
233 }
234
235 pub fn topology(&mut self) -> &TopologyService {
240 if self.topology_service.is_none() {
241 self.topology_service = Some(TopologyService::new(self.working_dir.clone()));
242 }
243 self.topology_service.as_ref().unwrap()
244 }
245
246 pub fn validate_docker(&mut self) -> anyhow::Result<()> {
250 let docker = self.docker();
251 docker.check_installation()?;
252 docker.check_daemon()?;
253 Ok(())
254 }
255
256 pub async fn validate_redis(&mut self) -> anyhow::Result<()> {
258 let redis = self.redis()?;
259 let _ = redis;
262 Ok(())
263 }
264
265 pub fn validate_project_structure(&self) -> anyhow::Result<()> {
270 if !self.is_project_initialized() {
271 return Err(anyhow::anyhow!("Project not initialized. Run 'mecha10 init' first."));
272 }
273
274 let required_dirs = vec!["nodes", "drivers"];
276 for dir in required_dirs {
277 let path = self.project_path(dir);
278 if !path.exists() {
279 return Err(anyhow::anyhow!(
280 "Required directory missing: {}. Expected at: {}",
281 dir,
282 path.display()
283 ));
284 }
285 }
286
287 Ok(())
288 }
289
290 pub fn validate_godot(&mut self) -> anyhow::Result<()> {
292 let sim = self.simulation();
293 sim.validate_godot()?;
294 Ok(())
295 }
296
297 pub fn is_ci(&self) -> bool {
301 std::env::var("CI").is_ok()
302 || std::env::var("GITHUB_ACTIONS").is_ok()
303 || std::env::var("GITLAB_CI").is_ok()
304 || std::env::var("CIRCLECI").is_ok()
305 || std::env::var("JENKINS_HOME").is_ok()
306 }
307
308 pub fn is_interactive(&self) -> bool {
310 atty::is(atty::Stream::Stdout) && !self.is_ci()
311 }
312
313 pub fn logs_dir(&self) -> PathBuf {
315 self.project_path("logs")
316 }
317
318 pub fn data_dir(&self) -> PathBuf {
320 self.project_path("data")
321 }
322
323 pub fn recordings_dir(&self) -> PathBuf {
325 self.project_path("data/recordings")
326 }
327
328 pub fn maps_dir(&self) -> PathBuf {
330 self.project_path("data/maps")
331 }
332
333 pub fn telemetry_dir(&self) -> PathBuf {
335 self.project_path("data/telemetry")
336 }
337
338 pub fn simulation_dir(&self) -> PathBuf {
340 self.project_path("simulation")
341 }
342
343 pub fn target_debug_dir(&self) -> PathBuf {
345 self.project_path("target/debug")
346 }
347
348 pub fn target_release_dir(&self) -> PathBuf {
350 self.project_path("target/release")
351 }
352
353 pub fn packages_dir(&self) -> PathBuf {
355 self.project_path("target/packages")
356 }
357
358 pub fn ensure_dir(&self, path: &PathBuf) -> anyhow::Result<()> {
360 if !path.exists() {
361 std::fs::create_dir_all(path)?;
362 }
363 Ok(())
364 }
365}
366
367impl Default for CliContext {
368 fn default() -> Self {
369 CliContextBuilder::new()
370 .log_level(Level::INFO)
371 .build()
372 .expect("Failed to build default CliContext")
373 }
374}
375
376pub struct CliContextBuilder {
400 config_path: Option<PathBuf>,
401 log_level: Option<Level>,
402 working_dir: Option<PathBuf>,
403 redis_url: Option<String>,
404 postgres_url: Option<String>,
405 verbose: Option<bool>,
406 dev_mode: Option<bool>,
407}
408
409impl CliContextBuilder {
410 pub fn new() -> Self {
412 Self {
413 config_path: None,
414 log_level: None,
415 working_dir: None,
416 redis_url: None,
417 postgres_url: None,
418 verbose: None,
419 dev_mode: None,
420 }
421 }
422
423 pub fn config_path(mut self, path: Option<PathBuf>) -> Self {
425 self.config_path = path;
426 self
427 }
428
429 pub fn log_level(mut self, level: Level) -> Self {
431 self.log_level = Some(level);
432 self
433 }
434
435 pub fn working_dir(mut self, dir: PathBuf) -> Self {
437 self.working_dir = Some(dir);
438 self
439 }
440
441 pub fn redis_url(mut self, url: String) -> Self {
443 self.redis_url = Some(url);
444 self
445 }
446
447 pub fn postgres_url(mut self, url: Option<String>) -> Self {
449 self.postgres_url = url;
450 self
451 }
452
453 pub fn verbose(mut self, verbose: bool) -> Self {
455 self.verbose = Some(verbose);
456 self
457 }
458
459 pub fn dev_mode(mut self, dev: bool) -> Self {
461 self.dev_mode = Some(dev);
462 self
463 }
464
465 pub fn build(self) -> anyhow::Result<CliContext> {
467 let working_dir = self
469 .working_dir
470 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
471
472 let config_path = self.config_path.unwrap_or_else(|| working_dir.join("mecha10.json"));
474
475 let config_redis_url = if config_path.exists() {
477 std::fs::read_to_string(&config_path)
478 .ok()
479 .and_then(|content| serde_json::from_str::<serde_json::Value>(&content).ok())
480 .and_then(|json| json.get("redis")?.get("url")?.as_str().map(String::from))
481 } else {
482 None
483 };
484
485 let redis_url = self
487 .redis_url
488 .or_else(|| std::env::var("MECHA10_REDIS_URL").ok())
489 .or_else(|| std::env::var("REDIS_URL").ok())
490 .or(config_redis_url)
491 .unwrap_or_else(|| "redis://localhost:6380".to_string());
492
493 let postgres_url = self
495 .postgres_url
496 .or_else(|| std::env::var("DATABASE_URL").ok())
497 .or_else(|| std::env::var("POSTGRES_URL").ok());
498
499 let log_level = self.log_level.unwrap_or(Level::INFO);
501
502 let verbose = self.verbose.unwrap_or(false);
504
505 let dev_mode = self.dev_mode.unwrap_or_else(|| {
507 std::env::var("MECHA10_DEV_MODE")
508 .or_else(|_| std::env::var("DEV_MODE"))
509 .map(|v| v == "1" || v.to_lowercase() == "true")
510 .unwrap_or(false)
511 });
512
513 Ok(CliContext {
514 config_path,
515 log_level,
516 working_dir,
517 redis_url,
518 postgres_url,
519 verbose,
520 dev_mode,
521 redis_service: None,
522 project_service: None,
523 template_service: None,
524 simulation_service: None,
525 process_service: None,
526 docker_service: None,
527 package_service: None,
528 deployment_service: None,
529 component_catalog_service: None,
530 discovery_service: None,
531 init_service: None,
532 project_template_service: None,
533 dev_service: None,
534 topology_service: None,
535 _phantom: std::marker::PhantomData,
536 })
537 }
538}
539
540impl Default for CliContextBuilder {
541 fn default() -> Self {
542 Self::new()
543 }
544}