Skip to main content

systemprompt_cli/commands/admin/config/
server.rs

1use anyhow::{Context, Result, bail};
2use clap::{Args, Subcommand};
3use std::fs;
4use systemprompt_config::ProfileBootstrap;
5use systemprompt_logging::CliService;
6use systemprompt_models::Profile;
7
8use super::types::{CorsListOutput, CorsModifyOutput, ServerConfigOutput, ServerSetOutput};
9use crate::CliConfig;
10use crate::cli_settings::OutputFormat;
11use crate::shared::{CommandResult, render_result};
12
13#[derive(Debug, Subcommand)]
14pub enum ServerCommands {
15    #[command(about = "Show server configuration")]
16    Show,
17
18    #[command(about = "Set server configuration value")]
19    Set(SetArgs),
20
21    #[command(subcommand, about = "Manage CORS allowed origins")]
22    Cors(CorsCommands),
23}
24
25#[derive(Debug, Clone, Args)]
26pub struct SetArgs {
27    #[arg(long, help = "Server host address")]
28    pub host: Option<String>,
29
30    #[arg(long, help = "Server port")]
31    pub port: Option<u16>,
32
33    #[arg(long, help = "Enable/disable HTTPS")]
34    pub use_https: Option<bool>,
35
36    #[arg(long, help = "API server URL")]
37    pub api_server_url: Option<String>,
38
39    #[arg(long, help = "API internal URL")]
40    pub api_internal_url: Option<String>,
41
42    #[arg(long, help = "API external URL")]
43    pub api_external_url: Option<String>,
44}
45
46#[derive(Debug, Subcommand)]
47pub enum CorsCommands {
48    #[command(about = "List CORS allowed origins")]
49    List,
50
51    #[command(about = "Add a CORS origin")]
52    Add(CorsAddArgs),
53
54    #[command(about = "Remove a CORS origin")]
55    Remove(CorsRemoveArgs),
56}
57
58#[derive(Debug, Clone, Args)]
59pub struct CorsAddArgs {
60    #[arg(help = "Origin URL to add (e.g., https://example.com)")]
61    pub origin: String,
62}
63
64#[derive(Debug, Clone, Args)]
65pub struct CorsRemoveArgs {
66    #[arg(help = "Origin URL to remove")]
67    pub origin: String,
68}
69
70pub fn execute(command: &ServerCommands, config: &CliConfig) -> Result<()> {
71    match command {
72        ServerCommands::Show => execute_show(config),
73        ServerCommands::Set(args) => execute_set(args, config),
74        ServerCommands::Cors(cmd) => execute_cors(cmd, config),
75    }
76}
77
78fn execute_show(_config: &CliConfig) -> Result<()> {
79    let profile = ProfileBootstrap::get()?;
80
81    let output = ServerConfigOutput {
82        host: profile.server.host.clone(),
83        port: profile.server.port,
84        api_server_url: profile.server.api_server_url.clone(),
85        api_internal_url: profile.server.api_internal_url.clone(),
86        api_external_url: profile.server.api_external_url.clone(),
87        use_https: profile.server.use_https,
88        cors_allowed_origins: profile.server.cors_allowed_origins.clone(),
89    };
90
91    render_result(&CommandResult::card(output).with_title("Server Configuration"));
92
93    Ok(())
94}
95
96fn execute_set(args: &SetArgs, config: &CliConfig) -> Result<()> {
97    if args.host.is_none()
98        && args.port.is_none()
99        && args.use_https.is_none()
100        && args.api_server_url.is_none()
101        && args.api_internal_url.is_none()
102        && args.api_external_url.is_none()
103    {
104        bail!(
105            "Must specify at least one option: --host, --port, --use-https, --api-server-url, \
106             --api-internal-url, --api-external-url"
107        );
108    }
109
110    let profile_path = ProfileBootstrap::get_path()?;
111    let mut profile = load_profile(profile_path)?;
112
113    let mut changes: Vec<ServerSetOutput> = Vec::new();
114
115    if let Some(ref host) = args.host {
116        let old = profile.server.host.clone();
117        profile.server.host.clone_from(host);
118        changes.push(ServerSetOutput {
119            field: "host".to_string(),
120            old_value: old,
121            new_value: host.clone(),
122            message: format!("Updated host to {}", host),
123        });
124    }
125
126    if let Some(port) = args.port {
127        let old = profile.server.port;
128        profile.server.port = port;
129        changes.push(ServerSetOutput {
130            field: "port".to_string(),
131            old_value: old.to_string(),
132            new_value: port.to_string(),
133            message: format!("Updated port to {}", port),
134        });
135    }
136
137    if let Some(use_https) = args.use_https {
138        let old = profile.server.use_https;
139        profile.server.use_https = use_https;
140        changes.push(ServerSetOutput {
141            field: "use_https".to_string(),
142            old_value: old.to_string(),
143            new_value: use_https.to_string(),
144            message: format!("Updated use_https to {}", use_https),
145        });
146    }
147
148    if let Some(ref url) = args.api_server_url {
149        let old = profile.server.api_server_url.clone();
150        profile.server.api_server_url.clone_from(url);
151        changes.push(ServerSetOutput {
152            field: "api_server_url".to_string(),
153            old_value: old,
154            new_value: url.clone(),
155            message: format!("Updated api_server_url to {}", url),
156        });
157    }
158
159    if let Some(ref url) = args.api_internal_url {
160        let old = profile.server.api_internal_url.clone();
161        profile.server.api_internal_url.clone_from(url);
162        changes.push(ServerSetOutput {
163            field: "api_internal_url".to_string(),
164            old_value: old,
165            new_value: url.clone(),
166            message: format!("Updated api_internal_url to {}", url),
167        });
168    }
169
170    if let Some(ref url) = args.api_external_url {
171        let old = profile.server.api_external_url.clone();
172        profile.server.api_external_url.clone_from(url);
173        changes.push(ServerSetOutput {
174            field: "api_external_url".to_string(),
175            old_value: old,
176            new_value: url.clone(),
177            message: format!("Updated api_external_url to {}", url),
178        });
179    }
180
181    save_profile(&profile, profile_path)?;
182
183    for change in &changes {
184        render_result(&CommandResult::text(change.clone()).with_title("Server Updated"));
185    }
186
187    if config.output_format() == OutputFormat::Table {
188        CliService::warning("Restart services for changes to take effect");
189    }
190
191    Ok(())
192}
193
194fn execute_cors(command: &CorsCommands, config: &CliConfig) -> Result<()> {
195    match command {
196        CorsCommands::List => execute_cors_list(),
197        CorsCommands::Add(args) => execute_cors_add(args, config),
198        CorsCommands::Remove(args) => execute_cors_remove(args, config),
199    }
200}
201
202fn execute_cors_list() -> Result<()> {
203    let profile = ProfileBootstrap::get()?;
204
205    let output = CorsListOutput {
206        origins: profile.server.cors_allowed_origins.clone(),
207        count: profile.server.cors_allowed_origins.len(),
208    };
209
210    render_result(&CommandResult::list(output).with_title("CORS Allowed Origins"));
211
212    Ok(())
213}
214
215fn execute_cors_add(args: &CorsAddArgs, config: &CliConfig) -> Result<()> {
216    let profile_path = ProfileBootstrap::get_path()?;
217    let mut profile = load_profile(profile_path)?;
218
219    if profile.server.cors_allowed_origins.contains(&args.origin) {
220        let output = CorsModifyOutput {
221            action: "skipped".to_string(),
222            origin: args.origin.clone(),
223            message: format!("Origin {} already exists", args.origin),
224        };
225        render_result(&CommandResult::text(output).with_title("CORS Origin"));
226        return Ok(());
227    }
228
229    profile
230        .server
231        .cors_allowed_origins
232        .push(args.origin.clone());
233    save_profile(&profile, profile_path)?;
234
235    let output = CorsModifyOutput {
236        action: "added".to_string(),
237        origin: args.origin.clone(),
238        message: format!("Added CORS origin: {}", args.origin),
239    };
240    render_result(&CommandResult::text(output).with_title("CORS Origin Added"));
241
242    if config.output_format() == OutputFormat::Table {
243        CliService::warning("Restart services for changes to take effect");
244    }
245
246    Ok(())
247}
248
249fn execute_cors_remove(args: &CorsRemoveArgs, config: &CliConfig) -> Result<()> {
250    let profile_path = ProfileBootstrap::get_path()?;
251    let mut profile = load_profile(profile_path)?;
252
253    let original_len = profile.server.cors_allowed_origins.len();
254    profile
255        .server
256        .cors_allowed_origins
257        .retain(|o| o != &args.origin);
258
259    if profile.server.cors_allowed_origins.len() == original_len {
260        let output = CorsModifyOutput {
261            action: "skipped".to_string(),
262            origin: args.origin.clone(),
263            message: format!("Origin {} not found", args.origin),
264        };
265        render_result(&CommandResult::text(output).with_title("CORS Origin"));
266        return Ok(());
267    }
268
269    save_profile(&profile, profile_path)?;
270
271    let output = CorsModifyOutput {
272        action: "removed".to_string(),
273        origin: args.origin.clone(),
274        message: format!("Removed CORS origin: {}", args.origin),
275    };
276    render_result(&CommandResult::text(output).with_title("CORS Origin Removed"));
277
278    if config.output_format() == OutputFormat::Table {
279        CliService::warning("Restart services for changes to take effect");
280    }
281
282    Ok(())
283}
284
285fn load_profile(path: &str) -> Result<Profile> {
286    let content =
287        fs::read_to_string(path).with_context(|| format!("Failed to read profile: {}", path))?;
288    let profile: Profile = serde_yaml::from_str(&content)
289        .with_context(|| format!("Failed to parse profile: {}", path))?;
290    Ok(profile)
291}
292
293fn save_profile(profile: &Profile, path: &str) -> Result<()> {
294    let content = serde_yaml::to_string(profile).context("Failed to serialize profile")?;
295    fs::write(path, content).with_context(|| format!("Failed to write profile: {}", path))?;
296    Ok(())
297}