systemprompt_cli/commands/admin/config/
server.rs1use 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}