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 {
92 ($(#[$meta:meta])* fn $name:ident() -> &$ty:ty, $field:ident, $ctor:expr) => {
93 $(#[$meta])*
94 pub fn $name(&mut self) -> &$ty {
95 if self.$field.is_none() {
96 self.$field = Some($ctor);
97 }
98 self.$field.as_ref().unwrap()
99 }
100 };
101 ($(#[$meta:meta])* mut fn $name:ident() -> &mut $ty:ty, $field:ident, $ctor:expr) => {
102 $(#[$meta])*
103 pub fn $name(&mut self) -> &mut $ty {
104 if self.$field.is_none() {
105 self.$field = Some($ctor);
106 }
107 self.$field.as_mut().unwrap()
108 }
109 };
110 ($(#[$meta:meta])* try fn $name:ident() -> &$ty:ty, $field:ident, $ctor:expr) => {
111 $(#[$meta])*
112 pub fn $name(&mut self) -> anyhow::Result<&$ty> {
113 if self.$field.is_none() {
114 self.$field = Some($ctor);
115 }
116 Ok(self.$field.as_ref().unwrap())
117 }
118 };
119}
120
121impl CliContext {
122 pub async fn load_project_config(&self) -> anyhow::Result<ProjectConfig> {
128 ConfigService::load_from(&self.config_path).await
129 }
130
131 pub fn is_project_initialized(&self) -> bool {
133 ConfigService::is_initialized(&self.working_dir)
134 }
135
136 pub fn project_path(&self, path: &str) -> PathBuf {
138 self.working_dir.join(path)
139 }
140
141 pub fn is_verbose(&self) -> bool {
145 self.verbose
146 }
147
148 pub fn redis_url(&self) -> &str {
150 &self.redis_url
151 }
152
153 pub fn postgres_url(&self) -> Option<&str> {
155 self.postgres_url.as_deref()
156 }
157
158 pub fn redis(&mut self) -> anyhow::Result<&RedisService> {
165 if self.redis_service.is_none() {
166 self.redis_service = Some(RedisService::new(&self.redis_url)?);
167 }
168 Ok(self.redis_service.as_ref().unwrap())
169 }
170
171 pub fn project(&mut self) -> anyhow::Result<&ProjectService> {
174 if self.project_service.is_none() {
175 self.project_service = Some(ProjectService::detect(&self.working_dir)?);
176 }
177 Ok(self.project_service.as_ref().unwrap())
178 }
179
180 lazy_init_getter!(
181 fn simulation() -> &SimulationService, simulation_service, SimulationService::new()
183 );
184
185 lazy_init_getter!(
186 mut fn process() -> &mut ProcessService, process_service, ProcessService::new()
188 );
189
190 pub fn shared_process(&mut self) -> std::sync::Arc<std::sync::Mutex<ProcessService>> {
196 if self.shared_process_service.is_none() {
197 self.shared_process_service = Some(std::sync::Arc::new(std::sync::Mutex::new(ProcessService::new())));
198 }
199 self.shared_process_service.as_ref().unwrap().clone()
200 }
201
202 lazy_init_getter!(
203 fn docker() -> &DockerService, docker_service, DockerService::new()
205 );
206
207 pub fn package(&mut self) -> anyhow::Result<&PackageService> {
211 if self.package_service.is_none() {
212 let project = self.project()?;
213 let name = project.name()?;
214 let version = project.version()?;
215 self.package_service = Some(PackageService::new(name, version, self.working_dir.clone())?);
216 }
217 Ok(self.package_service.as_ref().unwrap())
218 }
219
220 lazy_init_getter!(
221 fn deployment() -> &DeploymentService, deployment_service, DeploymentService::new()
223 );
224
225 lazy_init_getter!(
226 fn component_catalog() -> &ComponentCatalogService, component_catalog_service, ComponentCatalogService::new()
228 );
229
230 lazy_init_getter!(
231 mut fn discovery() -> &mut DiscoveryService, discovery_service, DiscoveryService::new()
233 );
234
235 lazy_init_getter!(
236 fn init_service() -> &InitService, init_service, InitService::new()
238 );
239
240 lazy_init_getter!(
241 fn project_template_service() -> &ProjectTemplateService, project_template_service, ProjectTemplateService::new()
243 );
244
245 pub fn dev(&mut self) -> &DevService {
248 if self.dev_service.is_none() {
249 self.dev_service = Some(DevService::new(self.redis_url.clone()));
250 }
251 self.dev_service.as_ref().unwrap()
252 }
253
254 pub fn topology(&mut self) -> &TopologyService {
261 if self.topology_service.is_none() {
262 self.topology_service = Some(TopologyService::new(self.working_dir.clone()));
263 }
264 self.topology_service.as_ref().unwrap()
265 }
266
267 lazy_init_getter!(
268 fn template_download() -> &TemplateDownloadService, template_download_service, TemplateDownloadService::new()
273 );
274
275 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 is_ci(&self) -> bool {
306 std::env::var("CI").is_ok()
307 || std::env::var("GITHUB_ACTIONS").is_ok()
308 || std::env::var("GITLAB_CI").is_ok()
309 || std::env::var("CIRCLECI").is_ok()
310 || std::env::var("JENKINS_HOME").is_ok()
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 {
399 config_path: Option<PathBuf>,
400 log_level: Option<Level>,
401 working_dir: Option<PathBuf>,
402 redis_url: Option<String>,
403 postgres_url: Option<String>,
404 verbose: Option<bool>,
405}
406
407impl CliContextBuilder {
408 pub fn new() -> Self {
410 Self {
411 config_path: None,
412 log_level: None,
413 working_dir: None,
414 redis_url: None,
415 postgres_url: None,
416 verbose: None,
417 }
418 }
419
420 mecha10_core::builder_setter!(
421 assign, config_path, config_path, Option<PathBuf>
423 );
424
425 mecha10_core::builder_setter!(
426 into_some, log_level, log_level, Level
428 );
429
430 mecha10_core::builder_setter!(
431 into_some, working_dir, working_dir, PathBuf
433 );
434
435 mecha10_core::builder_setter!(
436 into_some, redis_url, redis_url, String
438 );
439
440 mecha10_core::builder_setter!(
441 assign, postgres_url, postgres_url, Option<String>
443 );
444
445 mecha10_core::builder_setter!(
446 into_some, verbose, verbose, bool
448 );
449
450 pub fn build(self) -> anyhow::Result<CliContext> {
452 let working_dir = self
454 .working_dir
455 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
456
457 let config_path = self
459 .config_path
460 .unwrap_or_else(|| working_dir.join(paths::PROJECT_CONFIG));
461
462 let config_redis_url = if config_path.exists() {
464 std::fs::read_to_string(&config_path)
465 .ok()
466 .and_then(|content| serde_json::from_str::<serde_json::Value>(&content).ok())
467 .and_then(|json| json.get("redis")?.get("url")?.as_str().map(String::from))
468 } else {
469 None
470 };
471
472 let redis_url = self
474 .redis_url
475 .or_else(|| std::env::var("MECHA10_REDIS_URL").ok())
476 .or_else(|| std::env::var("REDIS_URL").ok())
477 .or(config_redis_url)
478 .unwrap_or_else(|| "redis://localhost:6379".to_string());
479
480 let postgres_url = self
482 .postgres_url
483 .or_else(|| std::env::var("DATABASE_URL").ok())
484 .or_else(|| std::env::var("POSTGRES_URL").ok());
485
486 let log_level = self.log_level.unwrap_or(Level::INFO);
488
489 let verbose = self.verbose.unwrap_or(false);
491
492 Ok(CliContext {
493 config_path,
494 log_level,
495 working_dir,
496 redis_url,
497 postgres_url,
498 verbose,
499 redis_service: None,
500 project_service: None,
501 simulation_service: None,
502 process_service: None,
503 shared_process_service: None,
504 docker_service: None,
505 package_service: None,
506 deployment_service: None,
507 component_catalog_service: None,
508 discovery_service: None,
509 init_service: None,
510 project_template_service: None,
511 dev_service: None,
512 topology_service: None,
513 template_download_service: None,
514 _phantom: std::marker::PhantomData,
515 })
516 }
517}
518
519impl Default for CliContextBuilder {
520 fn default() -> Self {
521 Self::new()
522 }
523}