use crate::commands::{TargetCommand, TargetPlatformArgs};
use crate::context::CliContext;
use crate::paths;
use crate::services::build_platforms::{label_for_triple, BUILD_PLATFORMS};
use anyhow::{Context, Result};
use mecha10_cli_core::types::project::TargetsConfig;
const PLATFORM_TARGETS: &[&str] = &["robot", "remote", "rl"];
pub async fn handle_target(ctx: &mut CliContext, command: Option<&TargetCommand>) -> Result<()> {
match command {
None => show_targets(ctx).await,
Some(TargetCommand::Platform(args)) => handle_target_platform(ctx, args).await,
}
}
async fn show_targets(ctx: &mut CliContext) -> Result<()> {
println!();
if !ctx.is_project_initialized() {
println!("Not inside a mecha10 project (no mecha10.json found).");
println!();
println!("Run `mecha10 init` to create a new project.");
println!();
return Ok(());
}
let project_config = ctx.load_project_config().await?;
let entries = project_config.targets.present_entries();
if entries.is_empty() {
println!("No targets configured in mecha10.json.");
println!();
return Ok(());
}
for (name, nodes) in entries {
println!("{}:", name);
if nodes.is_empty() {
println!(" Nodes: (none)");
} else {
let labels: Vec<String> = nodes.iter().map(ToString::to_string).collect();
println!(" Nodes: {}", labels.join(", "));
}
if PLATFORM_TARGETS.contains(&name) {
let platforms = platforms_for(&project_config.targets, name);
if platforms.is_empty() {
println!(" Platforms: host architecture (default)");
} else {
let labels: Vec<&str> = platforms.iter().map(|t| label_for_triple(t)).collect();
println!(" Platforms: {}", labels.join(", "));
}
}
println!();
}
Ok(())
}
async fn handle_target_platform(ctx: &mut CliContext, args: &TargetPlatformArgs) -> Result<()> {
println!();
if !ctx.is_project_initialized() {
println!("Not inside a mecha10 project (no mecha10.json found).");
println!();
println!("Run `mecha10 init` to create a new project.");
println!();
return Ok(());
}
let target_key = resolve_target_key(args.target.as_deref())?;
let project_config = ctx.load_project_config().await?;
let configured = platforms_for(&project_config.targets, &target_key);
let labels: Vec<&str> = BUILD_PLATFORMS.iter().map(|p| p.label).collect();
let checked = platforms_checked(&configured);
let selected = crate::ui::prompts::multiselect(
&format!("Select platform(s) for target \"{}\"", target_key),
&labels,
&checked,
)?;
let selected_triples: Vec<String> = selected
.into_iter()
.map(|i| BUILD_PLATFORMS[i].triple.to_string())
.collect();
if selected_triples == configured {
println!("No change.");
println!();
return Ok(());
}
update_platforms(ctx, &target_key, &selected_triples).await
}
fn resolve_target_key(target: Option<&str>) -> Result<String> {
if let Some(target) = target {
if !PLATFORM_TARGETS.contains(&target) {
anyhow::bail!(
"Unknown target \"{}\" - expected one of: {}",
target,
PLATFORM_TARGETS.join(", ")
);
}
return Ok(target.to_string());
}
let selected = crate::ui::prompts::select("Which target do you want to modify?", PLATFORM_TARGETS)?;
Ok(PLATFORM_TARGETS[selected].to_string())
}
fn platforms_checked(configured: &[String]) -> Vec<bool> {
BUILD_PLATFORMS
.iter()
.map(|p| configured.iter().any(|triple| triple == p.triple))
.collect()
}
fn platforms_for(targets: &TargetsConfig, target_key: &str) -> Vec<String> {
match target_key {
"robot" => targets.robot.clone().and_then(|t| t.platforms).unwrap_or_default(),
"remote" => targets.remote.clone().and_then(|t| t.platforms).unwrap_or_default(),
"rl" => targets.rl.clone().and_then(|t| t.platforms).unwrap_or_default(),
_ => Vec::new(),
}
}
async fn update_platforms(ctx: &CliContext, target_key: &str, platforms: &[String]) -> Result<()> {
let config_path = ctx.working_dir.join(paths::PROJECT_CONFIG);
let config_content = tokio::fs::read_to_string(&config_path)
.await
.context("Failed to read mecha10.json")?;
let mut config: serde_json::Value =
serde_json::from_str(&config_content).context("Failed to parse mecha10.json")?;
if platforms.is_empty() {
if let Some(entry) = config["targets"][target_key].as_object_mut() {
entry.remove("platforms");
}
} else {
config["targets"][target_key]["platforms"] = serde_json::json!(platforms);
}
let updated_content = serde_json::to_string_pretty(&config)?;
tokio::fs::write(&config_path, updated_content)
.await
.context("Failed to write mecha10.json")?;
let summary = if platforms.is_empty() {
"host architecture".to_string()
} else {
platforms.join(", ")
};
println!(
"📝 Updated mecha10.json: targets.{}.platforms → {}",
target_key, summary
);
println!();
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::context::CliContextBuilder;
use tempfile::tempdir;
#[test]
fn resolve_target_key_accepts_known_targets() {
assert_eq!(resolve_target_key(Some("robot")).unwrap(), "robot");
assert_eq!(resolve_target_key(Some("remote")).unwrap(), "remote");
assert_eq!(resolve_target_key(Some("rl")).unwrap(), "rl");
}
#[test]
fn resolve_target_key_rejects_unknown_target() {
let err = resolve_target_key(Some("dev")).unwrap_err();
assert!(err.to_string().contains("Unknown target"));
}
#[test]
fn platforms_checked_matches_configured_triples() {
let configured = vec!["x86_64-unknown-linux-gnu".to_string()];
let checked = platforms_checked(&configured);
let expected: Vec<bool> = BUILD_PLATFORMS
.iter()
.map(|p| p.triple == "x86_64-unknown-linux-gnu")
.collect();
assert_eq!(checked, expected);
}
#[test]
fn platforms_for_reads_the_matching_target_entry() {
let targets: TargetsConfig = serde_json::from_value(serde_json::json!({
"robot": { "nodes": ["@mecha10/motor"], "platforms": ["aarch64-unknown-linux-gnu"] }
}))
.unwrap();
assert_eq!(platforms_for(&targets, "robot"), vec!["aarch64-unknown-linux-gnu"]);
assert_eq!(platforms_for(&targets, "remote"), Vec::<String>::new());
assert_eq!(platforms_for(&targets, "rl"), Vec::<String>::new());
}
#[test]
fn platforms_for_reads_the_rl_target_entry() {
let targets: TargetsConfig = serde_json::from_value(serde_json::json!({
"rl": { "nodes": ["@mecha10/rl-policy"], "platforms": ["x86_64-unknown-linux-gnu"] }
}))
.unwrap();
assert_eq!(platforms_for(&targets, "rl"), vec!["x86_64-unknown-linux-gnu"]);
}
#[tokio::test]
async fn update_platforms_writes_the_new_triples_into_the_target_entry() {
let dir = tempdir().unwrap();
let config_path = dir.path().join("mecha10.json");
std::fs::write(
&config_path,
serde_json::json!({
"name": "proj",
"version": "1.0.0",
"targets": {
"remote": { "nodes": ["@mecha10/object-detector"], "platforms": ["x86_64-unknown-linux-gnu"] }
}
})
.to_string(),
)
.unwrap();
let ctx = CliContextBuilder::new()
.working_dir(dir.path().to_path_buf())
.config_path(Some(config_path.clone()))
.build()
.unwrap();
update_platforms(&ctx, "remote", &["aarch64-unknown-linux-gnu".to_string()])
.await
.unwrap();
let content = std::fs::read_to_string(&config_path).unwrap();
let value: serde_json::Value = serde_json::from_str(&content).unwrap();
assert_eq!(
value["targets"]["remote"]["platforms"],
serde_json::json!(["aarch64-unknown-linux-gnu"])
);
}
}