systemprompt-cli 0.18.0

Unified CLI for systemprompt.io AI governance: agent orchestration, MCP governance, analytics, profiles, cloud deploy, and self-hosted operations.
Documentation
use anyhow::{Result, anyhow, bail};
use dialoguer::Confirm;
use dialoguer::theme::ColorfulTheme;
use systemprompt_cloud::{CloudApiClient, CloudPath, TenantStore, TenantType, get_cloud_paths};
use systemprompt_identifiers::TenantId;
use systemprompt_logging::CliService;

use super::select::{get_credentials, select_tenant};
use crate::cli_settings::CliConfig;
use crate::cloud::types::RotateCredentialsOutput;
use crate::shared::CommandOutput;

pub async fn rotate_credentials(
    id: Option<String>,
    skip_confirm: bool,
    config: &CliConfig,
) -> Result<CommandOutput> {
    let cloud_paths = get_cloud_paths();
    let tenants_path = cloud_paths.resolve(CloudPath::Tenants);
    let mut store = TenantStore::load_from_path(&tenants_path).unwrap_or_else(|e| {
        if !config.is_json_output() {
            CliService::warning(&format!("Failed to load tenant store: {}", e));
        }
        TenantStore::default()
    });

    let tenant_id = resolve_rotation_target(id, &store, skip_confirm)?;

    let tenant = store
        .tenants
        .iter()
        .find(|t| t.id == tenant_id.as_str())
        .ok_or_else(|| anyhow!("Tenant not found: {}", tenant_id.as_str()))?;

    if tenant.tenant_type != TenantType::Cloud {
        bail!("Credential rotation is only available for cloud tenants");
    }

    if !skip_confirm && !confirm_rotation(&tenant.name)? {
        return Ok(cancelled_output(&tenant_id, config));
    }

    let creds = get_credentials()?;
    let client = CloudApiClient::new(&creds.api_url, &creds.api_token)?;

    let response = if config.is_json_output() {
        client.rotate_credentials(&tenant_id).await?
    } else {
        let spinner = CliService::spinner("Rotating database credentials...");
        let resp = client.rotate_credentials(&tenant_id).await?;
        spinner.finish_and_clear();
        resp
    };

    let tenant = store
        .tenants
        .iter_mut()
        .find(|t| t.id == tenant_id.as_str())
        .ok_or_else(|| anyhow!("Tenant not found after rotation"))?;

    tenant.internal_database_url = Some(response.internal_database_url.clone());
    if tenant.external_db_access {
        tenant.database_url = Some(response.external_database_url.clone());
    }

    store.save_to_path(&tenants_path)?;

    let output = RotateCredentialsOutput {
        tenant: tenant_id.as_str().to_owned(),
        status: response.status.clone(),
        internal_database_url: response.internal_database_url.clone(),
        external_database_url: response.external_database_url,
    };

    if !config.is_json_output() {
        render_rotation_result(&output);
    }

    Ok(CommandOutput::card_value("Rotate Credentials", &output))
}

fn resolve_rotation_target(
    id: Option<String>,
    store: &TenantStore,
    skip_confirm: bool,
) -> Result<TenantId> {
    if let Some(id) = id {
        return Ok(TenantId::new(id));
    }
    if store.tenants.is_empty() {
        bail!("No tenants configured.");
    }
    if skip_confirm {
        bail!("Tenant ID required in non-interactive mode");
    }
    Ok(TenantId::new(select_tenant(&store.tenants)?.id.clone()))
}

fn confirm_rotation(tenant_name: &str) -> Result<bool> {
    Ok(Confirm::with_theme(&ColorfulTheme::default())
        .with_prompt(format!(
            "Rotate database credentials for '{}'? This will generate a new password.",
            tenant_name
        ))
        .default(false)
        .interact()?)
}

fn cancelled_output(tenant_id: &TenantId, config: &CliConfig) -> CommandOutput {
    if !config.is_json_output() {
        CliService::info("Cancelled");
    }
    let output = RotateCredentialsOutput {
        tenant: tenant_id.as_str().to_owned(),
        status: "cancelled".to_owned(),
        internal_database_url: String::new(),
        external_database_url: String::new(),
    };
    CommandOutput::card_value("Rotate Credentials", &output)
}

fn render_rotation_result(output: &RotateCredentialsOutput) {
    CliService::success("Database credentials rotated");
    CliService::key_value("Status", &output.status);

    CliService::section("New Database Connection");
    CliService::key_value("Internal URL", &output.internal_database_url);
    CliService::key_value("External URL", &output.external_database_url);
}