use super::common::{get_analysis, parse_ssh_target, print_analysis_results};
use super::error::CliError;
use super::parser::Cli;
use super::ui;
use super::ui::handle_deployer_events;
use crate::deployer::{types::DeploymentConfig, Deployer};
use crate::executor::SshCommandExecutor;
use clap::Args;
use std::path::PathBuf;
use std::time::Duration;
use tokio::sync::mpsc;
use tracing::{debug, info, instrument};
#[derive(Debug, Args)]
pub struct Up {
#[arg(required = true)]
target: String,
#[arg(long)]
no_health_check: bool,
#[arg(long)]
pub no_progress: bool,
}
impl Up {
#[instrument(name = "up", skip(self, cli_args), fields(target = %self.target))]
pub async fn run(&self, cli_args: &Cli) -> Result<(), CliError> {
let target = parse_ssh_target(&self.target)?;
info!(
"Deploying services to {}",
ui::format_highlight(&self.target)
);
debug!(user = %target.user, port = %target.port, key = %cli_args.identity_file.display(), "SSH details");
let analysis_pb = ui::create_spinner("Performing local analysis...");
let analysis = get_analysis(cli_args).await.map_err(|e| {
analysis_pb.finish_and_clear(); CliError::OperationFailed(format!("Local analysis failed: {}", e))
})?;
analysis_pb.finish_with_message("Local analysis complete.");
print_analysis_results(&analysis);
let ssh_pb = ui::create_spinner(&format!(
"Connecting to {}...",
ui::format_highlight(&target.host)
));
let addr_str = format!("{}:{}", target.host, target.port);
let mut executor = SshCommandExecutor::connect(
&cli_args.identity_file,
&target.user,
&addr_str,
Duration::from_secs(30), )
.await
.map_err(|e| {
ssh_pb.finish_and_clear();
CliError::OperationFailed(format!("SSH connection failed: {}", e))
})?;
ssh_pb.finish_with_message(format!(
"Connected to {}.",
ui::format_highlight(&target.host)
));
let (progress_sender, ui_update_task_handle) = if !self.no_progress {
let (sender, receiver) = mpsc::channel::<crate::deployer::types::DeployerEvent>(32); let deploy_pb = ui::create_spinner("Initializing deployment...");
let ui_task = tokio::spawn(handle_deployer_events(receiver, deploy_pb.clone()));
(Some(sender), Some((ui_task, deploy_pb))) } else {
info!("Progress spinner disabled via --no-progress.");
(None, None)
};
let deploy_config = DeploymentConfig {
project_dir: analysis.resolved_project_dir.clone(),
remote_dir: cli_args.remote_dir.clone(),
compose_files: analysis.resolved_compose_files.clone(),
env_files: analysis.resolved_env_files.clone(),
consumed_env: analysis.consumed_env,
exposed_ports: analysis.exposed_ports,
local_references: analysis
.local_references
.iter()
.map(PathBuf::from)
.collect(),
volumes: analysis.volumes,
};
let mut deployer = Deployer::new(deploy_config, &mut executor, progress_sender);
let deploy_result = deployer.deploy().await;
drop(deployer);
if let Some((ui_task, deploy_pb)) = ui_update_task_handle {
if let Err(e) = ui_task.await {
tracing::error!("UI update task failed: {}", e); }
match &deploy_result {
Ok(_) => {
deploy_pb.finish_with_message("✅ Deployment tasks finished.");
}
Err(_) => {
deploy_pb.finish_with_message("❌ Deployment failed".to_string());
}
}
}
let status = match deploy_result {
Ok(status) => status,
Err(e) => {
return Err(CliError::OperationFailed(format!(
"Deployment failed: {}",
e
)));
}
};
if !status.services_healthy && !self.no_health_check {
return Err(CliError::OperationFailed(
ui::format_warning("Some services are not healthy after deployment.").to_string(),
));
} else if status.services_healthy {
info!("{}", ui::format_success("All services reported healthy."));
} else if self.no_health_check {
info!("{}", ui::format_warning("Skipped service health check."));
}
info!("{}", ui::format_success("Deployment successful!"));
Ok(())
}
}