xbp 10.32.0

XBP is a zero-config build pack that can also interact with proxies, kafka, sockets, synthetic monitors.
Documentation
use super::project::{
    discover_worker_apps, resolve_project_script_names, script_belongs_to_project,
    worker_root_for_script, DiscoveredWorkerApp,
};
use super::wrangler::run_wrangler;
use super::{build_cloudflare_client, load_local_env, resolve_target_script_name};
use crate::cli::commands::WorkersLogsCmd;
use crate::provider_support::{CloudflareClient, CloudflareWorkerScript};
use colored::Colorize;
use dialoguer::{theme::ColorfulTheme, FuzzySelect};
use serde_json::Value;
use std::env;
use std::path::Path;

pub async fn run_worker_logs(
    root_override: Option<&Path>,
    token_override: Option<&str>,
    account_id_override: Option<&str>,
    cmd: WorkersLogsCmd,
) -> Result<(), String> {
    let start = root_override
        .map(Path::to_path_buf)
        .or_else(|| env::current_dir().ok())
        .ok_or_else(|| "Failed to resolve current directory.".to_string())?;

    let apps = discover_worker_apps(&start);
    let project_scripts = resolve_project_script_names(&apps);
    let local_env = apps
        .first()
        .map(|app| load_local_env(&app.root))
        .transpose()?
        .unwrap_or_default();
    let client = build_cloudflare_client(token_override, account_id_override, &local_env)?;

    let scripts = client.list_worker_scripts().await?;
    let scoped_scripts = filter_scripts(&scripts, &project_scripts);
    let script_name = resolve_script_name(
        &start,
        &apps,
        &cmd,
        &scoped_scripts,
        &scripts,
        &project_scripts,
    )?;

    if cmd.build {
        return show_build_logs(&client, &scripts, &script_name, &cmd).await;
    }

    let worker_root = worker_root_for_script(&script_name, &apps)
        .or_else(|| apps.first().map(|app| app.root.as_path()))
        .ok_or_else(|| {
            format!(
                "Could not resolve a Workers project root for `{}`. Pass `xbp workers --root <path> logs`.",
                script_name
            )
        })?;

    if cmd.follow {
        tail_runtime_logs(worker_root, &script_name, &cmd)?;
        return Ok(());
    }

    show_deployment_status(worker_root, &script_name)
}

fn filter_scripts<'a>(
    scripts: &'a [CloudflareWorkerScript],
    project_scripts: &[String],
) -> Vec<&'a CloudflareWorkerScript> {
    if project_scripts.is_empty() {
        return scripts.iter().collect();
    }
    scripts
        .iter()
        .filter(|script| script_belongs_to_project(&script.id, project_scripts))
        .collect()
}

fn resolve_script_name(
    start: &Path,
    apps: &[DiscoveredWorkerApp],
    cmd: &WorkersLogsCmd,
    scoped_scripts: &[&CloudflareWorkerScript],
    all_scripts: &[CloudflareWorkerScript],
    project_scripts: &[String],
) -> Result<String, String> {
    if let Some(script) = cmd
        .target
        .script
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
    {
        return Ok(script.to_string());
    }

    if let Some(script) = cmd
        .script_name
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
    {
        return Ok(script.to_string());
    }

    if let Some(worker_root) = apps.first().map(|app| app.root.as_path()) {
        let local_env = load_local_env(worker_root).unwrap_or_default();
        let resolved = resolve_target_script_name(worker_root, &cmd.target, &local_env);
        if all_scripts.iter().any(|script| script.id == resolved) {
            return Ok(resolved);
        }
    } else if let Ok(worker_root) = super::project::resolve_workers_project_root(Some(start)) {
        let local_env = load_local_env(&worker_root).unwrap_or_default();
        let resolved = resolve_target_script_name(&worker_root, &cmd.target, &local_env);
        if all_scripts.iter().any(|script| script.id == resolved) {
            return Ok(resolved);
        }
    }

    let candidates: Vec<&CloudflareWorkerScript> = if scoped_scripts.is_empty() {
        all_scripts.iter().collect()
    } else {
        scoped_scripts.to_vec()
    };

    if candidates.is_empty() {
        return Err("No Workers found in this Cloudflare account.".to_string());
    }

    if candidates.len() == 1 {
        return Ok(candidates[0].id.clone());
    }

    let labels: Vec<String> = candidates
        .iter()
        .map(|script| format_worker_choice(script, project_scripts))
        .collect();
    let selection = FuzzySelect::with_theme(&ColorfulTheme::default())
        .with_prompt("Select a Worker to inspect")
        .items(&labels)
        .default(0)
        .interact()
        .map_err(|error| format!("Failed to run Worker selection prompt: {}", error))?;
    Ok(candidates[selection].id.clone())
}

fn format_worker_choice(script: &CloudflareWorkerScript, project_scripts: &[String]) -> String {
    let scope =
        if project_scripts.is_empty() || script_belongs_to_project(&script.id, project_scripts) {
            "project"
        } else {
            "account"
        };
    format!("{} ({scope})", script.id)
}

async fn show_build_logs(
    client: &CloudflareClient,
    scripts: &[CloudflareWorkerScript],
    script_name: &str,
    cmd: &WorkersLogsCmd,
) -> Result<(), String> {
    let script = scripts
        .iter()
        .find(|script| script.id == script_name)
        .ok_or_else(|| format!("Worker script `{}` was not found.", script_name))?;
    let tag = script
        .tag
        .as_deref()
        .filter(|value| !value.is_empty())
        .ok_or_else(|| {
            format!(
                "Worker `{}` has no build tag. Workers Builds may not be configured for it.",
                script_name
            )
        })?;

    let builds = client.list_worker_builds(tag).await?;
    if builds.is_empty() {
        println!(
            "{}",
            format!("No Workers Builds history found for `{}`.", script_name).dimmed()
        );
        return Ok(());
    }

    let build = if cmd.failed {
        builds
            .iter()
            .find(|build| is_failed_build_status(build.status.as_deref()))
            .or_else(|| builds.first())
    } else {
        builds.first()
    }
    .ok_or_else(|| format!("No build records found for `{}`.", script_name))?;

    let status = build
        .status
        .clone()
        .unwrap_or_else(|| "unknown".to_string());
    println!(
        "{} {} {} {}",
        "Build".bold(),
        build.build_uuid.cyan(),
        status_color(&status),
        build
            .branch
            .as_deref()
            .map(|branch| format!("branch={branch}"))
            .unwrap_or_default()
            .dimmed()
    );

    let logs = client.get_worker_build_logs(&build.build_uuid).await?;
    if cmd.json {
        println!(
            "{}",
            serde_json::to_string_pretty(&logs)
                .map_err(|error| format!("Failed to encode build logs: {}", error))?
        );
        return Ok(());
    }

    print_build_logs(&logs);
    if is_failed_build_status(build.status.as_deref()) {
        println!(
            "\n{}",
            "Latest matching build failed. Use `xbp workers logs --build --failed` after a new failure to jump to it.".yellow()
        );
    }
    Ok(())
}

fn is_failed_build_status(status: Option<&str>) -> bool {
    matches!(
        status.map(str::to_ascii_lowercase).as_deref(),
        Some("failed") | Some("failure") | Some("error") | Some("cancelled") | Some("canceled")
    )
}

fn status_color(status: &str) -> colored::ColoredString {
    match status.to_ascii_lowercase().as_str() {
        "success" | "succeeded" | "deployed" | "active" => status.green(),
        "failed" | "failure" | "error" | "cancelled" | "canceled" => status.red(),
        "running" | "building" | "queued" | "pending" | "in_progress" => status.yellow(),
        _ => status.normal(),
    }
}

fn print_build_logs(value: &Value) {
    match value {
        Value::String(text) if !text.trim().is_empty() => println!("{text}"),
        Value::Array(lines) => {
            for line in lines {
                match line {
                    Value::String(text) => println!("{text}"),
                    other => println!("{other}"),
                }
            }
        }
        Value::Object(map) => {
            if let Some(Value::String(text)) = map.get("logs") {
                println!("{text}");
                return;
            }
            if let Some(Value::Array(lines)) = map.get("logs") {
                for line in lines {
                    match line {
                        Value::String(text) => println!("{text}"),
                        Value::Object(entry) => {
                            let message = entry
                                .get("message")
                                .or_else(|| entry.get("text"))
                                .or_else(|| entry.get("line"))
                                .and_then(Value::as_str)
                                .unwrap_or("");
                            if !message.is_empty() {
                                println!("{message}");
                            }
                        }
                        other => println!("{other}"),
                    }
                }
                return;
            }
            println!(
                "{}",
                serde_json::to_string_pretty(value).unwrap_or_else(|_| value.to_string())
            );
        }
        Value::Null => println!("{}", "No build logs were returned.".dimmed()),
        other => println!("{other}"),
    }
}

fn tail_runtime_logs(
    worker_root: &Path,
    script_name: &str,
    cmd: &WorkersLogsCmd,
) -> Result<(), String> {
    let mut args = vec!["tail".to_string(), script_name.to_string()];
    if let Some(environment) = cmd
        .target
        .environment
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
    {
        args.push("--env".to_string());
        args.push(environment.to_string());
    }
    args.push("--format".to_string());
    args.push(if cmd.json {
        "json".to_string()
    } else {
        "pretty".to_string()
    });
    println!(
        "{} {} {}",
        "Tailing runtime logs for".dimmed(),
        script_name.cyan().bold(),
        "(Ctrl+C to stop)".dimmed()
    );
    run_wrangler(worker_root, &args)
}

fn show_deployment_status(worker_root: &Path, script_name: &str) -> Result<(), String> {
    println!(
        "{} {}",
        "Recent deployments for".dimmed(),
        script_name.cyan().bold()
    );
    let args = vec![
        "deployments".to_string(),
        "list".to_string(),
        "--name".to_string(),
        script_name.to_string(),
    ];
    run_wrangler(worker_root, &args)?;
    println!(
        "\n{}",
        "Use `xbp workers logs -f` to stream runtime logs, or `xbp workers logs --build` for Workers Builds CI output.".dimmed()
    );
    Ok(())
}