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