use crate::commands::config::{ConfigListArgs, ConfigPullArgs, ConfigPushArgs};
use crate::context::CliContext;
use crate::services::credentials::CredentialsService;
use anyhow::Result;
use mecha10_robot_config::{
collect_configs_from_dir, write_configs_to_dir, CollectOptions, RobotConfigClient, WriteOptions,
};
const ROBOT_CONFIG_PATH: &str = "/api/robot-config";
pub async fn handle_config_push(ctx: &mut CliContext, args: &ConfigPushArgs) -> Result<()> {
println!();
println!("Pushing configurations...");
println!();
let robot_id = get_robot_id(ctx, args.robot_id.as_deref()).await?;
let configs_dir = match &args.path {
Some(path) => path.clone(),
None => ctx.working_dir.join("configs"),
};
let options = CollectOptions {
include_mecha10_json: true,
project_root: Some(ctx.working_dir.clone()),
};
let configs = collect_configs_from_dir(&configs_dir, &options)
.await
.map_err(|e| anyhow::anyhow!("Failed to collect configs: {}", e))?;
if configs.is_empty() {
println!("No config files found");
println!();
println!("Expected:");
println!(" - configs/ directory with node configs");
println!(" - mecha10.json at project root");
return Ok(());
}
println!("Robot ID: {}", robot_id);
if configs_dir.exists() {
println!("Configs dir: {}", configs_dir.display());
}
println!();
println!("Found {} config(s):", configs.len());
for (key, _) in &configs {
println!(" - {}", key);
}
println!();
if args.dry_run {
println!("DRY RUN - No changes will be made");
return Ok(());
}
let client = create_config_client(ctx).await?;
match client.bulk_upsert_configs(&robot_id, configs).await {
Ok(result) => {
println!(
"Pushed {} config(s) ({} created, {} updated)",
result.configs.len(),
result.created,
result.updated
);
}
Err(e) => {
return Err(anyhow::anyhow!("Failed to push configs: {}", e));
}
}
println!();
Ok(())
}
pub async fn handle_config_pull(ctx: &mut CliContext, args: &ConfigPullArgs) -> Result<()> {
println!();
println!("Pulling configurations...");
println!();
let robot_id = get_robot_id(ctx, args.robot_id.as_deref()).await?;
let output_dir = args.output.clone().unwrap_or_else(|| ctx.working_dir.join("configs"));
println!("Robot ID: {}", robot_id);
println!("Output dir: {}", output_dir.display());
println!();
let client = create_config_client(ctx).await?;
let configs = match client.get_configs(&robot_id).await {
Ok(configs) => configs,
Err(e) => {
return Err(anyhow::anyhow!("Failed to fetch configs: {}", e));
}
};
if configs.is_empty() {
println!("No configs found for robot {}", robot_id);
return Ok(());
}
println!("Found {} config(s):", configs.len());
for config in &configs {
println!(" - {}", config.config_key);
}
println!();
let write_options = WriteOptions {
force: args.force,
project_root: Some(ctx.working_dir.clone()),
};
let result = write_configs_to_dir(&configs, &output_dir, &write_options)
.await
.map_err(|e| anyhow::anyhow!("Failed to write configs: {}", e))?;
for key in &result.written_keys {
let display_name = if key == "mecha10.json" {
"mecha10.json".to_string()
} else {
format!("{}.json", key.replace('/', "_"))
};
println!(" Wrote {}", display_name);
}
for key in &result.skipped_keys {
let display_name = if key == "mecha10.json" {
"mecha10.json".to_string()
} else {
format!("{}.json", key.replace('/', "_"))
};
println!(" Skipped {} (exists, use --force to overwrite)", display_name);
}
println!();
println!("Pulled {} config(s), skipped {}", result.written, result.skipped);
println!();
Ok(())
}
pub async fn handle_config_list(ctx: &mut CliContext, args: &ConfigListArgs) -> Result<()> {
println!();
let robot_id = get_robot_id(ctx, args.robot_id.as_deref()).await?;
println!("Configs for robot: {}", robot_id);
println!();
let client = create_config_client(ctx).await?;
let configs = match client.get_configs(&robot_id).await {
Ok(configs) => configs,
Err(e) => {
return Err(anyhow::anyhow!("Failed to fetch configs: {}", e));
}
};
if configs.is_empty() {
println!("No configs found");
println!();
return Ok(());
}
for config in configs {
println!("{}:", config.config_key);
println!(" Updated: {}", config.updated_at.format("%Y-%m-%d %H:%M:%S UTC"));
if let Some(ref user) = config.updated_by {
println!(" By: {}", user);
}
if args.verbose {
let value_str = serde_json::to_string_pretty(&config.config_value).unwrap_or_else(|_| "{}".to_string());
for line in value_str.lines() {
println!(" {}", line);
}
}
println!();
}
Ok(())
}
async fn get_robot_id(ctx: &mut CliContext, arg_robot_id: Option<&str>) -> Result<String> {
if let Some(id) = arg_robot_id {
return Ok(id.to_string());
}
if ctx.is_project_initialized() {
let project_config = ctx.load_project_config().await?;
return Ok(project_config.robot.id);
}
Err(anyhow::anyhow!(
"No robot ID specified. Use --robot-id or run from a mecha10 project directory."
))
}
async fn create_config_client(ctx: &mut CliContext) -> Result<RobotConfigClient> {
let credentials_service = CredentialsService::new();
let credentials = credentials_service
.load()?
.ok_or_else(|| anyhow::anyhow!("Not logged in. Run `mecha10 auth login` first."))?;
let project_config = ctx.load_project_config().await?;
let control_plane_url = project_config.environments.control_plane_url();
let config_url = format!("{}{}", control_plane_url, ROBOT_CONFIG_PATH);
Ok(RobotConfigClient::with_user_id(
config_url,
credentials.api_key,
Some(credentials.user_id),
))
}