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