systemprompt-cli 0.25.0

Unified CLI for systemprompt.io AI governance: agent orchestration, MCP governance, analytics, profiles, cloud deploy, and self-hosted operations.
Documentation
//! CLI rendering for the tenant provisioning flow.
//!
//! Implements the cloud crate's [`ProvisioningProgress`] seam over
//! `CliService` spinners and message sinks; the
//! [`TenantProvisioningService`](systemprompt_cloud::tenants::TenantProvisioningService)
//! owns sequencing, this type owns presentation.
//!
//! Copyright (c) systemprompt.io — Business Source License 1.1.
//! See <https://systemprompt.io> for licensing details.

use std::sync::Mutex;

use indicatif::ProgressBar;
use systemprompt_cloud::tenants::{ProvisioningProgress, ProvisioningProgressEvent};
use systemprompt_logging::CliService;
use url::Url;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DatabaseConnectionInfo {
    pub host: String,
    pub port: u16,
    pub database: String,
    pub username: String,
    pub password: String,
}

impl DatabaseConnectionInfo {
    #[must_use]
    pub fn parse(url: &str) -> Option<Self> {
        let parsed = Url::parse(url).ok()?;
        Some(Self {
            host: parsed.host_str().unwrap_or("unknown").to_owned(),
            port: parsed.port().unwrap_or(5432),
            database: parsed.path().trim_start_matches('/').to_owned(),
            username: parsed.username().to_owned(),
            password: parsed.password().unwrap_or("********").to_owned(),
        })
    }

    #[must_use]
    pub fn psql_command(&self) -> String {
        format!(
            "PGPASSWORD='{}' psql -h {} -p {} -U {} -d {}",
            self.password, self.host, self.port, self.username, self.database
        )
    }
}

pub(super) struct CliProvisioningProgress {
    spinner: Mutex<Option<ProgressBar>>,
}

impl CliProvisioningProgress {
    pub(super) const fn new() -> Self {
        Self {
            spinner: Mutex::new(None),
        }
    }

    fn start_spinner(&self, message: &str) {
        if let Ok(mut slot) = self.spinner.lock() {
            *slot = Some(CliService::spinner(message));
        }
    }

    fn clear_spinner(&self) {
        if let Ok(mut slot) = self.spinner.lock()
            && let Some(spinner) = slot.take()
        {
            spinner.finish_and_clear();
        }
    }
}

impl ProvisioningProgress for CliProvisioningProgress {
    fn event(&self, event: &ProvisioningProgressEvent<'_>) {
        if !matches!(event, ProvisioningProgressEvent::ProvisioningUpdate { .. }) {
            self.clear_spinner();
        }
        match event {
            ProvisioningProgressEvent::CheckoutSessionStarted => {
                self.start_spinner("Creating checkout session...");
            },
            ProvisioningProgressEvent::CheckoutComplete { tenant_id } => {
                CliService::success(&format!(
                    "Checkout complete! Tenant ID: {}",
                    tenant_id.as_str()
                ));
            },
            ProvisioningProgressEvent::ProvisioningStarted => {
                self.start_spinner("Waiting for infrastructure provisioning...");
            },
            ProvisioningProgressEvent::ProvisioningUpdate { message } => {
                CliService::info(message);
            },
            ProvisioningProgressEvent::Provisioned => {
                CliService::success("Tenant provisioned successfully");
            },
            ProvisioningProgressEvent::CredentialsFetchStarted => {
                self.start_spinner("Fetching database credentials...");
            },
            ProvisioningProgressEvent::CredentialsFetched => {
                CliService::success("Database credentials retrieved");
            },
            ProvisioningProgressEvent::ExternalAccessStarted => {
                self.start_spinner("Enabling external database access...");
            },
            ProvisioningProgressEvent::ExternalAccessEnabled { database_url } => {
                CliService::success("External database access enabled");
                print_database_connection_info(database_url);
            },
            ProvisioningProgressEvent::ExternalAccessFailed { error } => {
                CliService::warning(&format!("Failed to enable external access: {}", error));
                CliService::info("You can enable it later with 'systemprompt cloud tenant edit'");
            },
            ProvisioningProgressEvent::TenantSyncStarted => {
                self.start_spinner("Syncing new tenant...");
            },
            ProvisioningProgressEvent::CheckoutSessionCreated
            | ProvisioningProgressEvent::TenantSynced => {},
        }
    }
}

fn print_database_connection_info(url: &str) {
    let Some(info) = DatabaseConnectionInfo::parse(url) else {
        return;
    };

    CliService::section("Database Connection");
    CliService::key_value("Host", &info.host);
    CliService::key_value("Port", &info.port.to_string());
    CliService::key_value("Database", &info.database);
    CliService::key_value("User", &info.username);
    CliService::key_value("Password", &info.password);
    CliService::key_value("SSL", "required");
    CliService::info("");
    CliService::key_value("Connection URL", url);
    CliService::info("");
    CliService::info(&format!("Connect with psql:\n  {}", info.psql_command()));
}