1#![allow(dead_code)]
2
3use crate::paths;
9use crate::services::{
10 AssetsDownloadService, ComponentCatalogService, ConfigService, DeploymentService, DevService, DiscoveryService,
11 DockerService, 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 {
19 pub config_path: PathBuf,
20
21 pub log_level: Level,
22
23 pub working_dir: PathBuf,
24
25 redis_url: String,
26
27 postgres_url: Option<String>,
28
29 verbose: bool,
30
31 redis_service: Option<RedisService>,
32 project_service: Option<ProjectService>,
33 simulation_service: Option<SimulationService>,
34 process_service: Option<ProcessService>,
35 shared_process_service: Option<std::sync::Arc<std::sync::Mutex<ProcessService>>>,
36 docker_service: Option<DockerService>,
37 package_service: Option<PackageService>,
38 deployment_service: Option<DeploymentService>,
39 component_catalog_service: Option<ComponentCatalogService>,
40 discovery_service: Option<DiscoveryService>,
41 init_service: Option<InitService>,
42 project_template_service: Option<ProjectTemplateService>,
43 dev_service: Option<DevService>,
44 topology_service: Option<TopologyService>,
45 template_download_service: Option<TemplateDownloadService>,
46 assets_download_service: Option<AssetsDownloadService>,
47
48 _phantom: std::marker::PhantomData<ProjectConfig>,
49}
50
51macro_rules! lazy_init_getter {
52 ($(#[$meta:meta])* fn $name:ident() -> &$ty:ty, $field:ident, self |$self_:ident| $ctor:expr) => {
53 $(#[$meta])*
54 pub fn $name(&mut self) -> &$ty {
55 if self.$field.is_none() {
56 let value = {
57 let $self_ = &*self;
58 $ctor
59 };
60 self.$field = Some(value);
61 }
62 self.$field.as_ref().unwrap()
63 }
64 };
65 ($(#[$meta:meta])* fn $name:ident() -> &$ty:ty, $field:ident, $ctor:expr) => {
66 $(#[$meta])*
67 pub fn $name(&mut self) -> &$ty {
68 if self.$field.is_none() {
69 self.$field = Some($ctor);
70 }
71 self.$field.as_ref().unwrap()
72 }
73 };
74 ($(#[$meta:meta])* mut fn $name:ident() -> &mut $ty:ty, $field:ident, $ctor:expr) => {
75 $(#[$meta])*
76 pub fn $name(&mut self) -> &mut $ty {
77 if self.$field.is_none() {
78 self.$field = Some($ctor);
79 }
80 self.$field.as_mut().unwrap()
81 }
82 };
83 ($(#[$meta:meta])* try fn $name:ident() -> &$ty:ty, $field:ident, |$self_:ident| $ctor:expr) => {
84 $(#[$meta])*
85 pub fn $name(&mut self) -> anyhow::Result<&$ty> {
86 if self.$field.is_none() {
87 let value = {
88 let $self_ = &*self;
89 $ctor
90 };
91 self.$field = Some(value);
92 }
93 Ok(self.$field.as_ref().unwrap())
94 }
95 };
96}
97
98impl CliContext {
99 pub async fn load_project_config(&self) -> anyhow::Result<ProjectConfig> {
105 ConfigService::load_from(&self.config_path).await
106 }
107
108 pub fn is_project_initialized(&self) -> bool {
110 ConfigService::is_initialized(&self.working_dir)
111 }
112
113 pub fn project_path(&self, path: &str) -> PathBuf {
115 self.working_dir.join(path)
116 }
117
118 pub fn is_verbose(&self) -> bool {
120 self.verbose
121 }
122
123 pub fn redis_url(&self) -> &str {
125 &self.redis_url
126 }
127
128 pub fn postgres_url(&self) -> Option<&str> {
130 self.postgres_url.as_deref()
131 }
132
133 lazy_init_getter!(
134 try fn redis() -> &RedisService, redis_service, |s| RedisService::new(&s.redis_url)?
136 );
137
138 lazy_init_getter!(
139 try fn project() -> &ProjectService, project_service, |s| ProjectService::detect(&s.working_dir)?
141 );
142
143 lazy_init_getter!(
144 fn simulation() -> &SimulationService, simulation_service, SimulationService::new()
146 );
147
148 lazy_init_getter!(
149 mut fn process() -> &mut ProcessService, process_service, ProcessService::new()
151 );
152
153 pub fn shared_process(&mut self) -> std::sync::Arc<std::sync::Mutex<ProcessService>> {
157 if self.shared_process_service.is_none() {
158 self.shared_process_service = Some(std::sync::Arc::new(std::sync::Mutex::new(ProcessService::new())));
159 }
160 self.shared_process_service.as_ref().unwrap().clone()
161 }
162
163 lazy_init_getter!(
164 fn docker() -> &DockerService, docker_service, DockerService::new()
166 );
167
168 pub fn package(&mut self) -> anyhow::Result<&PackageService> {
172 if self.package_service.is_none() {
173 let project = self.project()?;
174 let name = project.name()?;
175 let version = project.version()?;
176 self.package_service = Some(PackageService::new(name, version, self.working_dir.clone())?);
177 }
178 Ok(self.package_service.as_ref().unwrap())
179 }
180
181 lazy_init_getter!(
182 fn deployment() -> &DeploymentService, deployment_service, DeploymentService::new()
184 );
185
186 lazy_init_getter!(
187 fn component_catalog() -> &ComponentCatalogService, component_catalog_service, ComponentCatalogService::new()
189 );
190
191 lazy_init_getter!(
192 mut fn discovery() -> &mut DiscoveryService, discovery_service, DiscoveryService::new()
194 );
195
196 lazy_init_getter!(
197 fn init_service() -> &InitService, init_service, InitService::new()
199 );
200
201 lazy_init_getter!(
202 fn project_template_service() -> &ProjectTemplateService, project_template_service, ProjectTemplateService::new()
204 );
205
206 lazy_init_getter!(
207 fn dev() -> &DevService, dev_service, self |s| DevService::new(s.redis_url.clone())
209 );
210
211 lazy_init_getter!(
212 fn topology() -> &TopologyService, topology_service, self |s| TopologyService::new(s.working_dir.clone())
217 );
218
219 lazy_init_getter!(
220 fn template_download() -> &TemplateDownloadService, template_download_service, TemplateDownloadService::new()
225 );
226
227 lazy_init_getter!(
228 fn assets_download() -> &AssetsDownloadService, assets_download_service, AssetsDownloadService::new()
234 );
235
236 pub fn validate_project_structure(&self) -> anyhow::Result<()> {
241 if !self.is_project_initialized() {
242 return Err(anyhow::anyhow!("Project not initialized. Run 'mecha10 init' first."));
243 }
244
245 let required_dirs = vec!["nodes", "drivers"];
246 for dir in required_dirs {
247 let path = self.project_path(dir);
248 if !path.exists() {
249 return Err(anyhow::anyhow!(
250 "Required directory missing: {}. Expected at: {}",
251 dir,
252 path.display()
253 ));
254 }
255 }
256
257 Ok(())
258 }
259
260 pub fn is_ci(&self) -> bool {
262 is_ci_environment()
263 }
264
265 pub fn logs_dir(&self) -> PathBuf {
267 self.project_path("logs")
268 }
269
270 pub fn data_dir(&self) -> PathBuf {
272 self.project_path("data")
273 }
274
275 pub fn recordings_dir(&self) -> PathBuf {
277 self.project_path("data/recordings")
278 }
279
280 pub fn maps_dir(&self) -> PathBuf {
282 self.project_path("data/maps")
283 }
284
285 pub fn telemetry_dir(&self) -> PathBuf {
287 self.project_path("data/telemetry")
288 }
289
290 pub fn simulation_dir(&self) -> PathBuf {
292 self.project_path(paths::project::SIMULATION_DIR)
293 }
294
295 pub fn rl_tasks_dir(&self) -> PathBuf {
299 self.project_path(paths::project::RL_TASKS_DIR)
300 }
301
302 pub fn target_debug_dir(&self) -> PathBuf {
304 self.project_path("target/debug")
305 }
306
307 pub fn target_release_dir(&self) -> PathBuf {
309 self.project_path("target/release")
310 }
311
312 pub fn packages_dir(&self) -> PathBuf {
314 self.project_path("target/packages")
315 }
316
317 pub fn ensure_dir(&self, path: &PathBuf) -> anyhow::Result<()> {
319 if !path.exists() {
320 std::fs::create_dir_all(path)?;
321 }
322 Ok(())
323 }
324}
325
326impl Default for CliContext {
327 fn default() -> Self {
328 CliContextBuilder::new()
329 .log_level(Level::INFO)
330 .build()
331 .expect("Failed to build default CliContext")
332 }
333}
334
335pub struct CliContextBuilder {
336 config_path: Option<PathBuf>,
337 log_level: Option<Level>,
338 working_dir: Option<PathBuf>,
339 redis_url: Option<String>,
340 postgres_url: Option<String>,
341 verbose: Option<bool>,
342}
343
344impl CliContextBuilder {
345 pub fn new() -> Self {
347 Self {
348 config_path: None,
349 log_level: None,
350 working_dir: None,
351 redis_url: None,
352 postgres_url: None,
353 verbose: None,
354 }
355 }
356
357 mecha10_core::builder_setter!(
358 assign, config_path, config_path, Option<PathBuf>
360 );
361
362 mecha10_core::builder_setter!(
363 into_some, log_level, log_level, Level
365 );
366
367 mecha10_core::builder_setter!(
368 into_some, working_dir, working_dir, PathBuf
370 );
371
372 mecha10_core::builder_setter!(
373 into_some, redis_url, redis_url, String
375 );
376
377 mecha10_core::builder_setter!(
378 assign, postgres_url, postgres_url, Option<String>
380 );
381
382 mecha10_core::builder_setter!(
383 into_some, verbose, verbose, bool
385 );
386
387 pub fn build(self) -> anyhow::Result<CliContext> {
389 let working_dir = self
390 .working_dir
391 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
392
393 let config_path = self
394 .config_path
395 .unwrap_or_else(|| working_dir.join(paths::PROJECT_CONFIG));
396
397 let config_redis_url = if config_path.exists() {
398 std::fs::read_to_string(&config_path)
399 .ok()
400 .and_then(|content| serde_json::from_str::<serde_json::Value>(&content).ok())
401 .and_then(|json| json.get("redis")?.get("url")?.as_str().map(String::from))
402 } else {
403 None
404 };
405
406 let redis_url = self
407 .redis_url
408 .or_else(|| std::env::var("MECHA10_REDIS_URL").ok())
409 .or_else(|| std::env::var("REDIS_URL").ok())
410 .or(config_redis_url)
411 .unwrap_or_else(|| "redis://localhost:6379".to_string());
412
413 let postgres_url = self
414 .postgres_url
415 .or_else(|| std::env::var("DATABASE_URL").ok())
416 .or_else(|| std::env::var("POSTGRES_URL").ok());
417
418 let log_level = self.log_level.unwrap_or(Level::INFO);
419
420 let verbose = self.verbose.unwrap_or(false);
421
422 Ok(CliContext {
423 config_path,
424 log_level,
425 working_dir,
426 redis_url,
427 postgres_url,
428 verbose,
429 redis_service: None,
430 project_service: None,
431 simulation_service: None,
432 process_service: None,
433 shared_process_service: None,
434 docker_service: None,
435 package_service: None,
436 deployment_service: None,
437 component_catalog_service: None,
438 discovery_service: None,
439 init_service: None,
440 project_template_service: None,
441 dev_service: None,
442 topology_service: None,
443 template_download_service: None,
444 assets_download_service: None,
445 _phantom: std::marker::PhantomData,
446 })
447 }
448}
449
450impl Default for CliContextBuilder {
451 fn default() -> Self {
452 Self::new()
453 }
454}
455
456pub fn is_ci_environment() -> bool {
460 std::env::var("CI").is_ok()
461 || std::env::var("GITHUB_ACTIONS").is_ok()
462 || std::env::var("GITLAB_CI").is_ok()
463 || std::env::var("CIRCLECI").is_ok()
464 || std::env::var("JENKINS_HOME").is_ok()
465}