use crate::commands::{RunCommand, RunRemoteArgs, RunRobotArgs};
use crate::context::CliContext;
use crate::handlers::dev::ops::maybe_inject_teleop_locomotion_env;
use crate::services::credentials::CredentialsService;
use anyhow::{Context, Result};
use mecha10_core::enum_str_method;
use std::collections::HashMap;
use std::process::Command;
#[derive(Debug, Clone, Copy)]
pub enum RunTarget {
Robot,
Remote,
}
enum_str_method!(
fn display_name(RunTarget) -> &'static str {
Robot => "Robot",
Remote => "Remote",
}
);
enum_str_method!(
fn description(RunTarget) -> &'static str {
Robot => "edge/on-robot nodes",
Remote => "cloud/remote server nodes",
}
);
pub async fn handle_run(ctx: &mut CliContext, command: &RunCommand) -> Result<()> {
mecha10_core::relay_topics::regenerate_websocket_bridge_relay_config(&ctx.working_dir)
.context("Failed to regenerate websocket-bridge relay topics")?;
match command {
RunCommand::Robot(args) => run_robot_target(ctx, args).await,
RunCommand::Remote(args) => run_remote_target(ctx, args).await,
}
}
async fn run_robot_target(ctx: &mut CliContext, _args: &RunRobotArgs) -> Result<()> {
let target = RunTarget::Robot;
println!();
println!("🚀 Running Mecha10 Project - {} Target", target.display_name());
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!();
if !ctx.is_project_initialized() {
println!("⚠️ Not in a Mecha10 project directory");
println!();
return Err(anyhow::anyhow!("Not in a Mecha10 project"));
}
let project = ctx.project()?;
let project_name = project.name()?;
println!("Project: {}", project_name);
println!("Target: {} ({})", target.display_name(), target.description());
let build_profile = "release";
println!("Profile: {}", build_profile);
println!();
let redis_url = ctx.redis_url().to_string();
println!("Redis: {}", redis_url);
println!();
let binary_path = ctx.working_dir.join("target").join(build_profile).join(project_name);
if !binary_path.exists() {
println!("❌ Binary not found: {}", binary_path.display());
println!();
println!("Build first with:");
println!(" mecha10 build robot");
return Err(anyhow::anyhow!("Binary not found"));
}
println!("Binary: {}", binary_path.display());
println!();
let config = ctx.load_project_config().await?;
let control_plane_url = config.environments.control_plane_url();
let relay_url = config.environments.relay_url();
let robot_id = config.robot.id.clone();
let credentials_service = CredentialsService::new();
let api_key = match credentials_service.get_api_key() {
Ok(Some(key)) => {
println!("Auth: ✓ (logged in)");
key
}
_ => {
println!("Auth: ⚠ Not logged in - run 'mecha10 auth login'");
String::new()
}
};
println!("Control: {}", control_plane_url);
println!("Relay: {}", relay_url);
println!("Robot: {}", robot_id);
println!();
println!("Starting {}...", target.display_name());
println!();
let mut cmd = Command::new(&binary_path);
cmd.arg("run")
.current_dir(&ctx.working_dir)
.env("MECHA10_REDIS_URL", &redis_url)
.env("MECHA10_ENV", config.environments.current_env_name())
.env("CONTROL_PLANE_URL", &control_plane_url)
.env("WEBRTC_RELAY_URL", &relay_url)
.env("ROBOT_ID", &robot_id)
.env("ROBOT_API_KEY", &api_key)
.env("LOG_LEVEL", ctx.log_level.to_string().to_lowercase());
cmd.envs(&locomotion_env_for_nodes(&config.nodes.get_node_names()));
cmd.stdin(std::process::Stdio::inherit())
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit());
let status = cmd.status()?;
println!();
if status.success() {
println!("✅ {} target stopped gracefully", target.display_name());
} else {
println!(
"❌ {} target exited with error: {:?}",
target.display_name(),
status.code()
);
}
Ok(())
}
fn locomotion_env_for_nodes(node_names: &[String]) -> HashMap<String, String> {
let mut env = HashMap::new();
maybe_inject_teleop_locomotion_env(node_names, &mut env);
env
}
async fn run_remote_target(ctx: &mut CliContext, _args: &RunRemoteArgs) -> Result<()> {
let target = RunTarget::Remote;
println!();
println!("🐳 Running Mecha10 Project - {} Target (Docker)", target.display_name());
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!();
if !ctx.is_project_initialized() {
println!("⚠️ Not in a Mecha10 project directory");
println!();
return Err(anyhow::anyhow!("Not in a Mecha10 project"));
}
let project = ctx.project()?;
let project_name = project.name()?;
println!("Project: {}", project_name);
println!("Target: {} ({})", target.display_name(), target.description());
println!();
let compose_file = ctx.working_dir.join("docker").join("docker-compose.remote.yml");
if !compose_file.exists() {
println!("❌ Docker compose file not found: {}", compose_file.display());
println!();
println!("Expected: docker/docker-compose.remote.yml");
println!("Run 'mecha10 init' to create a new project with Docker support.");
return Err(anyhow::anyhow!("docker-compose.remote.yml not found"));
}
println!("Compose: {}", compose_file.display());
let config = ctx.load_project_config().await?;
let control_plane_url = config.environments.control_plane_url();
let robot_id = config.robot.id.clone();
let credentials_service = CredentialsService::new();
let api_key = match credentials_service.get_api_key() {
Ok(Some(key)) => {
println!("Auth: ✓ (logged in)");
key
}
_ => {
println!("Auth: ⚠ Not logged in - run 'mecha10 auth login'");
String::new()
}
};
println!("Control: {}", control_plane_url);
println!("Robot: {}", robot_id);
println!();
println!("Starting Docker containers...");
println!();
let mut compose_args = vec!["compose", "-f", "docker/docker-compose.remote.yml", "up", "--build"];
compose_args.push("-d");
println!("Mode: Detached (background)");
println!();
let github_token = std::env::var("GITHUB_TOKEN").unwrap_or_default();
if !github_token.is_empty() {
println!("GitHub: ✓ (token found)");
} else {
println!("GitHub: ⚠ No GITHUB_TOKEN - will fail if repo is private");
}
let mut cmd = Command::new("docker");
cmd.args(&compose_args)
.current_dir(&ctx.working_dir)
.env("ROBOT_ID", &robot_id)
.env("ROBOT_API_KEY", &api_key)
.env("CONTROL_PLANE_URL", &control_plane_url)
.env("GITHUB_TOKEN", &github_token)
.env("LOG_LEVEL", ctx.log_level.to_string().to_lowercase());
cmd.stdin(std::process::Stdio::inherit())
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit());
let status = cmd.status()?;
println!();
if status.success() {
println!("✅ Remote containers started in background");
println!();
println!("View logs: docker compose -f docker/docker-compose.remote.yml logs -f");
println!("Stop: docker compose -f docker/docker-compose.remote.yml down");
} else {
println!("❌ Docker compose exited with error: {:?}", status.code());
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sets_locomotion_speed_vars_when_simulator_node_configured() {
let env = locomotion_env_for_nodes(&["simulator".to_string()]);
assert!(env.contains_key("MECHA10_LOCOMOTION_MAX_LINEAR_SPEED"));
assert!(env.contains_key("MECHA10_LOCOMOTION_MAX_ANGULAR_SPEED"));
}
#[test]
fn sets_locomotion_speed_vars_for_simulator_and_teleop_nodes() {
let env = locomotion_env_for_nodes(&["simulator".to_string(), "teleop".to_string()]);
assert!(env.contains_key("MECHA10_LOCOMOTION_MAX_LINEAR_SPEED"));
assert!(env.contains_key("MECHA10_LOCOMOTION_MAX_ANGULAR_SPEED"));
}
#[test]
fn does_not_set_locomotion_vars_without_simulator_node() {
let env = locomotion_env_for_nodes(&["teleop".to_string()]);
assert!(!env.contains_key("MECHA10_LOCOMOTION_MAX_LINEAR_SPEED"));
assert!(!env.contains_key("MECHA10_LOCOMOTION_MAX_ANGULAR_SPEED"));
assert!(!env.contains_key("MECHA10_LOCOMOTION_ACCELERATION"));
}
}