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