Skip to main content

systemprompt_cli/commands/cloud/tenant/create/
progress.rs

1//! CLI rendering for the tenant provisioning flow.
2//!
3//! Implements the cloud crate's [`ProvisioningProgress`] seam over
4//! `CliService` spinners and message sinks; the
5//! [`TenantProvisioningService`](systemprompt_cloud::tenants::TenantProvisioningService)
6//! owns sequencing, this type owns presentation.
7//!
8//! Copyright (c) systemprompt.io — Business Source License 1.1.
9//! See <https://systemprompt.io> for licensing details.
10
11use std::sync::Mutex;
12
13use indicatif::ProgressBar;
14use systemprompt_cloud::tenants::{ProvisioningProgress, ProvisioningProgressEvent};
15use systemprompt_logging::CliService;
16use url::Url;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct DatabaseConnectionInfo {
20    pub host: String,
21    pub port: u16,
22    pub database: String,
23    pub username: String,
24    pub password: String,
25}
26
27impl DatabaseConnectionInfo {
28    #[must_use]
29    pub fn parse(url: &str) -> Option<Self> {
30        let parsed = Url::parse(url).ok()?;
31        Some(Self {
32            host: parsed.host_str().unwrap_or("unknown").to_owned(),
33            port: parsed.port().unwrap_or(5432),
34            database: parsed.path().trim_start_matches('/').to_owned(),
35            username: parsed.username().to_owned(),
36            password: parsed.password().unwrap_or("********").to_owned(),
37        })
38    }
39
40    #[must_use]
41    pub fn psql_command(&self) -> String {
42        format!(
43            "PGPASSWORD='{}' psql -h {} -p {} -U {} -d {}",
44            self.password, self.host, self.port, self.username, self.database
45        )
46    }
47}
48
49pub(super) struct CliProvisioningProgress {
50    spinner: Mutex<Option<ProgressBar>>,
51}
52
53impl CliProvisioningProgress {
54    pub(super) const fn new() -> Self {
55        Self {
56            spinner: Mutex::new(None),
57        }
58    }
59
60    fn start_spinner(&self, message: &str) {
61        if let Ok(mut slot) = self.spinner.lock() {
62            *slot = Some(CliService::spinner(message));
63        }
64    }
65
66    fn clear_spinner(&self) {
67        if let Ok(mut slot) = self.spinner.lock()
68            && let Some(spinner) = slot.take()
69        {
70            spinner.finish_and_clear();
71        }
72    }
73}
74
75impl ProvisioningProgress for CliProvisioningProgress {
76    fn event(&self, event: &ProvisioningProgressEvent<'_>) {
77        if !matches!(event, ProvisioningProgressEvent::ProvisioningUpdate { .. }) {
78            self.clear_spinner();
79        }
80        match event {
81            ProvisioningProgressEvent::CheckoutSessionStarted => {
82                self.start_spinner("Creating checkout session...");
83            },
84            ProvisioningProgressEvent::CheckoutComplete { tenant_id } => {
85                CliService::success(&format!(
86                    "Checkout complete! Tenant ID: {}",
87                    tenant_id.as_str()
88                ));
89            },
90            ProvisioningProgressEvent::ProvisioningStarted => {
91                self.start_spinner("Waiting for infrastructure provisioning...");
92            },
93            ProvisioningProgressEvent::ProvisioningUpdate { message } => {
94                CliService::info(message);
95            },
96            ProvisioningProgressEvent::Provisioned => {
97                CliService::success("Tenant provisioned successfully");
98            },
99            ProvisioningProgressEvent::CredentialsFetchStarted => {
100                self.start_spinner("Fetching database credentials...");
101            },
102            ProvisioningProgressEvent::CredentialsFetched => {
103                CliService::success("Database credentials retrieved");
104            },
105            ProvisioningProgressEvent::ExternalAccessStarted => {
106                self.start_spinner("Enabling external database access...");
107            },
108            ProvisioningProgressEvent::ExternalAccessEnabled { database_url } => {
109                CliService::success("External database access enabled");
110                print_database_connection_info(database_url);
111            },
112            ProvisioningProgressEvent::ExternalAccessFailed { error } => {
113                CliService::warning(&format!("Failed to enable external access: {}", error));
114                CliService::info("You can enable it later with 'systemprompt cloud tenant edit'");
115            },
116            ProvisioningProgressEvent::TenantSyncStarted => {
117                self.start_spinner("Syncing new tenant...");
118            },
119            ProvisioningProgressEvent::CheckoutSessionCreated
120            | ProvisioningProgressEvent::TenantSynced => {},
121        }
122    }
123}
124
125fn print_database_connection_info(url: &str) {
126    let Some(info) = DatabaseConnectionInfo::parse(url) else {
127        return;
128    };
129
130    CliService::section("Database Connection");
131    CliService::key_value("Host", &info.host);
132    CliService::key_value("Port", &info.port.to_string());
133    CliService::key_value("Database", &info.database);
134    CliService::key_value("User", &info.username);
135    CliService::key_value("Password", &info.password);
136    CliService::key_value("SSL", "required");
137    CliService::info("");
138    CliService::key_value("Connection URL", url);
139    CliService::info("");
140    CliService::info(&format!("Connect with psql:\n  {}", info.psql_command()));
141}