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//!
6//! Copyright (c) systemprompt.io — Business Source License 1.1.
7//! See <https://systemprompt.io> for licensing details.
8
9use 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}