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 anyhow::{Result, anyhow};
use dialoguer::Select;
use dialoguer::theme::ColorfulTheme;
use systemprompt_cloud::{
    CloudApiClient, CloudCredentials, CloudPath, OAuthTemplates, TenantInfo, TenantStore,
    UserMeResponse, get_cloud_paths, run_oauth_flow,
};
use systemprompt_logging::CliService;
use systemprompt_models::modules::ApiPaths;

use crate::cli_settings::CliConfig;
use crate::cloud::templates::{AUTH_ERROR_HTML, AUTH_SUCCESS_HTML};
use crate::cloud::types::{
    LoginCustomerInfo, LoginOutput, LoginTenantInfo, LoginUserInfo, TenantPlanInfo,
};
use crate::cloud::{Environment, OAuthProvider};
use crate::shared::CommandResult;

pub async fn execute(
    environment: Environment,
    config: &CliConfig,
) -> Result<CommandResult<LoginOutput>> {
    if !config.is_interactive() {
        return Err(anyhow!(
            "OAuth login requires interactive mode.\n\nAlternatives:\n- Set \
             SYSTEMPROMPT_CLOUD_TOKEN environment variable"
        ));
    }

    let api_url = environment.api_url();

    CliService::section("systemprompt.io Cloud Login");
    CliService::info(&format!("Environment: {:?}", environment));

    let cloud_paths = get_cloud_paths();

    if cloud_paths.exists(CloudPath::Credentials) {
        let creds_path = cloud_paths.resolve(CloudPath::Credentials);
        let existing = CloudCredentials::load_from_path(&creds_path)?;
        CliService::warning(&format!("Already logged in as: {}", existing.user_email));
        CliService::info("Re-authenticating...");
    }

    let providers = [OAuthProvider::Github, OAuthProvider::Google];
    let provider_names: Vec<&str> = providers.iter().map(OAuthProvider::display_name).collect();

    let selection = Select::with_theme(&ColorfulTheme::default())
        .with_prompt("Select authentication provider")
        .items(&provider_names)
        .default(0)
        .interact()?;

    let provider = providers[selection];

    let templates = OAuthTemplates {
        success_html: AUTH_SUCCESS_HTML,
        error_html: AUTH_ERROR_HTML,
    };
    let token = run_oauth_flow(api_url, provider, templates).await?;

    let spinner = CliService::spinner("Verifying token...");
    let client = CloudApiClient::new(api_url, &token)?;
    let response = client.get_user().await?;
    spinner.finish_and_clear();

    let creds = CloudCredentials::new(token, api_url.to_string(), response.user.email.clone());

    let save_path = cloud_paths.resolve(CloudPath::Credentials);
    creds.save_to_path(&save_path)?;
    CliService::key_value("Credentials saved to", &save_path.display().to_string());

    let tenant_store = TenantStore::from_tenant_infos(&response.tenants);
    let tenants_path = cloud_paths.resolve(CloudPath::Tenants);
    tenant_store.save_to_path(&tenants_path)?;
    CliService::key_value("Tenants synced to", &tenants_path.display().to_string());

    CliService::success("Logged in successfully");

    if let Err(e) = client
        .report_activity(ApiPaths::ACTIVITY_EVENT_LOGIN, &response.user.id)
        .await
    {
        tracing::debug!(error = %e, "Failed to report login activity");
    }

    CliService::section("Syncing Admin User to Profiles");
    if let Some(cloud_user) = crate::cloud::sync::admin_user::CloudUser::from_credentials()? {
        let verbose = config.should_show_verbose();
        let results =
            crate::cloud::sync::admin_user::sync_admin_to_all_profiles(&cloud_user, verbose).await;
        crate::cloud::sync::admin_user::print_sync_results(&results);
    } else {
        CliService::warning("Could not load cloud user for admin sync");
    }

    print_login_result(&response);

    let output = build_login_output(&response, &save_path, &tenants_path);

    Ok(CommandResult::card(output)
        .with_title("Cloud Login")
        .with_skip_render())
}

fn build_login_output(
    response: &UserMeResponse,
    credentials_path: &std::path::Path,
    tenants_path: &std::path::Path,
) -> LoginOutput {
    let user = LoginUserInfo {
        id: response.user.id.clone(),
        email: response.user.email.clone(),
        name: response.user.name.clone(),
    };

    let customer = response
        .customer
        .as_ref()
        .map(|c| LoginCustomerInfo { id: c.id.clone() });

    let tenants: Vec<LoginTenantInfo> = response
        .tenants
        .iter()
        .map(|t| LoginTenantInfo {
            id: t.id.clone(),
            name: t.name.clone(),
            subscription_status: t.subscription_status.map(|s| format!("{s:?}")),
            plan: t.plan.as_ref().map(|p| TenantPlanInfo {
                name: p.name.clone(),
                memory_mb: p.memory_mb,
                volume_gb: p.volume_gb,
            }),
            region: t.region.clone(),
            hostname: t.hostname.clone(),
        })
        .collect();

    LoginOutput {
        user,
        customer,
        tenants,
        credentials_path: credentials_path.display().to_string(),
        tenants_path: tenants_path.display().to_string(),
    }
}

fn print_login_result(response: &UserMeResponse) {
    CliService::section("User");
    CliService::key_value("Email", &response.user.email);
    if let Some(name) = &response.user.name {
        CliService::key_value("Name", name);
    }
    CliService::key_value("ID", &response.user.id);

    if let Some(customer) = &response.customer {
        CliService::section("Customer");
        CliService::key_value("ID", &customer.id);
    }

    print_tenants(&response.tenants);
}

fn print_tenants(tenants: &[TenantInfo]) {
    if tenants.is_empty() {
        CliService::info("No cloud tenants found.");
        CliService::info(
            "Run 'systemprompt cloud tenant create' (or 'just tenant') to create a local tenant.",
        );
        return;
    }

    CliService::section("Available Tenants");
    for tenant in tenants {
        let status_str = tenant
            .subscription_status
            .map_or_else(|| "Unknown".to_string(), |s| format!("{s:?}"));
        CliService::key_value(&tenant.name, &status_str);
        if let Some(plan) = &tenant.plan {
            CliService::info(&format!(
                "  Plan: {} ({}MB RAM, {}GB storage)",
                plan.name, plan.memory_mb, plan.volume_gb
            ));
        }
        if let Some(region) = &tenant.region {
            CliService::info(&format!("  Region: {region}"));
        }
        if let Some(hostname) = &tenant.hostname {
            CliService::info(&format!("  URL: https://{hostname}"));
        }
    }
    CliService::info("");
    CliService::info(
        "Run 'systemprompt cloud tenant create' (or 'just tenant') to add a local tenant,",
    );
    CliService::info("then 'systemprompt cloud profile create <name>' to create a profile.");
}