Skip to main content

systemprompt_cli/commands/cloud/profile/
create_setup.rs

1//! Post-create profile setup, including the Postgres compose container.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use anyhow::Result;
7use std::path::Path;
8use systemprompt_cloud::{DockerCli, ProjectContext};
9use systemprompt_logging::CliService;
10
11use super::templates::{run_migrations_cmd, validate_connection};
12use crate::cloud::tenant::wait_for_postgres_healthy;
13use crate::interactive::Prompter;
14
15pub async fn handle_local_tenant_setup(
16    prompter: &dyn Prompter,
17    cloud_user: &crate::cloud::auth::admin_user::CloudUser,
18    db_url: &str,
19    tenant_name: &str,
20    profile_path: &Path,
21) -> Result<()> {
22    let spinner = CliService::spinner("Validating PostgreSQL connection...");
23    let mut connection_valid = validate_connection(db_url).await;
24    spinner.finish_and_clear();
25
26    if !connection_valid {
27        let ctx = ProjectContext::discover();
28        let compose_path = ctx.docker_dir().join(format!("{}.yaml", tenant_name));
29
30        if compose_path.exists() {
31            let start_docker =
32                prompter.confirm("PostgreSQL not running. Start Docker container?", true)?;
33
34            if start_docker {
35                connection_valid = start_postgres_container(&compose_path).await?;
36            }
37        } else {
38            CliService::warning("Could not connect to PostgreSQL.");
39            CliService::info("Ensure PostgreSQL is running before starting services.");
40        }
41    }
42
43    if connection_valid {
44        CliService::success("PostgreSQL connection verified");
45
46        let run_migrations = prompter.confirm("Run database migrations?", true)?;
47
48        let migrations_succeeded = if run_migrations {
49            match run_migrations_cmd(profile_path) {
50                Ok(()) => true,
51                Err(e) => {
52                    CliService::warning(&format!("Migration failed: {}", e));
53                    false
54                },
55            }
56        } else {
57            false
58        };
59
60        if migrations_succeeded {
61            let result = crate::cloud::auth::admin_user::sync_admin_to_database(
62                cloud_user,
63                db_url,
64                tenant_name,
65            )
66            .await;
67
68            match &result {
69                crate::cloud::auth::admin_user::SyncResult::Created { email, .. } => {
70                    CliService::success(&format!("Created admin user: {}", email));
71                },
72                crate::cloud::auth::admin_user::SyncResult::Promoted { email, .. } => {
73                    CliService::success(&format!("Promoted user to admin: {}", email));
74                },
75                crate::cloud::auth::admin_user::SyncResult::AlreadyAdmin { email, .. } => {
76                    CliService::info(&format!("User '{}' is already admin", email));
77                },
78                crate::cloud::auth::admin_user::SyncResult::ConnectionFailed { error, .. } => {
79                    CliService::warning(&format!("Could not sync admin user: {}", error));
80                },
81                crate::cloud::auth::admin_user::SyncResult::Failed { error, .. } => {
82                    CliService::warning(&format!("Admin user sync failed: {}", error));
83                },
84            }
85        }
86    }
87
88    Ok(())
89}
90
91pub fn get_cloud_user() -> Result<crate::cloud::auth::admin_user::CloudUser> {
92    crate::cloud::auth::admin_user::CloudUser::from_credentials()?.ok_or_else(|| {
93        anyhow::anyhow!("Cloud credentials required. Run 'systemprompt cloud login' first.")
94    })
95}
96
97async fn start_postgres_container(compose_path: &Path) -> Result<bool> {
98    CliService::info("Starting PostgreSQL container...");
99
100    let compose_path_str = compose_path
101        .to_str()
102        .ok_or_else(|| anyhow::anyhow!("Invalid compose path"))?;
103    let project = compose_path
104        .file_stem()
105        .and_then(|s| s.to_str())
106        .ok_or_else(|| anyhow::anyhow!("Invalid compose path"))?
107        .to_owned();
108
109    let docker = DockerCli::new();
110    let status = docker
111        .status(&[
112            "compose",
113            "-p",
114            &project,
115            "-f",
116            compose_path_str,
117            "up",
118            "-d",
119        ])
120        .map_err(|_e| anyhow::anyhow!("Failed to execute docker compose. Is Docker running?"))?;
121
122    if !status.success() {
123        CliService::warning("Failed to start PostgreSQL container. Is Docker running?");
124        return Ok(false);
125    }
126
127    let spinner = CliService::spinner("Waiting for PostgreSQL to be ready...");
128    match wait_for_postgres_healthy(&docker, &project, compose_path, 60).await {
129        Ok(()) => {
130            spinner.finish_and_clear();
131            Ok(true)
132        },
133        Err(e) => {
134            spinner.finish_and_clear();
135            CliService::warning(&format!("PostgreSQL failed to become healthy: {}", e));
136            Ok(false)
137        },
138    }
139}