systemprompt_cli/commands/cloud/tenant/create/
local.rs1use anyhow::{Context, Result, bail};
12use systemprompt_cloud::{DockerCli, ProjectContext, StoredTenant};
13use systemprompt_logging::CliService;
14
15use crate::cloud::init::ensure_project_scaffolding;
16use crate::cloud::profile::templates::validate_connection;
17use crate::cloud::profile::{
18 collect_api_keys, create_profile_for_tenant, get_cloud_user, handle_local_tenant_setup,
19};
20use crate::interactive::Prompter;
21
22use super::super::docker::{
23 TenantContainer, generate_admin_password, is_project_running, nanoid, new_local_tenant_id,
24 remove_project, start_project,
25};
26
27use super::sanitize_database_name;
28
29pub async fn create_local_tenant(prompter: &dyn Prompter) -> Result<StoredTenant> {
30 CliService::section("Create Local PostgreSQL Tenant");
31
32 let name = prompter.input_with_default("Tenant name", "local")?;
33
34 if name.is_empty() {
35 bail!("Tenant name cannot be empty");
36 }
37
38 let project = format!("{}_{}", sanitize_database_name(&name), nanoid());
39
40 let port: u16 = prompter
41 .input_with_default("PostgreSQL port", "5432")?
42 .parse()
43 .context("PostgreSQL port must be a number")?;
44
45 let docker = DockerCli::new();
46
47 if is_project_running(&docker, &project) {
48 bail!("A container for project '{project}' is already running");
49 }
50
51 let container = TenantContainer::new(project.clone(), generate_admin_password(), port);
52
53 let spinner = CliService::spinner("Starting PostgreSQL container...");
54 let started = start_project(&docker, &container).await;
55 spinner.finish_and_clear();
56
57 if let Err(e) = started {
58 remove_project(&docker, &project).ok();
59 return Err(e);
60 }
61 CliService::success(&format!("PostgreSQL container '{project}' is ready"));
62
63 let database_url = container.database_url();
64
65 let id = new_local_tenant_id();
66 let tenant = StoredTenant::new_local_docker(id, name.clone(), database_url.clone(), project);
67
68 setup_local_profile(&tenant, &name, &database_url, prompter).await?;
69
70 Ok(tenant)
71}
72
73pub async fn create_external_tenant(prompter: &dyn Prompter) -> Result<StoredTenant> {
74 CliService::section("Create Local Tenant (External PostgreSQL)");
75
76 let name = prompter.input_with_default("Tenant name", "local")?;
77
78 if name.is_empty() {
79 bail!("Tenant name cannot be empty");
80 }
81
82 let database_url = prompter.input("PostgreSQL connection URL")?;
83
84 if database_url.is_empty() {
85 bail!("Database URL cannot be empty");
86 }
87
88 let spinner = CliService::spinner("Validating connection...");
89 let valid = validate_connection(&database_url).await;
90 spinner.finish_and_clear();
91
92 if !valid {
93 bail!("Could not connect to database. Check your connection URL and try again.");
94 }
95 CliService::success("Database connection verified");
96
97 let id = new_local_tenant_id();
98 let tenant = StoredTenant::new_local(id, name.clone(), database_url.clone());
99
100 setup_local_profile(&tenant, &name, &database_url, prompter).await?;
101
102 Ok(tenant)
103}
104
105async fn setup_local_profile(
106 tenant: &StoredTenant,
107 name: &str,
108 database_url: &str,
109 prompter: &dyn Prompter,
110) -> Result<()> {
111 CliService::section("Profile Setup");
112 let profile_name = prompter.input_with_default("Profile name", name)?;
113
114 CliService::section("API Keys");
115 let api_keys = collect_api_keys(prompter)?;
116
117 let profile = create_profile_for_tenant(prompter, tenant, &api_keys, &profile_name, None)?;
118 CliService::success(&format!("Profile '{}' created", profile.name));
119
120 let ctx = ProjectContext::discover();
121 ensure_project_scaffolding(ctx.root())?;
122
123 let cloud_user = get_cloud_user()?;
124 let profile_path = ctx.profile_dir(&profile.name).join("profile.yaml");
125 handle_local_tenant_setup(prompter, &cloud_user, database_url, name, &profile_path).await?;
126
127 Ok(())
128}