use crate::commands::dev::DevArgs;
use crate::context::CliContext;
use crate::dev::lifecycle_adapter::CliLifecycleManager;
use crate::dev::node_selection::NodeToRun;
use crate::services::dev::SimulationPaths;
use crate::types::ProjectConfig;
use crate::ui::{dev_banners, LogBuffer};
use anyhow::Result;
use std::collections::HashMap;
use super::command_listener::{CommandListener, CommandListenerConfig};
use super::ops;
pub struct DevSession {
pub project_name: String,
#[allow(dead_code)]
pub project_version: String,
pub config: ProjectConfig,
pub nodes: Vec<NodeToRun>,
pub pids: HashMap<String, u32>,
pub godot_path: Option<String>,
pub sim_paths: Option<SimulationPaths>,
pub should_build: bool,
pub watch_mode: bool,
pub cli_nodes_specified: bool,
pub lifecycle: Option<CliLifecycleManager>,
pub log_buffer: LogBuffer,
}
impl DevSession {
pub async fn initialize(ctx: &mut CliContext, args: &DevArgs) -> Result<Self> {
dev_banners::print_header();
let project_root = std::env::current_dir()?;
crate::dev::validation::check_project_initialized(&project_root)?;
use crate::services::DevService;
let (project_name, project_version, config) = DevService::load_project_info(ctx).await?;
dev_banners::print_project_info(&project_name, &project_version);
let nodes = crate::dev::node_selection::get_nodes_to_run(&args.nodes, &config);
if !args.nodes.is_empty() {
let available = crate::dev::node_selection::get_available_node_names(&config);
crate::dev::validation::validate_node_names(&args.nodes, &available)?;
}
if nodes.is_empty() {
dev_banners::print_no_nodes()?;
return Err(anyhow::anyhow!("No nodes configured to run"));
}
dev_banners::print_nodes(&nodes);
let godot_path = ctx.simulation().validate_godot().ok().map(|info| info.path);
let sim_paths = DevService::resolve_simulation_paths(ctx, &config)?;
let mut lifecycle = CliLifecycleManager::from_project_config(&config);
if let Some(ref mut lifecycle_mgr) = lifecycle {
if lifecycle_mgr.current_mode() != "dev" && lifecycle_mgr.has_mode("dev") {
tracing::info!(
"Switching from '{}' to 'dev' mode for development",
lifecycle_mgr.current_mode()
);
let _ = lifecycle_mgr.change_mode("dev");
}
tracing::info!(
"Lifecycle management enabled. Mode: {}",
lifecycle_mgr.current_mode()
);
}
let log_buffer = LogBuffer::new(1000);
Ok(Self {
project_name,
project_version,
config,
nodes,
pids: HashMap::new(),
godot_path,
sim_paths,
should_build: !args.no_build,
watch_mode: args.watch,
cli_nodes_specified: !args.nodes.is_empty(),
lifecycle,
log_buffer,
})
}
pub async fn setup_infrastructure(&self, ctx: &mut CliContext) -> Result<()> {
if self.config.docker.auto_start {
ops::start_docker_services(ctx, &self.config, &self.project_name).await?;
}
ops::setup_redis_connection(ctx).await?;
Ok(())
}
pub fn build_if_needed(&self, ctx: &mut CliContext) -> Result<()> {
if self.should_build {
ops::build_nodes(ctx)?;
}
Ok(())
}
pub async fn spawn_nodes(&mut self, ctx: &mut CliContext) -> Result<()> {
let lifecycle = self.lifecycle.as_mut().ok_or_else(|| {
anyhow::anyhow!(
"Lifecycle configuration is required. Please add a 'lifecycle' section to your mecha10.json"
)
})?;
let nodes_to_spawn: Vec<String> = if self.cli_nodes_specified {
tracing::info!(
"CLI specified nodes: {:?} (overriding lifecycle mode '{}')",
self.nodes.iter().map(|n| &n.name).collect::<Vec<_>>(),
lifecycle.current_mode()
);
self.nodes.iter().map(|n| n.name.clone()).collect()
} else {
let mode_nodes = lifecycle.nodes_for_current_mode();
tracing::info!(
"Spawning nodes for mode '{}': {:?}",
lifecycle.current_mode(),
mode_nodes
);
mode_nodes
};
let needs_headless_sim = nodes_to_spawn
.iter()
.any(|n| n == "simulation-bridge" || n.ends_with("/simulation-bridge"))
&& self.sim_paths.as_ref().is_some_and(|paths| paths.headless);
if needs_headless_sim {
if let Some(sim_paths) = &self.sim_paths {
println!("🐳 Starting headless simulation container...");
self.log_buffer
.push("Starting headless simulation container".to_string());
ops::start_headless_simulation(ctx, sim_paths, &self.config).await?;
}
}
self.log_buffer
.push(format!("MODE: Starting in {} mode", lifecycle.current_mode()));
self.pids = ops::spawn_nodes_by_name(
ctx,
&nodes_to_spawn,
&self.project_name,
&self.config,
Some(&self.log_buffer),
)?;
let actually_running: Vec<String> = self.pids.keys().cloned().collect();
lifecycle.mark_nodes_running(&actually_running);
Ok(())
}
pub async fn run(&mut self, ctx: &mut CliContext) -> Result<()> {
if self.watch_mode {
ops::run_watch_mode(ctx, self.pids.clone()).await?;
} else {
if let Some(ref lifecycle) = self.lifecycle {
let running: Vec<String> = self.pids.keys().cloned().collect();
dev_banners::print_mode_status(lifecycle.current_mode(), &running, &self.nodes);
}
let config = ops::InteractiveModeConfig {
project_config: &self.config,
nodes_to_run: &self.nodes,
godot_path: &self.godot_path,
sim_paths: &self.sim_paths,
project_name: &self.project_name,
};
let mut state = ops::InteractiveModeState {
pids: std::mem::take(&mut self.pids),
lifecycle: self.lifecycle.take(),
log_buffer: self.log_buffer.clone(),
};
ops::run_interactive_mode(ctx, config, &mut state).await?;
self.pids = state.pids;
self.lifecycle = state.lifecycle;
self.log_buffer = state.log_buffer;
}
Ok(())
}
pub async fn execute(mut self, ctx: &mut CliContext) -> Result<()> {
self.setup_infrastructure(ctx).await?;
self.build_if_needed(ctx)?;
let shared_process = ctx.shared_process();
let (shutdown_tx, shutdown_rx) = tokio::sync::broadcast::channel::<()>(1);
let listener_config = CommandListenerConfig {
redis_url: ctx.redis_url().to_string(),
project_name: self.project_name.clone(),
robot_id: self.config.robot.id.clone(),
control_plane_url: self.config.environments.control_plane_url(),
relay_url: self.config.environments.relay_url(),
log_buffer: Some(self.log_buffer.clone()),
};
let command_listener = CommandListener::new(listener_config, shared_process.clone());
let listener_handle = tokio::spawn(async move {
if let Err(e) = command_listener.run(shutdown_rx).await {
tracing::error!("Command listener error: {}", e);
}
});
tracing::info!("Command listener started for remote node control");
self.spawn_nodes(ctx).await?;
self.run(ctx).await?;
let _ = shutdown_tx.send(());
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
listener_handle.abort();
println!();
Ok(())
}
}