xbp 10.13.0

XBP is a zero-config build pack that can also interact with proxies, kafka, sockets, synthetic monitors.
Documentation
//! Remote redeploy via SSH
//!
//! Establishes an SSH connection using stored or prompted credentials and
//! executes `redeploy.sh` in the configured remote project directory. Captures
//! stdout/stderr for logging and diagnostics.
use crate::commands::ssh_helpers::{prompt_for_input, prompt_for_password};
use crate::config::SshConfig;
use crate::logging::{log_error, log_info, log_success};
use anyhow::Result;
use async_ssh2_tokio::client::{AuthMethod, Client, ServerCheckMethod};
use tracing::{error, info};

/// Execute the `redeploy_v2` command over SSH.
///
/// Arguments are resolved in priority order: explicit CLI values, stored config,
/// and finally interactive prompts for any missing values. When `debug` is true,
/// emits additional progress logs.
pub async fn run_redeploy_v2(
    password: Option<String>,
    username: Option<String>,
    host: Option<String>,
    project_dir: Option<String>,
    debug: bool,
) -> Result<(), String> {
    let mut config: SshConfig =
        SshConfig::load().map_err(|e| format!("Failed to load SSH config: {}", e))?;

    let final_username: Option<String> = username.or_else(|| config.username.clone());
    let final_host: Option<String> = host.or_else(|| config.host.clone());
    let final_project_dir: Option<String> = project_dir.or_else(|| config.project_dir.clone());

    let mut input_password: Option<String> = password;

    let resolved_username: String = if let Some(u) = final_username {
        u
    } else {
        let u: String = prompt_for_input("Enter SSH username: ")?;
        config.username = Some(u.clone());
        u
    };

    let resolved_host: String = if let Some(h) = final_host {
        h
    } else {
        let h = prompt_for_input("Enter SSH host: ")?;
        config.host = Some(h.clone());
        h
    };

    let resolved_project_dir: String = if let Some(d) = final_project_dir {
        d
    } else {
        let d = prompt_for_input("Enter remote project directory: ")?;
        config.project_dir = Some(d.clone());
        d
    };

    if input_password.is_none() {
        input_password = config.password.clone();
    }

    let resolved_password: String = if let Some(p) = input_password {
        p
    } else {
        let p = prompt_for_password("Enter SSH password: ")?;
        config.password = Some(p.clone());
        p
    };

    config
        .save()
        .map_err(|e| format!("Failed to save SSH config: {}", e))?;

    log_info(
        "redeploy_v2",
        "Attempting SSH connection",
        Some(&format!(
            "Host: {}, User: {}",
            resolved_host, resolved_username
        )),
    )
    .await?;

    // Establish SSH connection
    let client: Client = Client::connect(
        (resolved_host.as_str(), 22),
        resolved_username.as_str(),
        AuthMethod::with_password(resolved_password.as_str()),
        ServerCheckMethod::NoCheck,
    )
    .await
    .map_err(|e| format!("SSH connection failed: {}", e))?;

    log_success("redeploy_v2", "SSH connection established", None).await?;

    // Execute redeploy.sh
    let command_to_execute = format!("cd {} && sudo sh redeploy.sh", resolved_project_dir);
    if debug {
        log_info(
            "redeploy_v2",
            "Executing remote command",
            Some(&command_to_execute),
        )
        .await?;
    }

    let result: async_ssh2_tokio::client::CommandExecutedResult = client
        .execute(command_to_execute.as_str())
        .await
        .map_err(|e| format!("Failed to execute remote command: {}", e))?;

    // Read output
    let stdout: String = result.stdout;
    let stderr: String = result.stderr;

    if !stdout.is_empty() {
        log_info("redeploy_v2", "Remote stdout", Some(&stdout)).await?;
        info!("Remote Output:\n{}", stdout);
    }
    if !stderr.is_empty() {
        log_error("redeploy_v2", "Remote stderr", Some(&stderr)).await?;
        error!("Remote Error:\n{}", stderr);
    }

    let exit_status: u32 = result.exit_status;

    if exit_status == 0 {
        log_success(
            "redeploy_v2",
            "Remote redeploy.sh executed successfully",
            None,
        )
        .await?;
        Ok(())
    } else {
        log_error(
            "redeploy_v2",
            &format!(
                "Remote redeploy.sh failed with exit status: {}",
                exit_status
            ),
            None,
        )
        .await?;
        Err(format!(
            "Remote redeploy.sh failed with exit status: {}",
            exit_status
        ))
    }
}