systemprompt-cli 0.2.2

Unified CLI for systemprompt.io AI governance: agent orchestration, MCP governance, analytics, profiles, cloud deploy, and self-hosted operations.
Documentation
use std::sync::Arc;
use systemprompt_database::Database;
use systemprompt_logging::CliService;
use systemprompt_users::{PromoteResult, UserAdminService, UserService};

use super::discovery::{discover_profiles, print_discovery_summary};
use super::types::{CloudUser, SyncResult};

async fn promote_existing_user(
    admin_service: &UserAdminService,
    email: &str,
    profile_name: &str,
) -> SyncResult {
    match admin_service.promote_to_admin(email).await {
        Ok(PromoteResult::Promoted(_, _)) => SyncResult::Promoted {
            email: email.to_string(),
            profile: profile_name.to_string(),
        },
        Ok(PromoteResult::AlreadyAdmin(_)) => SyncResult::AlreadyAdmin {
            email: email.to_string(),
            profile: profile_name.to_string(),
        },
        Ok(PromoteResult::UserNotFound) => SyncResult::Failed {
            profile: profile_name.to_string(),
            error: "User not found after existence check".to_string(),
        },
        Err(e) => SyncResult::Failed {
            profile: profile_name.to_string(),
            error: format!("Promotion failed: {}", e),
        },
    }
}

async fn create_and_promote_user(
    user_service: &UserService,
    admin_service: &UserAdminService,
    user: &CloudUser,
    profile_name: &str,
) -> SyncResult {
    let username = user.username();
    let display_name = user.name.as_deref();

    match user_service
        .create(&username, &user.email, display_name, display_name)
        .await
    {
        Ok(_) => match admin_service.promote_to_admin(&user.email).await {
            Ok(_) => SyncResult::Created {
                email: user.email.clone(),
                profile: profile_name.to_string(),
            },
            Err(e) => SyncResult::Failed {
                profile: profile_name.to_string(),
                error: format!("Created user but promotion failed: {}", e),
            },
        },
        Err(e) => SyncResult::Failed {
            profile: profile_name.to_string(),
            error: format!("User creation failed: {}", e),
        },
    }
}

pub async fn sync_admin_to_database(
    user: &CloudUser,
    database_url: &str,
    profile_name: &str,
) -> SyncResult {
    let db = match tokio::time::timeout(
        std::time::Duration::from_secs(5),
        Database::new_postgres(database_url),
    )
    .await
    {
        Ok(Ok(db)) => Arc::new(db),
        Ok(Err(e)) => {
            return SyncResult::ConnectionFailed {
                profile: profile_name.to_string(),
                error: e.to_string(),
            };
        },
        Err(e) => {
            tracing::warn!(profile = %profile_name, error = %e, "Database connection timed out");
            return SyncResult::ConnectionFailed {
                profile: profile_name.to_string(),
                error: "Connection timed out (5s)".to_string(),
            };
        },
    };

    let user_service = match UserService::new(&db) {
        Ok(s) => s,
        Err(e) => {
            return SyncResult::Failed {
                profile: profile_name.to_string(),
                error: format!("Failed to create user service: {}", e),
            };
        },
    };

    let admin_service = UserAdminService::new(user_service.clone());

    match user_service.find_by_email(&user.email).await {
        Ok(Some(_)) => promote_existing_user(&admin_service, &user.email, profile_name).await,
        Ok(None) => {
            create_and_promote_user(&user_service, &admin_service, user, profile_name).await
        },
        Err(e) => SyncResult::Failed {
            profile: profile_name.to_string(),
            error: format!("Failed to check existing user: {}", e),
        },
    }
}

pub async fn sync_admin_to_all_profiles(user: &CloudUser, verbose: bool) -> Vec<SyncResult> {
    let discovery = match discover_profiles() {
        Ok(d) => d,
        Err(e) => {
            CliService::warning(&format!("Failed to discover profiles: {}", e));
            return Vec::new();
        },
    };

    print_discovery_summary(&discovery, verbose);

    if discovery.profiles.is_empty() {
        if discovery.skipped.is_empty() {
            CliService::info("No profiles found to sync admin user.");
        } else {
            CliService::warning(
                "No profiles available for sync (all skipped due to configuration issues).",
            );
        }
        return Vec::new();
    }

    let mut results = Vec::new();

    for profile in discovery.profiles {
        let Some(database_url) = profile.database_url.as_deref() else {
            results.push(SyncResult::Failed {
                profile: profile.name.clone(),
                error: "Missing database_url".to_string(),
            });
            continue;
        };
        let result = sync_admin_to_database(user, database_url, &profile.name).await;
        results.push(result);
    }

    results
}

pub fn print_sync_results(results: &[SyncResult]) {
    for result in results {
        match result {
            SyncResult::Created { email, profile } => {
                CliService::success(&format!(
                    "Created admin user '{}' in profile '{}'",
                    email, profile
                ));
            },
            SyncResult::Promoted { email, profile } => {
                CliService::success(&format!(
                    "Promoted existing user '{}' to admin in profile '{}'",
                    email, profile
                ));
            },
            SyncResult::AlreadyAdmin { email, profile } => {
                CliService::info(&format!(
                    "User '{}' is already admin in profile '{}'",
                    email, profile
                ));
            },
            SyncResult::ConnectionFailed { profile, error } => {
                CliService::warning(&format!(
                    "Could not connect to profile '{}': {}",
                    profile, error
                ));
            },
            SyncResult::Failed { profile, error } => {
                CliService::warning(&format!(
                    "Failed to sync admin to profile '{}': {}",
                    profile, error
                ));
            },
        }
    }
}