Skip to main content

systemprompt_cli/commands/admin/config/
server.rs

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