//! Per-command handlers extracted from main.rs (the 500-line cap). //! `main.rs` keeps the clap dispatch; each `handle_*` does the command work. use crate::cli::{OutputFormat, PowerAction}; use crate::config::Config; use crate::config::load_recipe; use crate::config::resolve_cluster; use crate::config::{Cluster, select_cluster_by_name}; use crate::docker; use crate::exec; use crate::power; use crate::ssh::fetch_host_key; use anyhow::Result; use std::collections::HashSet; use tracing::info; /// Handle `logs` command: show container logs. pub(crate) async fn handle_logs( config: &Config, recipe_name: &str, cluster: &str, follow: bool, node: Option, dry_run: bool, ) -> Result<()> { let cluster = resolve_cluster_for_op(config, recipe_name, cluster).await?; docker::show_logs( config, &cluster, recipe_name, follow, node.as_deref(), dry_run, ) .await?; Ok(()) } /// Handle `health` command: check cluster health or show host key fingerprints. pub(crate) async fn handle_health( config: &Config, cluster_name: Option, fingerprint: bool, format: OutputFormat, ) -> Result<()> { if fingerprint { let clusters_to_check: Vec = match &cluster_name { Some(name) => vec![select_cluster_by_name(config, name)?], None => config.clusters.clone(), }; for cluster in &clusters_to_check { println!( "\n{}", fluent_i18n::t!("cluster-section", { "name" => &cluster.name }) ); for node in &cluster.nodes { match fetch_host_key(config, node).await { Ok(fp) => println!( "{}", fluent_i18n::t!("host-key-line", { "name" => &node.name, "ip" => &node.ssh_ip, "key" => &fp }) ), Err(e) => println!( "{}", fluent_i18n::t!("host-key-error", { "name" => &node.name, "ip" => &node.ssh_ip, "error" => &e.to_string() }) ), } } } } else { match cluster_name { Some(ref name) => { let cluster = select_cluster_by_name(config, name)?; docker::health::health_check(config, &cluster, false, format).await?; } None => { let fleet = docker::health::collect(config, None, false).await?; docker::health::print_fleet(&fleet, format); } } } Ok(()) } /// Handle `exec` command: run a command on cluster nodes via SSH. pub(crate) async fn handle_exec( config: &Config, cluster: Option, recipe: Option, node: Option, alias: Option, command: Vec, dry_run: bool, ) -> Result<()> { let (cluster, _recipe) = exec::resolve_target(config, cluster.as_deref(), recipe.as_deref()).await?; let cmd_str = exec::resolve_command(config, alias.as_deref(), &command)?; exec::run(config, &cluster, node.as_deref(), &cmd_str, dry_run).await?; Ok(()) } /// Handle `power` command: power on/off/switch nodes. pub(crate) async fn handle_power( config: &Config, action: PowerAction, dry_run: bool, ) -> Result<()> { match action { PowerAction::Off { cluster, node } => { let cluster = select_cluster_by_name(config, &cluster)?; let nodes = node.into_iter().collect::>(); power::power_off(config, &cluster, &nodes, dry_run).await?; } PowerAction::On { cluster, node } => { let cluster = select_cluster_by_name(config, &cluster)?; let nodes = node.into_iter().collect::>(); let targets = power::select_targets(&cluster, &nodes, "power on")?; let stale = if dry_run { Vec::new() } else { power::stale_nodes(&targets).await }; let report = power::power_on(config, &cluster, &nodes, dry_run, &stale).await?; for line in report { info!("{}", line); } } PowerAction::Switch { from, to } => { let from = select_cluster_by_name(config, &from)?; let to = select_cluster_by_name(config, &to)?; power::power_switch(config, &from, &to, dry_run).await?; } } Ok(()) } /// Resolve a cluster for container operations (stop/logs). /// When `auto`, tries live container label lookup first, then falls back /// to config-based auto-selection (for dry-run / offline scenarios). pub(crate) async fn resolve_cluster_for_op( config: &Config, recipe_name: &str, cluster_name: &str, ) -> Result { if cluster_name != "auto" { let recipe = load_recipe(recipe_name, &config.recipe_sources).await?; let (cluster, _) = resolve_cluster(config, &recipe, cluster_name, &HashSet::new())?; return Ok(cluster); } // Try live container label lookup first if let Ok(c) = docker::resolve_cluster_by_container_label(config, recipe_name).await { return Ok(c.clone()); } // Fall back to config-based auto (dry-run / containers not running) let recipe = load_recipe(recipe_name, &config.recipe_sources).await?; let (cluster, _) = resolve_cluster(config, &recipe, "auto", &HashSet::new())?; Ok(cluster) }