systemprompt_cli/commands/cloud/
domain.rs1use anyhow::{Result, bail};
7use clap::Subcommand;
8use systemprompt_cloud::CloudApiClient;
9use systemprompt_config::ProfileBootstrap;
10use systemprompt_identifiers::TenantId;
11use systemprompt_logging::CliService;
12
13use super::tenant::get_credentials;
14use crate::cli_settings::CliConfig;
15use crate::context::CommandContext;
16use crate::interactive::Prompter;
17
18#[derive(Debug, Subcommand)]
19pub enum DomainCommands {
20 #[command(about = "Set custom domain for tenant")]
21 Set {
22 #[arg(help = "Domain name (e.g., example.com)")]
23 domain: String,
24 },
25
26 #[command(about = "Check custom domain status")]
27 Status,
28
29 #[command(about = "Remove custom domain")]
30 Remove {
31 #[arg(short = 'y', long, help = "Skip confirmation")]
32 yes: bool,
33 },
34}
35
36pub(super) async fn execute(cmd: DomainCommands, ctx: &CommandContext) -> Result<()> {
37 match cmd {
38 DomainCommands::Set { domain } => set_domain(domain).await,
39 DomainCommands::Status => get_status().await,
40 DomainCommands::Remove { yes } => remove_domain(yes, ctx.prompter(), &ctx.cli).await,
41 }
42}
43
44pub(super) fn get_tenant_id() -> Result<TenantId> {
45 let profile =
46 ProfileBootstrap::get().map_err(|_e| anyhow::anyhow!("Profile not initialized"))?;
47
48 let cloud = profile
49 .cloud
50 .as_ref()
51 .ok_or_else(|| anyhow::anyhow!("Cloud not configured in profile"))?;
52
53 cloud
54 .tenant_id
55 .as_ref()
56 .map(TenantId::new)
57 .ok_or_else(|| anyhow::anyhow!("No tenant_id in profile. Create a cloud tenant first."))
58}
59
60async fn set_domain(domain: String) -> Result<()> {
61 CliService::section("Set Custom Domain");
62
63 let tenant_id = get_tenant_id()?;
64 let creds = get_credentials()?;
65 let client = CloudApiClient::new(&creds.api_url, creds.api_token.as_str())?;
66
67 let spinner = CliService::spinner(&format!("Configuring domain {}...", domain));
68 match client.set_custom_domain(&tenant_id, &domain).await {
69 Ok(response) => {
70 spinner.finish_and_clear();
71
72 CliService::success("Custom Domain Configuration");
73 CliService::info("");
74 CliService::info(&format!(" Domain: {}", response.domain));
75 CliService::info(&format!(" Status: {}", response.status));
76 CliService::info(&format!(" DNS Target: {}", response.dns_target));
77 CliService::info("");
78
79 CliService::info("DNS Configuration Required:");
80 CliService::info(&format!(
81 " Type: {}",
82 response.dns_instructions.record_type
83 ));
84 CliService::info(&format!(" Host: {}", response.dns_instructions.host));
85 CliService::info(&format!(" Value: {}", response.dns_instructions.value));
86 CliService::info(&format!(" TTL: {}", response.dns_instructions.ttl));
87 CliService::info("");
88
89 CliService::info(
90 "After configuring DNS, run 'systemprompt cloud domain status' to verify.",
91 );
92 },
93 Err(e) => {
94 spinner.finish_and_clear();
95 bail!("Failed to set custom domain: {}", e);
96 },
97 }
98
99 Ok(())
100}
101
102async fn get_status() -> Result<()> {
103 CliService::section("Custom Domain Status");
104
105 let tenant_id = get_tenant_id()?;
106 let creds = get_credentials()?;
107 let client = CloudApiClient::new(&creds.api_url, creds.api_token.as_str())?;
108
109 let spinner = CliService::spinner("Checking domain status...");
110 match client.get_custom_domain(&tenant_id).await {
111 Ok(response) => {
112 spinner.finish_and_clear();
113
114 CliService::info("");
115 CliService::info(&format!(" Domain: {}", response.domain));
116 CliService::info(&format!(" Status: {}", response.status));
117 CliService::info(&format!(
118 " Verified: {}",
119 if response.verified { "Yes" } else { "No" }
120 ));
121 CliService::info(&format!(" DNS Target: {}", response.dns_target));
122
123 if let Some(created) = &response.created_at {
124 CliService::info(&format!(" Created: {}", created));
125 }
126 if let Some(verified) = &response.verified_at {
127 CliService::info(&format!(" Verified: {}", verified));
128 }
129 CliService::info("");
130
131 if !response.verified {
132 CliService::info("DNS Configuration Required:");
133 CliService::info(&format!(
134 " Type: {}",
135 response.dns_instructions.record_type
136 ));
137 CliService::info(&format!(" Host: {}", response.dns_instructions.host));
138 CliService::info(&format!(" Value: {}", response.dns_instructions.value));
139 CliService::info(&format!(" TTL: {}", response.dns_instructions.ttl));
140 CliService::info("");
141 }
142 },
143 Err(e) => {
144 spinner.finish_and_clear();
145 let err_str = e.to_string();
146 if err_str.contains("not_found") || err_str.contains("404") {
147 CliService::info("No custom domain configured for this tenant.");
148 CliService::info("Use 'systemprompt cloud domain set <domain>' to configure one.");
149 return Ok(());
150 }
151 bail!("Failed to get domain status: {}", e);
152 },
153 }
154
155 Ok(())
156}
157
158async fn remove_domain(yes: bool, prompter: &dyn Prompter, config: &CliConfig) -> Result<()> {
159 CliService::section("Remove Custom Domain");
160
161 let tenant_id = get_tenant_id()?;
162 let creds = get_credentials()?;
163 let client = CloudApiClient::new(&creds.api_url, creds.api_token.as_str())?;
164
165 let domain_name = match client.get_custom_domain(&tenant_id).await {
166 Ok(response) => response.domain,
167 Err(e) => {
168 let err_str = e.to_string();
169 if err_str.contains("not_found") || err_str.contains("404") {
170 CliService::info("No custom domain configured for this tenant.");
171 return Ok(());
172 }
173 bail!("Failed to get domain status: {}", e);
174 },
175 };
176
177 if !yes {
178 if !config.is_interactive() {
179 return Err(anyhow::anyhow!(
180 "--yes is required in non-interactive mode for domain removal"
181 ));
182 }
183
184 let confirm = prompter.confirm(
185 &format!(
186 "Remove custom domain '{}'? This will delete the TLS certificate.",
187 domain_name
188 ),
189 false,
190 )?;
191
192 if !confirm {
193 CliService::info("Cancelled");
194 return Ok(());
195 }
196 }
197
198 let spinner = CliService::spinner(&format!("Removing domain {}...", domain_name));
199 match client.delete_custom_domain(&tenant_id).await {
200 Ok(()) => {
201 spinner.finish_and_clear();
202 CliService::success(&format!(
203 "Custom domain '{}' removed successfully",
204 domain_name
205 ));
206 },
207 Err(e) => {
208 spinner.finish_and_clear();
209 bail!("Failed to remove custom domain: {}", e);
210 },
211 }
212
213 Ok(())
214}