#![allow(dead_code)]
use crate::paths;
use crate::services::{
ComponentCatalogService, ConfigService, DeploymentService, DevService, DiscoveryService, DockerService,
InitService, PackageService, ProcessService, ProjectService, ProjectTemplateService, RedisService,
SimulationService, TemplateDownloadService, TopologyService,
};
use crate::types::ProjectConfig;
use std::path::PathBuf;
use tracing::Level;
pub struct CliContext {
pub config_path: PathBuf,
pub log_level: Level,
pub working_dir: PathBuf,
redis_url: String,
postgres_url: Option<String>,
verbose: bool,
redis_service: Option<RedisService>,
project_service: Option<ProjectService>,
simulation_service: Option<SimulationService>,
process_service: Option<ProcessService>,
shared_process_service: Option<std::sync::Arc<std::sync::Mutex<ProcessService>>>,
docker_service: Option<DockerService>,
package_service: Option<PackageService>,
deployment_service: Option<DeploymentService>,
component_catalog_service: Option<ComponentCatalogService>,
discovery_service: Option<DiscoveryService>,
init_service: Option<InitService>,
project_template_service: Option<ProjectTemplateService>,
dev_service: Option<DevService>,
topology_service: Option<TopologyService>,
template_download_service: Option<TemplateDownloadService>,
_phantom: std::marker::PhantomData<ProjectConfig>,
}
macro_rules! lazy_init_getter {
($(#[$meta:meta])* fn $name:ident() -> &$ty:ty, $field:ident, self |$self_:ident| $ctor:expr) => {
$(#[$meta])*
pub fn $name(&mut self) -> &$ty {
if self.$field.is_none() {
let value = {
let $self_ = &*self;
$ctor
};
self.$field = Some(value);
}
self.$field.as_ref().unwrap()
}
};
($(#[$meta:meta])* fn $name:ident() -> &$ty:ty, $field:ident, $ctor:expr) => {
$(#[$meta])*
pub fn $name(&mut self) -> &$ty {
if self.$field.is_none() {
self.$field = Some($ctor);
}
self.$field.as_ref().unwrap()
}
};
($(#[$meta:meta])* mut fn $name:ident() -> &mut $ty:ty, $field:ident, $ctor:expr) => {
$(#[$meta])*
pub fn $name(&mut self) -> &mut $ty {
if self.$field.is_none() {
self.$field = Some($ctor);
}
self.$field.as_mut().unwrap()
}
};
($(#[$meta:meta])* try fn $name:ident() -> &$ty:ty, $field:ident, |$self_:ident| $ctor:expr) => {
$(#[$meta])*
pub fn $name(&mut self) -> anyhow::Result<&$ty> {
if self.$field.is_none() {
let value = {
let $self_ = &*self;
$ctor
};
self.$field = Some(value);
}
Ok(self.$field.as_ref().unwrap())
}
};
}
impl CliContext {
pub async fn load_project_config(&self) -> anyhow::Result<ProjectConfig> {
ConfigService::load_from(&self.config_path).await
}
pub fn is_project_initialized(&self) -> bool {
ConfigService::is_initialized(&self.working_dir)
}
pub fn project_path(&self, path: &str) -> PathBuf {
self.working_dir.join(path)
}
pub fn is_verbose(&self) -> bool {
self.verbose
}
pub fn redis_url(&self) -> &str {
&self.redis_url
}
pub fn postgres_url(&self) -> Option<&str> {
self.postgres_url.as_deref()
}
lazy_init_getter!(
try fn redis() -> &RedisService, redis_service, |s| RedisService::new(&s.redis_url)?
);
lazy_init_getter!(
try fn project() -> &ProjectService, project_service, |s| ProjectService::detect(&s.working_dir)?
);
lazy_init_getter!(
fn simulation() -> &SimulationService, simulation_service, SimulationService::new()
);
lazy_init_getter!(
mut fn process() -> &mut ProcessService, process_service, ProcessService::new()
);
pub fn shared_process(&mut self) -> std::sync::Arc<std::sync::Mutex<ProcessService>> {
if self.shared_process_service.is_none() {
self.shared_process_service = Some(std::sync::Arc::new(std::sync::Mutex::new(ProcessService::new())));
}
self.shared_process_service.as_ref().unwrap().clone()
}
lazy_init_getter!(
fn docker() -> &DockerService, docker_service, DockerService::new()
);
pub fn package(&mut self) -> anyhow::Result<&PackageService> {
if self.package_service.is_none() {
let project = self.project()?;
let name = project.name()?;
let version = project.version()?;
self.package_service = Some(PackageService::new(name, version, self.working_dir.clone())?);
}
Ok(self.package_service.as_ref().unwrap())
}
lazy_init_getter!(
fn deployment() -> &DeploymentService, deployment_service, DeploymentService::new()
);
lazy_init_getter!(
fn component_catalog() -> &ComponentCatalogService, component_catalog_service, ComponentCatalogService::new()
);
lazy_init_getter!(
mut fn discovery() -> &mut DiscoveryService, discovery_service, DiscoveryService::new()
);
lazy_init_getter!(
fn init_service() -> &InitService, init_service, InitService::new()
);
lazy_init_getter!(
fn project_template_service() -> &ProjectTemplateService, project_template_service, ProjectTemplateService::new()
);
lazy_init_getter!(
fn dev() -> &DevService, dev_service, self |s| DevService::new(s.redis_url.clone())
);
lazy_init_getter!(
fn topology() -> &TopologyService, topology_service, self |s| TopologyService::new(s.working_dir.clone())
);
lazy_init_getter!(
fn template_download() -> &TemplateDownloadService, template_download_service, TemplateDownloadService::new()
);
pub fn validate_project_structure(&self) -> anyhow::Result<()> {
if !self.is_project_initialized() {
return Err(anyhow::anyhow!("Project not initialized. Run 'mecha10 init' first."));
}
let required_dirs = vec!["nodes", "drivers"];
for dir in required_dirs {
let path = self.project_path(dir);
if !path.exists() {
return Err(anyhow::anyhow!(
"Required directory missing: {}. Expected at: {}",
dir,
path.display()
));
}
}
Ok(())
}
pub fn is_ci(&self) -> bool {
is_ci_environment()
}
pub fn logs_dir(&self) -> PathBuf {
self.project_path("logs")
}
pub fn data_dir(&self) -> PathBuf {
self.project_path("data")
}
pub fn recordings_dir(&self) -> PathBuf {
self.project_path("data/recordings")
}
pub fn maps_dir(&self) -> PathBuf {
self.project_path("data/maps")
}
pub fn telemetry_dir(&self) -> PathBuf {
self.project_path("data/telemetry")
}
pub fn simulation_dir(&self) -> PathBuf {
self.project_path(paths::project::SIMULATION_DIR)
}
pub fn rl_tasks_dir(&self) -> PathBuf {
self.project_path(paths::project::RL_TASKS_DIR)
}
pub fn target_debug_dir(&self) -> PathBuf {
self.project_path("target/debug")
}
pub fn target_release_dir(&self) -> PathBuf {
self.project_path("target/release")
}
pub fn packages_dir(&self) -> PathBuf {
self.project_path("target/packages")
}
pub fn ensure_dir(&self, path: &PathBuf) -> anyhow::Result<()> {
if !path.exists() {
std::fs::create_dir_all(path)?;
}
Ok(())
}
}
impl Default for CliContext {
fn default() -> Self {
CliContextBuilder::new()
.log_level(Level::INFO)
.build()
.expect("Failed to build default CliContext")
}
}
pub struct CliContextBuilder {
config_path: Option<PathBuf>,
log_level: Option<Level>,
working_dir: Option<PathBuf>,
redis_url: Option<String>,
postgres_url: Option<String>,
verbose: Option<bool>,
}
impl CliContextBuilder {
pub fn new() -> Self {
Self {
config_path: None,
log_level: None,
working_dir: None,
redis_url: None,
postgres_url: None,
verbose: None,
}
}
mecha10_core::builder_setter!(
assign, config_path, config_path, Option<PathBuf>
);
mecha10_core::builder_setter!(
into_some, log_level, log_level, Level
);
mecha10_core::builder_setter!(
into_some, working_dir, working_dir, PathBuf
);
mecha10_core::builder_setter!(
into_some, redis_url, redis_url, String
);
mecha10_core::builder_setter!(
assign, postgres_url, postgres_url, Option<String>
);
mecha10_core::builder_setter!(
into_some, verbose, verbose, bool
);
pub fn build(self) -> anyhow::Result<CliContext> {
let working_dir = self
.working_dir
.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
let config_path = self
.config_path
.unwrap_or_else(|| working_dir.join(paths::PROJECT_CONFIG));
let config_redis_url = if config_path.exists() {
std::fs::read_to_string(&config_path)
.ok()
.and_then(|content| serde_json::from_str::<serde_json::Value>(&content).ok())
.and_then(|json| json.get("redis")?.get("url")?.as_str().map(String::from))
} else {
None
};
let redis_url = self
.redis_url
.or_else(|| std::env::var("MECHA10_REDIS_URL").ok())
.or_else(|| std::env::var("REDIS_URL").ok())
.or(config_redis_url)
.unwrap_or_else(|| "redis://localhost:6379".to_string());
let postgres_url = self
.postgres_url
.or_else(|| std::env::var("DATABASE_URL").ok())
.or_else(|| std::env::var("POSTGRES_URL").ok());
let log_level = self.log_level.unwrap_or(Level::INFO);
let verbose = self.verbose.unwrap_or(false);
Ok(CliContext {
config_path,
log_level,
working_dir,
redis_url,
postgres_url,
verbose,
redis_service: None,
project_service: None,
simulation_service: None,
process_service: None,
shared_process_service: None,
docker_service: None,
package_service: None,
deployment_service: None,
component_catalog_service: None,
discovery_service: None,
init_service: None,
project_template_service: None,
dev_service: None,
topology_service: None,
template_download_service: None,
_phantom: std::marker::PhantomData,
})
}
}
impl Default for CliContextBuilder {
fn default() -> Self {
Self::new()
}
}
pub fn is_ci_environment() -> bool {
std::env::var("CI").is_ok()
|| std::env::var("GITHUB_ACTIONS").is_ok()
|| std::env::var("GITLAB_CI").is_ok()
|| std::env::var("CIRCLECI").is_ok()
|| std::env::var("JENKINS_HOME").is_ok()
}