xbp 10.17.1

XBP is a zero-config build pack that can also interact with proxies, kafka, sockets, synthetic monitors.
Documentation
pub mod app;
pub mod commands;
pub mod error;
pub mod features;
pub mod handlers;
pub mod router;

pub use handlers::*;

use crate::cli::app::AppContext;
use crate::cli::error::CliResult;
use crate::commands::curl;
use crate::commands::generate_systemd::{run_generate_systemd, GenerateSystemdArgs};
use crate::commands::redeploy_v2::run_redeploy_v2;
use crate::commands::{
    install_package, list_services, open_global_config, run_config, run_config_secret_delete,
    run_config_secret_set, run_config_secret_show, run_init, run_login, run_redeploy,
    run_redeploy_service, run_service_command, run_setup, run_version_command,
    run_version_release_command, show_service_help, VersionReleaseOptions,
};
use crate::commands::{run_diag, run_nginx};
use crate::config::sync_versioning_files_registry;
use crate::logging::{init_logger, log_error, log_info, log_success, log_warn};
use clap::Parser;
use commands::Cli;

pub async fn run() -> CliResult<()> {
    let cli: Cli = Cli::parse();
    let debug: bool = cli.debug;

    if let Err(e) = init_logger(debug).await {
        let _ = log_error(
            "system",
            "Failed to initialize logger",
            Some(&e.to_string()),
        )
        .await;
    }

    if let Err(e) = sync_versioning_files_registry() {
        let _ = log_warn("config", "Failed to sync versioning registry", Some(&e)).await;
    }

    let mut ctx = AppContext::new(debug);
    router::dispatch(cli, &mut ctx).await
}

pub(super) async fn handle_init(debug: bool) -> CliResult<()> {
    if let Err(e) = run_init(debug).await {
        let _ = log_error("init", "Init failed", Some(&e)).await;
        return Err(e.into());
    }
    Ok(())
}

pub(super) async fn handle_setup(debug: bool) -> CliResult<()> {
    if let Err(e) = run_setup(debug).await {
        let _ = log_error("setup", "Setup failed", Some(&e)).await;
    }
    Ok(())
}

pub(super) async fn handle_redeploy(service_name: Option<String>, debug: bool) -> CliResult<()> {
    if let Some(name) = service_name {
        if let Err(e) = run_redeploy_service(&name, debug).await {
            let _ = log_error("redeploy", "Service redeploy failed", Some(&e)).await;
        }
    } else if let Err(e) = run_redeploy().await {
        let _ = log_error("redeploy", "Redeploy failed", Some(&e)).await;
    }
    Ok(())
}

pub(super) async fn handle_redeploy_v2(cmd: commands::RedeployV2Cmd, debug: bool) -> CliResult<()> {
    let _ = log_info("redeploy_v2", "Starting remote redeploy process", None).await;
    match run_redeploy_v2(cmd.password, cmd.username, cmd.host, cmd.project_dir, debug).await {
        Ok(()) => Ok(()),
        Err(e) => {
            let _ = log_error("redeploy_v2", "Remote redeploy failed", Some(&e)).await;
            Err(e.into())
        }
    }
}

pub(super) async fn handle_config(cmd: commands::ConfigCmd, debug: bool) -> CliResult<()> {
    let commands::ConfigCmd {
        project,
        no_open,
        provider,
    } = cmd;

    if provider.is_some() && (project || no_open) {
        return Err(
            "`xbp config <provider> ...` cannot be combined with `--project` or `--no-open`."
                .into(),
        );
    }

    if let Some(provider_cmd) = provider {
        match provider_cmd {
            commands::ConfigProviderCmd::Openrouter(subcmd) => match subcmd.action {
                commands::ConfigSecretAction::SetKey { key } => {
                    if let Err(e) = run_config_secret_set("openrouter", key).await {
                        let _ = log_error("config", "Failed to set OpenRouter key", Some(&e)).await;
                        return Err(e.into());
                    }
                }
                commands::ConfigSecretAction::DeleteKey => {
                    if let Err(e) = run_config_secret_delete("openrouter").await {
                        let _ =
                            log_error("config", "Failed to delete OpenRouter key", Some(&e)).await;
                        return Err(e.into());
                    }
                }
                commands::ConfigSecretAction::Show { raw } => {
                    if let Err(e) = run_config_secret_show("openrouter", raw).await {
                        let _ =
                            log_error("config", "Failed to show OpenRouter key", Some(&e)).await;
                        return Err(e.into());
                    }
                }
            },
            commands::ConfigProviderCmd::Github(subcmd) => match subcmd.action {
                commands::ConfigSecretAction::SetKey { key } => {
                    if let Err(e) = run_config_secret_set("github", key).await {
                        let _ = log_error("config", "Failed to set GitHub token", Some(&e)).await;
                        return Err(e.into());
                    }
                }
                commands::ConfigSecretAction::DeleteKey => {
                    if let Err(e) = run_config_secret_delete("github").await {
                        let _ =
                            log_error("config", "Failed to delete GitHub token", Some(&e)).await;
                        return Err(e.into());
                    }
                }
                commands::ConfigSecretAction::Show { raw } => {
                    if let Err(e) = run_config_secret_show("github", raw).await {
                        let _ = log_error("config", "Failed to show GitHub token", Some(&e)).await;
                        return Err(e.into());
                    }
                }
            },
        }
    } else if project {
        let _ = run_config(debug).await;
    } else if let Err(e) = open_global_config(no_open).await {
        let _ = log_error("config", "Failed to open global config", Some(&e)).await;
    }
    Ok(())
}

pub(super) async fn handle_install(package: String, debug: bool) -> CliResult<()> {
    if package.is_empty() || package == "--help" || package == "help" {
        return install_package("", debug).await.map_err(Into::into);
    }

    let install_msg: String = format!("Installing package: {}", package);
    let _ = log_info("install", &install_msg, None).await;
    match install_package(&package, debug).await {
        Ok(()) => {
            let success_msg = format!("Successfully installed: {}", package);
            let _ = log_success("install", &success_msg, None).await;
            Ok(())
        }
        Err(e) => Err(e.into()),
    }
}

pub(super) async fn handle_curl(cmd: commands::CurlCmd, debug: bool) -> CliResult<()> {
    let url = cmd
        .url
        .unwrap_or_else(|| "https://example.com/api".to_string());
    if let Err(e) = curl::run_curl(&url, cmd.no_timeout, debug).await {
        let _ = log_error("curl", "Curl command failed", Some(&e)).await;
    }
    Ok(())
}

pub(super) async fn handle_services(debug: bool) -> CliResult<()> {
    if let Err(e) = list_services(debug).await {
        let _ = log_error("services", "Failed to list services", Some(&e)).await;
    }
    Ok(())
}

pub(super) async fn handle_service(
    command: Option<String>,
    service_name: Option<String>,
    debug: bool,
) -> CliResult<()> {
    if let Some(cmd) = command {
        if cmd == "--help" || cmd == "help" {
            if let Some(name) = service_name {
                if let Err(e) = show_service_help(&name).await {
                    let _ = log_error("service", "Failed to show service help", Some(&e)).await;
                }
            } else {
                println!("Usage: xbp service <command> <service-name>");
                println!("Commands: build, install, start, dev");
                println!("Example: xbp service build zeus");
                println!("For help on a specific service: xbp service --help <service-name>");
            }
        } else if let Some(name) = service_name {
            if let Err(e) = run_service_command(&cmd, &name, debug).await {
                let _ = log_error(
                    "service",
                    &format!("Service command '{}' failed", cmd),
                    Some(&e),
                )
                .await;
            }
        } else {
            let _ = log_error("service", "Service name required", None).await;
        }
    } else {
        println!("Usage: xbp service <command> <service-name>");
        println!("Commands: build, install, start, dev");
        println!("Example: xbp service build zeus");
    }
    Ok(())
}

pub(super) async fn handle_nginx(cmd: commands::NginxSubCommand, debug: bool) -> CliResult<()> {
    if let Err(e) = run_nginx(cmd, debug).await {
        let _ = log_error("nginx", "Nginx command failed", Some(&e.to_string())).await;
    }
    Ok(())
}

pub(super) async fn handle_diag(cmd: commands::DiagCmd, debug: bool) -> CliResult<()> {
    if let Err(e) = run_diag(cmd, debug).await {
        let _ = log_error("diag", "Diag command failed", Some(&e.to_string())).await;
    }
    Ok(())
}

pub(super) async fn handle_generate(cmd: commands::GenerateCmd, debug: bool) -> CliResult<()> {
    match cmd.command {
        commands::GenerateSubCommand::Systemd(subcmd) => {
            let args = GenerateSystemdArgs {
                output_dir: subcmd.output_dir,
                service: subcmd.service,
                api: subcmd.api,
            };
            if let Err(e) = run_generate_systemd(args, debug).await {
                let _ = log_error(
                    "generate-systemd",
                    "Failed to generate systemd units",
                    Some(&e),
                )
                .await;
            }
        }
    }
    Ok(())
}

pub(super) async fn handle_done(cmd: commands::DoneCmd, _debug: bool) -> CliResult<()> {
    if let Err(e) = crate::commands::run_done(
        cmd.root,
        cmd.since,
        cmd.output,
        cmd.no_ai,
        cmd.recursive,
        cmd.exclude,
    )
    .await
    {
        let _ = log_error("done", "Done command failed", Some(&e)).await;
        return Err(e.into());
    }
    Ok(())
}

pub(super) async fn handle_login() -> CliResult<()> {
    if let Err(e) = run_login().await {
        let _ = log_error("login", "Login failed", Some(&e)).await;
        return Err(e.into());
    }
    Ok(())
}

pub(super) async fn handle_version(cmd: commands::VersionCmd, debug: bool) -> CliResult<()> {
    let commands::VersionCmd {
        target,
        git,
        command,
    } = cmd;

    if command.is_some() && (target.is_some() || git) {
        return Err(
            "`xbp version release` cannot be combined with `--git` or positional targets.".into(),
        );
    }

    if let Some(subcommand) = command {
        match subcommand {
            commands::VersionSubCommand::Release(release_cmd) => {
                let options = VersionReleaseOptions {
                    explicit_version: release_cmd.version,
                    allow_dirty: release_cmd.allow_dirty,
                    title: release_cmd.title,
                    notes: release_cmd.notes,
                    notes_file: release_cmd.notes_file,
                    draft: release_cmd.draft,
                    prerelease: release_cmd.prerelease,
                };
                if let Err(e) = run_version_release_command(options).await {
                    let _ = log_error("version", "Version release failed", Some(&e)).await;
                    return Err(e.into());
                }
                return Ok(());
            }
        }
    }

    if let Err(e) = run_version_command(target, git, debug).await {
        let _ = log_error("version", "Version command failed", Some(&e)).await;
        return Err(e.into());
    }
    Ok(())
}