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 {
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
47 _phantom: std::marker::PhantomData<ProjectConfig>,
48}
49
50macro_rules! lazy_init_getter {
51 ($(#[$meta:meta])* fn $name:ident() -> &$ty:ty, $field:ident, self |$self_:ident| $ctor:expr) => {
52 $(#[$meta])*
53 pub fn $name(&mut self) -> &$ty {
54 if self.$field.is_none() {
55 let value = {
56 let $self_ = &*self;
57 $ctor
58 };
59 self.$field = Some(value);
60 }
61 self.$field.as_ref().unwrap()
62 }
63 };
64 ($(#[$meta:meta])* fn $name:ident() -> &$ty:ty, $field:ident, $ctor:expr) => {
65 $(#[$meta])*
66 pub fn $name(&mut self) -> &$ty {
67 if self.$field.is_none() {
68 self.$field = Some($ctor);
69 }
70 self.$field.as_ref().unwrap()
71 }
72 };
73 ($(#[$meta:meta])* mut fn $name:ident() -> &mut $ty:ty, $field:ident, $ctor:expr) => {
74 $(#[$meta])*
75 pub fn $name(&mut self) -> &mut $ty {
76 if self.$field.is_none() {
77 self.$field = Some($ctor);
78 }
79 self.$field.as_mut().unwrap()
80 }
81 };
82 ($(#[$meta:meta])* try fn $name:ident() -> &$ty:ty, $field:ident, |$self_:ident| $ctor:expr) => {
83 $(#[$meta])*
84 pub fn $name(&mut self) -> anyhow::Result<&$ty> {
85 if self.$field.is_none() {
86 let value = {
87 let $self_ = &*self;
88 $ctor
89 };
90 self.$field = Some(value);
91 }
92 Ok(self.$field.as_ref().unwrap())
93 }
94 };
95}
96
97impl CliContext {
98 pub async fn load_project_config(&self) -> anyhow::Result<ProjectConfig> {
104 ConfigService::load_from(&self.config_path).await
105 }
106
107 pub fn is_project_initialized(&self) -> bool {
109 ConfigService::is_initialized(&self.working_dir)
110 }
111
112 pub fn project_path(&self, path: &str) -> PathBuf {
114 self.working_dir.join(path)
115 }
116
117 pub fn is_verbose(&self) -> bool {
119 self.verbose
120 }
121
122 pub fn redis_url(&self) -> &str {
124 &self.redis_url
125 }
126
127 pub fn postgres_url(&self) -> Option<&str> {
129 self.postgres_url.as_deref()
130 }
131
132 lazy_init_getter!(
133 try fn redis() -> &RedisService, redis_service, |s| RedisService::new(&s.redis_url)?
135 );
136
137 lazy_init_getter!(
138 try fn project() -> &ProjectService, project_service, |s| ProjectService::detect(&s.working_dir)?
140 );
141
142 lazy_init_getter!(
143 fn simulation() -> &SimulationService, simulation_service, SimulationService::new()
145 );
146
147 lazy_init_getter!(
148 mut fn process() -> &mut ProcessService, process_service, ProcessService::new()
150 );
151
152 pub fn shared_process(&mut self) -> std::sync::Arc<std::sync::Mutex<ProcessService>> {
158 if self.shared_process_service.is_none() {
159 self.shared_process_service = Some(std::sync::Arc::new(std::sync::Mutex::new(ProcessService::new())));
160 }
161 self.shared_process_service.as_ref().unwrap().clone()
162 }
163
164 lazy_init_getter!(
165 fn docker() -> &DockerService, docker_service, DockerService::new()
167 );
168
169 pub fn package(&mut self) -> anyhow::Result<&PackageService> {
173 if self.package_service.is_none() {
174 let project = self.project()?;
175 let name = project.name()?;
176 let version = project.version()?;
177 self.package_service = Some(PackageService::new(name, version, self.working_dir.clone())?);
178 }
179 Ok(self.package_service.as_ref().unwrap())
180 }
181
182 lazy_init_getter!(
183 fn deployment() -> &DeploymentService, deployment_service, DeploymentService::new()
185 );
186
187 lazy_init_getter!(
188 fn component_catalog() -> &ComponentCatalogService, component_catalog_service, ComponentCatalogService::new()
190 );
191
192 lazy_init_getter!(
193 mut fn discovery() -> &mut DiscoveryService, discovery_service, DiscoveryService::new()
195 );
196
197 lazy_init_getter!(
198 fn init_service() -> &InitService, init_service, InitService::new()
200 );
201
202 lazy_init_getter!(
203 fn project_template_service() -> &ProjectTemplateService, project_template_service, ProjectTemplateService::new()
205 );
206
207 lazy_init_getter!(
208 fn dev() -> &DevService, dev_service, self |s| DevService::new(s.redis_url.clone())
210 );
211
212 lazy_init_getter!(
213 fn topology() -> &TopologyService, topology_service, self |s| TopologyService::new(s.working_dir.clone())
218 );
219
220 lazy_init_getter!(
221 fn template_download() -> &TemplateDownloadService, template_download_service, TemplateDownloadService::new()
226 );
227
228 pub fn validate_project_structure(&self) -> anyhow::Result<()> {
233 if !self.is_project_initialized() {
234 return Err(anyhow::anyhow!("Project not initialized. Run 'mecha10 init' first."));
235 }
236
237 let required_dirs = vec!["nodes", "drivers"];
238 for dir in required_dirs {
239 let path = self.project_path(dir);
240 if !path.exists() {
241 return Err(anyhow::anyhow!(
242 "Required directory missing: {}. Expected at: {}",
243 dir,
244 path.display()
245 ));
246 }
247 }
248
249 Ok(())
250 }
251
252 pub fn is_ci(&self) -> bool {
254 is_ci_environment()
255 }
256
257 pub fn logs_dir(&self) -> PathBuf {
259 self.project_path("logs")
260 }
261
262 pub fn data_dir(&self) -> PathBuf {
264 self.project_path("data")
265 }
266
267 pub fn recordings_dir(&self) -> PathBuf {
269 self.project_path("data/recordings")
270 }
271
272 pub fn maps_dir(&self) -> PathBuf {
274 self.project_path("data/maps")
275 }
276
277 pub fn telemetry_dir(&self) -> PathBuf {
279 self.project_path("data/telemetry")
280 }
281
282 pub fn simulation_dir(&self) -> PathBuf {
284 self.project_path(paths::project::SIMULATION_DIR)
285 }
286
287 pub fn rl_tasks_dir(&self) -> PathBuf {
291 self.project_path(paths::project::RL_TASKS_DIR)
292 }
293
294 pub fn target_debug_dir(&self) -> PathBuf {
296 self.project_path("target/debug")
297 }
298
299 pub fn target_release_dir(&self) -> PathBuf {
301 self.project_path("target/release")
302 }
303
304 pub fn packages_dir(&self) -> PathBuf {
306 self.project_path("target/packages")
307 }
308
309 pub fn ensure_dir(&self, path: &PathBuf) -> anyhow::Result<()> {
311 if !path.exists() {
312 std::fs::create_dir_all(path)?;
313 }
314 Ok(())
315 }
316}
317
318impl Default for CliContext {
319 fn default() -> Self {
320 CliContextBuilder::new()
321 .log_level(Level::INFO)
322 .build()
323 .expect("Failed to build default CliContext")
324 }
325}
326
327pub struct CliContextBuilder {
328 config_path: Option<PathBuf>,
329 log_level: Option<Level>,
330 working_dir: Option<PathBuf>,
331 redis_url: Option<String>,
332 postgres_url: Option<String>,
333 verbose: Option<bool>,
334}
335
336impl CliContextBuilder {
337 pub fn new() -> Self {
339 Self {
340 config_path: None,
341 log_level: None,
342 working_dir: None,
343 redis_url: None,
344 postgres_url: None,
345 verbose: None,
346 }
347 }
348
349 mecha10_core::builder_setter!(
350 assign, config_path, config_path, Option<PathBuf>
352 );
353
354 mecha10_core::builder_setter!(
355 into_some, log_level, log_level, Level
357 );
358
359 mecha10_core::builder_setter!(
360 into_some, working_dir, working_dir, PathBuf
362 );
363
364 mecha10_core::builder_setter!(
365 into_some, redis_url, redis_url, String
367 );
368
369 mecha10_core::builder_setter!(
370 assign, postgres_url, postgres_url, Option<String>
372 );
373
374 mecha10_core::builder_setter!(
375 into_some, verbose, verbose, bool
377 );
378
379 pub fn build(self) -> anyhow::Result<CliContext> {
381 let working_dir = self
382 .working_dir
383 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
384
385 let config_path = self
386 .config_path
387 .unwrap_or_else(|| working_dir.join(paths::PROJECT_CONFIG));
388
389 let config_redis_url = if config_path.exists() {
390 std::fs::read_to_string(&config_path)
391 .ok()
392 .and_then(|content| serde_json::from_str::<serde_json::Value>(&content).ok())
393 .and_then(|json| json.get("redis")?.get("url")?.as_str().map(String::from))
394 } else {
395 None
396 };
397
398 let redis_url = self
399 .redis_url
400 .or_else(|| std::env::var("MECHA10_REDIS_URL").ok())
401 .or_else(|| std::env::var("REDIS_URL").ok())
402 .or(config_redis_url)
403 .unwrap_or_else(|| "redis://localhost:6379".to_string());
404
405 let postgres_url = self
406 .postgres_url
407 .or_else(|| std::env::var("DATABASE_URL").ok())
408 .or_else(|| std::env::var("POSTGRES_URL").ok());
409
410 let log_level = self.log_level.unwrap_or(Level::INFO);
411
412 let verbose = self.verbose.unwrap_or(false);
413
414 Ok(CliContext {
415 config_path,
416 log_level,
417 working_dir,
418 redis_url,
419 postgres_url,
420 verbose,
421 redis_service: None,
422 project_service: None,
423 simulation_service: None,
424 process_service: None,
425 shared_process_service: None,
426 docker_service: None,
427 package_service: None,
428 deployment_service: None,
429 component_catalog_service: None,
430 discovery_service: None,
431 init_service: None,
432 project_template_service: None,
433 dev_service: None,
434 topology_service: None,
435 template_download_service: None,
436 _phantom: std::marker::PhantomData,
437 })
438 }
439}
440
441impl Default for CliContextBuilder {
442 fn default() -> Self {
443 Self::new()
444 }
445}
446
447pub fn is_ci_environment() -> bool {
454 std::env::var("CI").is_ok()
455 || std::env::var("GITHUB_ACTIONS").is_ok()
456 || std::env::var("GITLAB_CI").is_ok()
457 || std::env::var("CIRCLECI").is_ok()
458 || std::env::var("JENKINS_HOME").is_ok()
459}