Skip to main content

systemprompt_cli/commands/admin/config/server/
mod.rs

1//! `admin config server` command: show and edit profile server settings.
2//!
3//! [`ServerCommands`] reports and updates host, port, URLs, and HTTPS settings,
4//! and delegates the CORS allowed-origins list to the `cors` submodule. Changes
5//! persist to the active profile.
6
7mod cors;
8
9use anyhow::{Context, Result, bail};
10use clap::{Args, Subcommand};
11use std::fs;
12use systemprompt_config::ProfileBootstrap;
13use systemprompt_logging::CliService;
14use systemprompt_models::Profile;
15
16use super::types::{ServerConfigOutput, ServerSetOutput};
17use crate::CliConfig;
18use crate::cli_settings::OutputFormat;
19use crate::shared::{CommandOutput, render_result};
20
21#[derive(Debug, Subcommand)]
22pub enum ServerCommands {
23    #[command(about = "Show server configuration")]
24    Show,
25
26    #[command(about = "Set server configuration value")]
27    Set(SetArgs),
28
29    #[command(subcommand, about = "Manage CORS allowed origins")]
30    Cors(cors::CorsCommands),
31}
32
33#[derive(Debug, Clone, Args)]
34pub struct SetArgs {
35    #[arg(long, help = "Server host address")]
36    pub host: Option<String>,
37
38    #[arg(long, help = "Server port")]
39    pub port: Option<u16>,
40
41    #[arg(long, help = "Enable/disable HTTPS")]
42    pub use_https: Option<bool>,
43
44    #[arg(long, help = "API server URL")]
45    pub api_server_url: Option<String>,
46
47    #[arg(long, help = "API internal URL")]
48    pub api_internal_url: Option<String>,
49
50    #[arg(long, help = "API external URL")]
51    pub api_external_url: Option<String>,
52}
53
54pub fn execute(command: &ServerCommands, config: &CliConfig) -> Result<()> {
55    match command {
56        ServerCommands::Show => execute_show(config),
57        ServerCommands::Set(args) => execute_set(args, config),
58        ServerCommands::Cors(cmd) => cors::execute(cmd, config),
59    }
60}
61
62pub(super) fn execute_show(config: &CliConfig) -> Result<()> {
63    let profile = ProfileBootstrap::get()?;
64
65    let output = ServerConfigOutput {
66        host: profile.server.host.clone(),
67        port: profile.server.port,
68        api_server_url: profile.server.api_server_url.clone(),
69        api_internal_url: profile.server.api_internal_url.clone(),
70        api_external_url: profile.server.api_external_url.clone(),
71        use_https: profile.server.use_https,
72        cors_allowed_origins: profile.server.cors_allowed_origins.clone(),
73    };
74
75    render_result(
76        &CommandOutput::card_value("Server Configuration", &output),
77        config,
78    );
79
80    Ok(())
81}
82
83fn change(field: &str, old: String, new: String) -> ServerSetOutput {
84    ServerSetOutput {
85        field: field.to_owned(),
86        message: format!("Updated {field} to {new}"),
87        old_value: old,
88        new_value: new,
89    }
90}
91
92pub(super) fn execute_set(args: &SetArgs, config: &CliConfig) -> Result<()> {
93    if args.host.is_none()
94        && args.port.is_none()
95        && args.use_https.is_none()
96        && args.api_server_url.is_none()
97        && args.api_internal_url.is_none()
98        && args.api_external_url.is_none()
99    {
100        bail!(
101            "Must specify at least one option: --host, --port, --use-https, --api-server-url, \
102             --api-internal-url, --api-external-url"
103        );
104    }
105
106    let profile_path = ProfileBootstrap::get_path()?;
107    let mut profile = load_profile(profile_path)?;
108
109    let mut changes: Vec<ServerSetOutput> = Vec::new();
110
111    if let Some(ref host) = args.host {
112        let old = profile.server.host.clone();
113        profile.server.host.clone_from(host);
114        changes.push(change("host", old, host.clone()));
115    }
116
117    if let Some(port) = args.port {
118        let old = profile.server.port;
119        profile.server.port = port;
120        changes.push(change("port", old.to_string(), port.to_string()));
121    }
122
123    if let Some(use_https) = args.use_https {
124        let old = profile.server.use_https;
125        profile.server.use_https = use_https;
126        changes.push(change("use_https", old.to_string(), use_https.to_string()));
127    }
128
129    if let Some(ref url) = args.api_server_url {
130        let old = profile.server.api_server_url.clone();
131        profile.server.api_server_url.clone_from(url);
132        changes.push(change("api_server_url", old, url.clone()));
133    }
134
135    if let Some(ref url) = args.api_internal_url {
136        let old = profile.server.api_internal_url.clone();
137        profile.server.api_internal_url.clone_from(url);
138        changes.push(change("api_internal_url", old, url.clone()));
139    }
140
141    if let Some(ref url) = args.api_external_url {
142        let old = profile.server.api_external_url.clone();
143        profile.server.api_external_url.clone_from(url);
144        changes.push(change("api_external_url", old, url.clone()));
145    }
146
147    save_profile(&profile, profile_path)?;
148
149    for change in &changes {
150        render_result(&CommandOutput::card_value("Server Updated", change), config);
151    }
152
153    if config.output_format() == OutputFormat::Table {
154        CliService::warning("Restart services for changes to take effect");
155    }
156
157    Ok(())
158}
159
160fn load_profile(path: &str) -> Result<Profile> {
161    let content =
162        fs::read_to_string(path).with_context(|| format!("Failed to read profile: {}", path))?;
163    let profile: Profile = serde_yaml::from_str(&content)
164        .with_context(|| format!("Failed to parse profile: {}", path))?;
165    Ok(profile)
166}
167
168pub(super) fn save_profile(profile: &Profile, path: &str) -> Result<()> {
169    let content = serde_yaml::to_string(profile).context("Failed to serialize profile")?;
170    fs::write(path, content).with_context(|| format!("Failed to write profile: {}", path))?;
171    Ok(())
172}