Skip to main content

systemprompt_cli/commands/cloud/
domain.rs

1//! `cloud domain` subcommands for managing a tenant's custom domain.
2//!
3//! Dispatches [`DomainCommands`] to set, inspect, or remove the custom domain
4//! via the cloud API, surfacing the DNS records the operator must configure.
5
6use 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}