xbp 0.5.1

XBP is a build pack and deployment management tool to deploy, rust, nextjs etc and manage the NGINX configs below it
Documentation
use std::time::Instant;
use tokio::process::Command;

/// Clean and canonicalize the URL:
/// - If it starts with "https://https://", deduplicate
/// - If it starts with just "domain.tld" (likely no protocol), add "https://"
/// - If it starts with "http://", keep as-is
fn canonicalize_url(raw: &str) -> String {
    let raw = raw.trim();
    if raw.starts_with("https://https://") {
        // Deduplicate
        let dedup = &raw["https://".len()..];
        format!("https://{}", dedup)
    } else if raw.starts_with("https://") || raw.starts_with("http://") {
        raw.to_string()
    } else {
        // If there's a protocol (ftp://, etc), don't mangle, but for this context, only check http/https
        format!("https://{}", raw)
    }
}

/// Run a curl command to fetch https://example.com/api and pretty-print with jq,
/// displaying HTTP status, headers, response time, and output in colored ANSI.
pub async fn run_curl(raw_url: &str, debug: bool) -> Result<(), String> {
    let url = canonicalize_url(raw_url);

    // Use curl to fetch headers and body
    let mut curl_cmd = Command::new("curl");
    curl_cmd
        .arg("-sS") // silent but show errors
        .arg("-D")
        .arg("-") // dump headers to stdout (before body)
        .arg("-w")
        .arg("%{http_code}") // output the status code at end (after body)
        .arg(&url);

    if debug {
        println!("[DEBUG] running curl command for '{}'", url);
    }

    let start = Instant::now();
    let output = curl_cmd
        .output()
        .await
        .map_err(|e| format!("failed to run curl: {}", e))?;
    let duration = start.elapsed();

    if !output.status.success() {
        return Err(format!(
            "\x1b[91mCurl failed with status: {}\nStderr: {}\x1b[0m",
            output.status,
            String::from_utf8_lossy(&output.stderr)
        ));
    }

    // Split headers, body, and grab status code at end
    let all_output = output.stdout;
    let all_str = String::from_utf8_lossy(&all_output);
    let mut lines = all_str.lines();

    // Read headers
    let mut headers = Vec::new();
    let mut body = String::new();
    let mut in_headers = true;
    for line in lines.by_ref() {
        if in_headers && line.trim() == "" {
            // End of headers
            in_headers = false;
            continue;
        }
        if in_headers {
            headers.push(line.to_string());
        } else {
            // Accumulate body
            body.push_str(line);
            body.push('\n');
        }
    }

    // Sometimes curl prints the status code after the body (due to -w)
    let mut status_code = None;
    if !body.is_empty() && body.trim().chars().all(|c| c.is_ascii_digit()) {
        status_code = Some(body.trim().to_string());
        body.clear();
    } else if let Some(code_line) = lines.last() {
        if code_line.chars().all(|c| c.is_ascii_digit()) {
            status_code = Some(code_line.to_string());
        }
    }

    // Print
    println!("\x1b[96m=== Curl to {}\x1b[0m", url);
    println!("\x1b[90m--- Response Headers ---\x1b[0m");
    for h in &headers {
        if h.starts_with("HTTP/") {
            println!("\x1b[94m{}\x1b[0m", h);
        } else {
            println!("\x1b[90m{}\x1b[0m", h);
        }
    }
    if let Some(status) = &status_code {
        let status_int = status.parse::<u16>().unwrap_or(0);
        let color = if status_int >= 200 && status_int < 300 {
            "\x1b[92m"
        } else if status_int >= 400 {
            "\x1b[91m"
        } else {
            "\x1b[93m"
        };
        println!(
            "\x1b[90m--- Status ---\x1b[0m {}{}{}\x1b[0m",
            color, status, "\x1b[0m"
        );
    }

    println!(
        "\x1b[90m--- Response Time ---\x1b[0m \x1b[95m{:.2?}\x1b[0m",
        duration
    );

    // Pretty print JSON with jq -C .
    let mut jq_cmd = Command::new("jq");
    jq_cmd.arg("-C").arg(".");
    if debug {
        println!("[DEBUG] piping response body to jq for colored pretty-print");
    }
    let mut jq_child = jq_cmd
        .stdin(std::process::Stdio::piped())
        .stdout(std::process::Stdio::piped())
        .spawn()
        .map_err(|e| format!("failed to spawn jq: {}", e))?;

    use tokio::io::AsyncWriteExt;
    if let Some(mut stdin) = jq_child.stdin.take() {
        use tokio::io::AsyncWriteExt;
        stdin
            .write_all(body.as_bytes())
            .await
            .map_err(|e| format!("failed to write to jq stdin: {}", e))?;
    }

    let jq_output = jq_child
        .wait_with_output()
        .await
        .map_err(|e| format!("failed to collect jq output: {}", e))?;

    println!("\x1b[90m--- Body (pretty JSON) ---\x1b[0m");
    print!("{}", String::from_utf8_lossy(&jq_output.stdout));

    if !jq_output.status.success() {
        println!("\x1b[93m[WARN] jq encountered an error (body may not be JSON)\x1b[0m");
        if !body.trim().is_empty() {
            println!("\x1b[90m--- Raw Body ---\x1b[0m");
            print!("{}", body);
        }
    }

    Ok(())
}