#![allow(dead_code)]
use super::types::{InfrastructureAction, InfrastructureArgs};
use crate::context::CliContext;
use crate::services::{ConfigService, DockerService, ProjectService};
use anyhow::Result;
pub async fn handle_infrastructure(ctx: &mut CliContext, args: &InfrastructureArgs) -> Result<()> {
println!();
let docker = ctx.docker();
if docker.check_installation().is_err() || docker.check_daemon().is_err() {
anyhow::bail!("Docker is not available. Please install Docker and start the daemon.");
}
let current_dir = std::env::current_dir()?;
match ProjectService::detect(¤t_dir) {
Ok(project) => {
println!("🤖 Project Infrastructure - {}", project.name()?);
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!();
handle_project_infrastructure(ctx, args, &project).await?;
}
Err(_) => {
println!("🔧 Control Plane Infrastructure");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!();
handle_control_plane_infrastructure(docker, args).await?;
}
}
Ok(())
}
async fn handle_project_infrastructure(
_ctx: &mut CliContext,
args: &InfrastructureArgs,
project: &ProjectService,
) -> Result<()> {
let config = ConfigService::load_from(&project.config_path()).await?;
let compose_path = project.root().join(&config.docker.compose_file);
if !compose_path.exists() {
anyhow::bail!("Docker Compose file not found: {}", compose_path.display());
}
let docker = DockerService::with_compose_file(compose_path.to_string_lossy().to_string());
let services_to_manage = if args.services.is_empty() {
config.docker.robot.clone()
} else {
args.services.clone()
};
match args.action {
InfrastructureAction::Start => handle_start_project(&docker, &services_to_manage).await?,
InfrastructureAction::Stop => handle_stop(&docker, &services_to_manage).await?,
InfrastructureAction::Restart => handle_restart(&docker, &services_to_manage).await?,
InfrastructureAction::Status => handle_status(&docker, &services_to_manage).await?,
InfrastructureAction::Logs => handle_logs(&docker, &args.services).await?,
}
Ok(())
}
async fn handle_control_plane_infrastructure(docker: &DockerService, args: &InfrastructureArgs) -> Result<()> {
match args.action {
InfrastructureAction::Start => handle_start(docker, &args.services).await?,
InfrastructureAction::Stop => handle_stop(docker, &args.services).await?,
InfrastructureAction::Restart => handle_restart(docker, &args.services).await?,
InfrastructureAction::Status => handle_status(docker, &args.services).await?,
InfrastructureAction::Logs => handle_logs(docker, &args.services).await?,
}
Ok(())
}
async fn run_infrastructure_action<Bulk, BulkFut, PerService, PerServiceFut>(
services: &[String],
verb_ing: &str,
verb_ed: &str,
bulk: Bulk,
per_service: PerService,
) -> Result<()>
where
Bulk: FnOnce() -> BulkFut,
BulkFut: std::future::Future<Output = Result<()>>,
PerService: Fn(&str) -> PerServiceFut,
PerServiceFut: std::future::Future<Output = Result<()>>,
{
println!("{} infrastructure services...\n", verb_ing);
if services.is_empty() {
bulk().await?;
println!("✅ All services {}\n", verb_ed);
} else {
for service in services {
println!("{} {}...", verb_ing, service);
per_service(service).await?;
println!("✅ {} {}", service, verb_ed);
}
println!();
}
Ok(())
}
async fn handle_start(docker: &DockerService, services: &[String]) -> Result<()> {
run_infrastructure_action(
services,
"Starting",
"started",
|| docker.compose_up(true),
|service: &str| {
let service = service.to_string();
async move { docker.compose_up_service(&service, true).await }
},
)
.await
}
async fn handle_start_project(docker: &DockerService, services: &[String]) -> Result<()> {
if services.is_empty() {
println!("No services configured\n");
return Ok(());
}
println!("Starting robot infrastructure...\n");
for service in services {
println!(" Starting {}...", service);
docker.compose_up_service(service, true).await?;
println!(" ✅ {} started", service);
}
println!("\n✅ All services started successfully\n");
println!("💡 Tip: Use 'mecha10 infrastructure status' to check service health");
Ok(())
}
async fn handle_stop(docker: &DockerService, services: &[String]) -> Result<()> {
run_infrastructure_action(
services,
"Stopping",
"stopped",
|| docker.compose_down(),
|service: &str| {
let service = service.to_string();
async move { docker.compose_stop(Some(&service)).await }
},
)
.await
}
async fn handle_restart(docker: &DockerService, services: &[String]) -> Result<()> {
run_infrastructure_action(
services,
"Restarting",
"restarted",
|| async {
docker.compose_down().await?;
docker.compose_up(true).await
},
|service: &str| {
let service = service.to_string();
async move { docker.compose_restart(Some(&service)).await }
},
)
.await
}
async fn handle_status(docker: &DockerService, services: &[String]) -> Result<()> {
println!("Infrastructure Status:\n");
let containers = docker.list_containers().await?;
if services.is_empty() {
for container in &containers {
let running = container.status.to_lowercase().contains("up");
let status = if running { "✅ Running" } else { "⬜ Stopped" };
println!(" {} {} ({})", status, container.name, container.status);
}
} else {
for service in services {
let container = containers.iter().find(|c| c.name.contains(service));
let running = container
.map(|c| c.status.to_lowercase().contains("up"))
.unwrap_or(false);
let status = if running { "✅ Running" } else { "⬜ Stopped" };
let detail = container.map(|c| c.status.as_str()).unwrap_or("Not found");
println!(" {} {} ({})", status, service, detail);
}
}
println!();
Ok(())
}
async fn handle_logs(docker: &DockerService, services: &[String]) -> Result<()> {
if services.is_empty() {
anyhow::bail!("Please specify a service to view logs.\nExample: mecha10 infrastructure logs redis");
}
let service = &services[0];
println!("Viewing logs for: {}\n", service);
docker.compose_logs(Some(service), false, Some(50))?;
Ok(())
}
#[cfg(test)]
mod run_infrastructure_action_tests {
use super::run_infrastructure_action;
use std::sync::atomic::{AtomicUsize, Ordering};
#[tokio::test]
async fn empty_services_runs_the_bulk_action_once_and_never_touches_per_service() {
let bulk_calls = AtomicUsize::new(0);
let per_service_calls = AtomicUsize::new(0);
run_infrastructure_action(
&[],
"Starting",
"started",
|| async {
bulk_calls.fetch_add(1, Ordering::SeqCst);
Ok(())
},
|_service| async {
per_service_calls.fetch_add(1, Ordering::SeqCst);
Ok(())
},
)
.await
.unwrap();
assert_eq!(bulk_calls.load(Ordering::SeqCst), 1);
assert_eq!(per_service_calls.load(Ordering::SeqCst), 0);
}
#[tokio::test]
async fn non_empty_services_runs_the_per_service_action_for_each_and_never_touches_bulk() {
let bulk_calls = AtomicUsize::new(0);
let seen = std::sync::Mutex::new(Vec::new());
let services = vec!["redis".to_string(), "postgres".to_string()];
run_infrastructure_action(
&services,
"Stopping",
"stopped",
|| async {
bulk_calls.fetch_add(1, Ordering::SeqCst);
Ok(())
},
|service| {
seen.lock().unwrap().push(service.to_string());
async { Ok(()) }
},
)
.await
.unwrap();
assert_eq!(bulk_calls.load(Ordering::SeqCst), 0);
assert_eq!(*seen.lock().unwrap(), vec!["redis".to_string(), "postgres".to_string()]);
}
#[tokio::test]
async fn propagates_a_per_service_error_and_stops_early() {
let seen = std::sync::Mutex::new(Vec::new());
let services = vec!["redis".to_string(), "postgres".to_string()];
let result = run_infrastructure_action(
&services,
"Restarting",
"restarted",
|| async { Ok(()) },
|service| {
seen.lock().unwrap().push(service.to_string());
async { anyhow::bail!("boom") }
},
)
.await;
assert!(result.is_err());
assert_eq!(*seen.lock().unwrap(), vec!["redis".to_string()]);
}
}