xbp 10.30.3

XBP is a zero-config build pack that can also interact with proxies, kafka, sockets, synthetic monitors.
Documentation
use std::collections::HashMap;
use std::path::Path;

use chrono::Utc;
use colored::Colorize;

use crate::cli::commands::VersionDiscoverServicesCmd;
use crate::commands::cli_session::{
    post_project_registration, CliProjectRegisterDevicePayload, CliProjectRegisterPayload,
    CliProjectRegisterResponse, CliProjectRegisterServicePayload,
};
use crate::commands::project_services::{
    discover_marker_service_projects, register_discovered_services, DiscoveredServiceProject,
    RegisterServicesOptions, RegisterServicesReport,
};
use crate::config::resolve_device_identity;
use crate::strategies::{ServiceConfig, XbpConfig};
use crate::utils::{
    find_xbp_config_upwards, git_remote_url_from_metadata, parse_config_with_auto_heal,
    parse_github_repo_from_remote_url,
};

pub async fn run_version_discover_services(
    args: &VersionDiscoverServicesCmd,
) -> Result<(), String> {
    let current_dir = std::env::current_dir()
        .map_err(|error| format!("Failed to read current directory: {}", error))?;
    let found = find_xbp_config_upwards(&current_dir).ok_or_else(|| {
        "Currently not in an XBP project. Run `xbp init` to create a project config here."
            .to_string()
    })?;
    let project_root = found.project_root.clone();

    let content = std::fs::read_to_string(&found.config_path).map_err(|error| {
        format!(
            "Failed to read root XBP config {}: {}",
            found.config_path.display(),
            error
        )
    })?;
    let (mut config, healed_content): (XbpConfig, Option<String>) =
        parse_config_with_auto_heal(&content, found.kind).map_err(|error| {
            format!(
                "Failed to parse root XBP config {}: {}",
                found.config_path.display(),
                error
            )
        })?;
    if let Some(healed_content) = healed_content {
        let _ = std::fs::write(&found.config_path, healed_content);
    }

    let discovered = discover_marker_service_projects(&project_root).await?;
    print_discovered_projects(&project_root, &discovered);

    let options = RegisterServicesOptions {
        dry_run: args.dry_run,
        no_register: args.no_register,
    };
    let report = register_discovered_services(
        &project_root,
        &found.config_path,
        &mut config,
        &options,
    )
    .await?;
    print_registration_report(&found.config_path, &report, &options);

    if should_sync_to_dashboard(&options, &report) {
        sync_project_registration_to_dashboard(
            &project_root,
            &config,
            found.kind,
            &discovered,
        )
        .await;
    }

    Ok(())
}

fn should_sync_to_dashboard(options: &RegisterServicesOptions, report: &RegisterServicesReport) -> bool {
    !options.dry_run && !options.no_register && report.discovered > 0
}

async fn sync_project_registration_to_dashboard(
    project_root: &Path,
    config: &XbpConfig,
    config_kind: &str,
    discovered: &[DiscoveredServiceProject],
) {
    let payload = match build_project_register_payload(project_root, config, config_kind, discovered)
    {
        Ok(payload) => payload,
        Err(error) => {
            println!(
                "  {} Could not prepare dashboard registration payload: {}",
                "!".bright_yellow(),
                error
            );
            return;
        }
    };

    match post_project_registration(&payload).await {
        Ok(response) => print_dashboard_registration_success(&response),
        Err(error) if error.contains("xbp login") => {
            println!();
            println!("Dashboard registration");
            println!(
                "  {} Skipped server registration. {}",
                "i".bright_blue(),
                error
            );
        }
        Err(error) => {
            println!();
            println!("Dashboard registration");
            println!(
                "  {} Failed to register project on xbp.app: {}",
                "!".bright_yellow(),
                error
            );
        }
    }
}

fn build_project_register_payload(
    project_root: &Path,
    config: &XbpConfig,
    config_kind: &str,
    discovered: &[DiscoveredServiceProject],
) -> Result<CliProjectRegisterPayload, String> {
    let device = resolve_device_identity()?;
    let marker_by_root = discovered
        .iter()
        .map(|project| (project.root_directory.clone(), project.marker.clone()))
        .collect::<HashMap<_, _>>();
    let (repository_owner, repository_name) = resolve_optional_github_repository(project_root);
    let services = config
        .services
        .as_ref()
        .map(|services| {
            services
                .iter()
                .map(|service| service_to_register_payload(service, &marker_by_root))
                .collect()
        })
        .unwrap_or_default();

    Ok(CliProjectRegisterPayload {
        project_name: config.project_name.clone(),
        project_path: canonical_project_path(project_root),
        version: config.version.clone(),
        build_dir: config.build_dir.clone(),
        port: config.port,
        app_type: config.app_type.clone(),
        branch: config.branch.clone(),
        target: config.target.clone(),
        config_kind: Some(config_kind.to_string()),
        repository_owner,
        repository_name,
        device: CliProjectRegisterDevicePayload {
            hardware_id: device.hardware_id,
            device_name: build_device_name(),
            hostname: current_hostname(),
            platform: Some(std::env::consts::OS.to_string()),
        },
        services,
        registered_at: Utc::now().to_rfc3339(),
    })
}

fn service_to_register_payload(
    service: &ServiceConfig,
    marker_by_root: &HashMap<String, String>,
) -> CliProjectRegisterServicePayload {
    let marker = service
        .root_directory
        .as_deref()
        .and_then(|root| marker_by_root.get(root).cloned());

    CliProjectRegisterServicePayload {
        name: service.name.clone(),
        target: service.target.clone(),
        branch: service.branch.clone(),
        port: service.port,
        root_directory: service.root_directory.clone(),
        url: service.url.clone(),
        healthcheck_path: service.healthcheck_path.clone(),
        restart_policy: service.restart_policy.clone(),
        start_wrapper: service.start_wrapper.clone(),
        systemd_service_name: service.systemd_service_name.clone(),
        marker,
        commands: service.commands.clone(),
        environment: service.environment.clone(),
        version_targets: service.version_targets.clone(),
    }
}

fn resolve_optional_github_repository(project_root: &Path) -> (Option<String>, Option<String>) {
    let Ok(Some(origin_url)) = git_remote_url_from_metadata(project_root, "origin") else {
        return (None, None);
    };
    let Some((owner, repo)) = parse_github_repo_from_remote_url(&origin_url) else {
        return (None, None);
    };

    (Some(owner), Some(repo))
}

fn canonical_project_path(project_root: &Path) -> String {
    std::fs::canonicalize(project_root)
        .unwrap_or_else(|_| project_root.to_path_buf())
        .to_string_lossy()
        .to_string()
}

fn current_hostname() -> Option<String> {
    for key in ["HOSTNAME", "COMPUTERNAME"] {
        if let Ok(value) = std::env::var(key) {
            let trimmed = value.trim();
            if !trimmed.is_empty() {
                return Some(trimmed.to_string());
            }
        }
    }

    None
}

fn build_device_name() -> Option<String> {
    let hostname = current_hostname();
    let username = std::env::var("USERNAME")
        .or_else(|_| std::env::var("USER"))
        .ok()
        .map(|value| value.trim().to_string())
        .filter(|value| !value.is_empty());

    match (username, hostname) {
        (Some(user), Some(host)) => Some(format!("{user}@{host}")),
        (Some(user), None) => Some(user),
        (None, Some(host)) => Some(host),
        (None, None) => None,
    }
}

fn print_discovered_projects(project_root: &Path, discovered: &[DiscoveredServiceProject]) {
    println!("Service discovery");
    println!("Root: {}", project_root.display());
    if discovered.is_empty() {
        println!(
            "  {} No package roots found (looked for Cargo.toml, package.json, pyproject.toml, requirements.txt, and related markers).",
            "i".bright_blue()
        );
        return;
    }

    println!(
        "  {} Found {} service candidate(s):",
        "".bright_green(),
        discovered.len()
    );
    for project in discovered {
        println!(
            "    {} {} (marker: {})",
            "".bright_cyan(),
            project.root_directory,
            project.marker
        );
    }
}

fn print_registration_report(
    config_path: &Path,
    report: &RegisterServicesReport,
    options: &RegisterServicesOptions,
) {
    if report.discovered == 0 {
        return;
    }

    println!();
    println!("Service registration");
    println!(
        "  {} added, {} updated, {} unchanged",
        report.added, report.updated, report.unchanged
    );

    if options.dry_run {
        println!(
            "  {} Dry run enabled; no changes written to {}",
            "i".bright_blue(),
            config_path.display()
        );
        return;
    }

    if options.no_register {
        println!(
            "  {} Registration skipped (`--no-register`); no changes written.",
            "i".bright_blue()
        );
        return;
    }

    if report.wrote_config {
        println!(
            "  {} Updated {}",
            "".bright_green(),
            config_path.display()
        );
    }
}

fn print_dashboard_registration_success(response: &CliProjectRegisterResponse) {
    if response.project_id.is_empty() {
        return;
    }

    println!();
    println!("Dashboard registration");
    println!(
        "  {} Registered on xbp.app ({} services upserted, {} removed)",
        "".bright_green(),
        response.services_upserted,
        response.services_removed
    );
    if response.project_created {
        println!("  {} Created new project record", "".bright_cyan());
    }
    if let Some(repository_id) = response.repository_id.as_deref() {
        println!(
            "  {} Linked GitHub repository ({})",
            "".bright_cyan(),
            repository_id
        );
    }
}