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, 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 redis_service: Option<RedisService>,
60 project_service: Option<ProjectService>,
61 simulation_service: Option<SimulationService>,
62 process_service: Option<ProcessService>,
63 shared_process_service: Option<std::sync::Arc<std::sync::Mutex<ProcessService>>>,
65 docker_service: Option<DockerService>,
66 package_service: Option<PackageService>,
67 deployment_service: Option<DeploymentService>,
68 component_catalog_service: Option<ComponentCatalogService>,
69 discovery_service: Option<DiscoveryService>,
70 init_service: Option<InitService>,
71 project_template_service: Option<ProjectTemplateService>,
72 dev_service: Option<DevService>,
73 topology_service: Option<TopologyService>,
74 template_download_service: Option<TemplateDownloadService>,
75
76 _phantom: std::marker::PhantomData<ProjectConfig>,
80}
81
82macro_rules! lazy_init_getter {
97 ($(#[$meta:meta])* fn $name:ident() -> &$ty:ty, $field:ident, self |$self_:ident| $ctor:expr) => {
98 $(#[$meta])*
99 pub fn $name(&mut self) -> &$ty {
100 if self.$field.is_none() {
101 let value = {
102 let $self_ = &*self;
103 $ctor
104 };
105 self.$field = Some(value);
106 }
107 self.$field.as_ref().unwrap()
108 }
109 };
110 ($(#[$meta:meta])* fn $name:ident() -> &$ty:ty, $field:ident, $ctor:expr) => {
111 $(#[$meta])*
112 pub fn $name(&mut self) -> &$ty {
113 if self.$field.is_none() {
114 self.$field = Some($ctor);
115 }
116 self.$field.as_ref().unwrap()
117 }
118 };
119 ($(#[$meta:meta])* mut fn $name:ident() -> &mut $ty:ty, $field:ident, $ctor:expr) => {
120 $(#[$meta])*
121 pub fn $name(&mut self) -> &mut $ty {
122 if self.$field.is_none() {
123 self.$field = Some($ctor);
124 }
125 self.$field.as_mut().unwrap()
126 }
127 };
128 ($(#[$meta:meta])* try fn $name:ident() -> &$ty:ty, $field:ident, |$self_:ident| $ctor:expr) => {
129 $(#[$meta])*
130 pub fn $name(&mut self) -> anyhow::Result<&$ty> {
131 if self.$field.is_none() {
132 let value = {
133 let $self_ = &*self;
134 $ctor
135 };
136 self.$field = Some(value);
137 }
138 Ok(self.$field.as_ref().unwrap())
139 }
140 };
141}
142
143impl CliContext {
144 pub async fn load_project_config(&self) -> anyhow::Result<ProjectConfig> {
150 ConfigService::load_from(&self.config_path).await
151 }
152
153 pub fn is_project_initialized(&self) -> bool {
155 ConfigService::is_initialized(&self.working_dir)
156 }
157
158 pub fn project_path(&self, path: &str) -> PathBuf {
160 self.working_dir.join(path)
161 }
162
163 pub fn is_verbose(&self) -> bool {
165 self.verbose
166 }
167
168 pub fn redis_url(&self) -> &str {
170 &self.redis_url
171 }
172
173 pub fn postgres_url(&self) -> Option<&str> {
175 self.postgres_url.as_deref()
176 }
177
178 lazy_init_getter!(
179 try fn redis() -> &RedisService, redis_service, |s| RedisService::new(&s.redis_url)?
181 );
182
183 lazy_init_getter!(
184 try fn project() -> &ProjectService, project_service, |s| ProjectService::detect(&s.working_dir)?
186 );
187
188 lazy_init_getter!(
189 fn simulation() -> &SimulationService, simulation_service, SimulationService::new()
191 );
192
193 lazy_init_getter!(
194 mut fn process() -> &mut ProcessService, process_service, ProcessService::new()
196 );
197
198 pub fn shared_process(&mut self) -> std::sync::Arc<std::sync::Mutex<ProcessService>> {
204 if self.shared_process_service.is_none() {
205 self.shared_process_service = Some(std::sync::Arc::new(std::sync::Mutex::new(ProcessService::new())));
206 }
207 self.shared_process_service.as_ref().unwrap().clone()
208 }
209
210 lazy_init_getter!(
211 fn docker() -> &DockerService, docker_service, DockerService::new()
213 );
214
215 pub fn package(&mut self) -> anyhow::Result<&PackageService> {
219 if self.package_service.is_none() {
220 let project = self.project()?;
221 let name = project.name()?;
222 let version = project.version()?;
223 self.package_service = Some(PackageService::new(name, version, self.working_dir.clone())?);
224 }
225 Ok(self.package_service.as_ref().unwrap())
226 }
227
228 lazy_init_getter!(
229 fn deployment() -> &DeploymentService, deployment_service, DeploymentService::new()
231 );
232
233 lazy_init_getter!(
234 fn component_catalog() -> &ComponentCatalogService, component_catalog_service, ComponentCatalogService::new()
236 );
237
238 lazy_init_getter!(
239 mut fn discovery() -> &mut DiscoveryService, discovery_service, DiscoveryService::new()
241 );
242
243 lazy_init_getter!(
244 fn init_service() -> &InitService, init_service, InitService::new()
246 );
247
248 lazy_init_getter!(
249 fn project_template_service() -> &ProjectTemplateService, project_template_service, ProjectTemplateService::new()
251 );
252
253 lazy_init_getter!(
254 fn dev() -> &DevService, dev_service, self |s| DevService::new(s.redis_url.clone())
256 );
257
258 lazy_init_getter!(
259 fn topology() -> &TopologyService, topology_service, self |s| TopologyService::new(s.working_dir.clone())
264 );
265
266 lazy_init_getter!(
267 fn template_download() -> &TemplateDownloadService, template_download_service, TemplateDownloadService::new()
272 );
273
274 pub fn validate_project_structure(&self) -> anyhow::Result<()> {
279 if !self.is_project_initialized() {
280 return Err(anyhow::anyhow!("Project not initialized. Run 'mecha10 init' first."));
281 }
282
283 let required_dirs = vec!["nodes", "drivers"];
284 for dir in required_dirs {
285 let path = self.project_path(dir);
286 if !path.exists() {
287 return Err(anyhow::anyhow!(
288 "Required directory missing: {}. Expected at: {}",
289 dir,
290 path.display()
291 ));
292 }
293 }
294
295 Ok(())
296 }
297
298 pub fn is_ci(&self) -> bool {
300 is_ci_environment()
301 }
302
303 pub fn logs_dir(&self) -> PathBuf {
305 self.project_path("logs")
306 }
307
308 pub fn data_dir(&self) -> PathBuf {
310 self.project_path("data")
311 }
312
313 pub fn recordings_dir(&self) -> PathBuf {
315 self.project_path("data/recordings")
316 }
317
318 pub fn maps_dir(&self) -> PathBuf {
320 self.project_path("data/maps")
321 }
322
323 pub fn telemetry_dir(&self) -> PathBuf {
325 self.project_path("data/telemetry")
326 }
327
328 pub fn simulation_dir(&self) -> PathBuf {
330 self.project_path(paths::project::SIMULATION_DIR)
331 }
332
333 pub fn rl_tasks_dir(&self) -> PathBuf {
337 self.project_path(paths::project::RL_TASKS_DIR)
338 }
339
340 pub fn target_debug_dir(&self) -> PathBuf {
342 self.project_path("target/debug")
343 }
344
345 pub fn target_release_dir(&self) -> PathBuf {
347 self.project_path("target/release")
348 }
349
350 pub fn packages_dir(&self) -> PathBuf {
352 self.project_path("target/packages")
353 }
354
355 pub fn ensure_dir(&self, path: &PathBuf) -> anyhow::Result<()> {
357 if !path.exists() {
358 std::fs::create_dir_all(path)?;
359 }
360 Ok(())
361 }
362}
363
364impl Default for CliContext {
365 fn default() -> Self {
366 CliContextBuilder::new()
367 .log_level(Level::INFO)
368 .build()
369 .expect("Failed to build default CliContext")
370 }
371}
372
373pub struct CliContextBuilder {
394 config_path: Option<PathBuf>,
395 log_level: Option<Level>,
396 working_dir: Option<PathBuf>,
397 redis_url: Option<String>,
398 postgres_url: Option<String>,
399 verbose: Option<bool>,
400}
401
402impl CliContextBuilder {
403 pub fn new() -> Self {
405 Self {
406 config_path: None,
407 log_level: None,
408 working_dir: None,
409 redis_url: None,
410 postgres_url: None,
411 verbose: None,
412 }
413 }
414
415 mecha10_core::builder_setter!(
416 assign, config_path, config_path, Option<PathBuf>
418 );
419
420 mecha10_core::builder_setter!(
421 into_some, log_level, log_level, Level
423 );
424
425 mecha10_core::builder_setter!(
426 into_some, working_dir, working_dir, PathBuf
428 );
429
430 mecha10_core::builder_setter!(
431 into_some, redis_url, redis_url, String
433 );
434
435 mecha10_core::builder_setter!(
436 assign, postgres_url, postgres_url, Option<String>
438 );
439
440 mecha10_core::builder_setter!(
441 into_some, verbose, verbose, bool
443 );
444
445 pub fn build(self) -> anyhow::Result<CliContext> {
447 let working_dir = self
448 .working_dir
449 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
450
451 let config_path = self
452 .config_path
453 .unwrap_or_else(|| working_dir.join(paths::PROJECT_CONFIG));
454
455 let config_redis_url = if config_path.exists() {
456 std::fs::read_to_string(&config_path)
457 .ok()
458 .and_then(|content| serde_json::from_str::<serde_json::Value>(&content).ok())
459 .and_then(|json| json.get("redis")?.get("url")?.as_str().map(String::from))
460 } else {
461 None
462 };
463
464 let redis_url = self
465 .redis_url
466 .or_else(|| std::env::var("MECHA10_REDIS_URL").ok())
467 .or_else(|| std::env::var("REDIS_URL").ok())
468 .or(config_redis_url)
469 .unwrap_or_else(|| "redis://localhost:6379".to_string());
470
471 let postgres_url = self
472 .postgres_url
473 .or_else(|| std::env::var("DATABASE_URL").ok())
474 .or_else(|| std::env::var("POSTGRES_URL").ok());
475
476 let log_level = self.log_level.unwrap_or(Level::INFO);
477
478 let verbose = self.verbose.unwrap_or(false);
479
480 Ok(CliContext {
481 config_path,
482 log_level,
483 working_dir,
484 redis_url,
485 postgres_url,
486 verbose,
487 redis_service: None,
488 project_service: None,
489 simulation_service: None,
490 process_service: None,
491 shared_process_service: None,
492 docker_service: None,
493 package_service: None,
494 deployment_service: None,
495 component_catalog_service: None,
496 discovery_service: None,
497 init_service: None,
498 project_template_service: None,
499 dev_service: None,
500 topology_service: None,
501 template_download_service: None,
502 _phantom: std::marker::PhantomData,
503 })
504 }
505}
506
507impl Default for CliContextBuilder {
508 fn default() -> Self {
509 Self::new()
510 }
511}
512
513pub fn is_ci_environment() -> bool {
520 std::env::var("CI").is_ok()
521 || std::env::var("GITHUB_ACTIONS").is_ok()
522 || std::env::var("GITLAB_CI").is_ok()
523 || std::env::var("CIRCLECI").is_ok()
524 || std::env::var("JENKINS_HOME").is_ok()
525}