Skip to main content

systemprompt_cli/commands/admin/config/
runtime.rs

1//! `admin config runtime` command: show and edit profile runtime settings.
2//!
3//! [`RuntimeCommands`] reports and updates the environment, log level, output
4//! format, and colour settings, persisting changes to the active profile.
5//!
6//! Copyright (c) systemprompt.io — Business Source License 1.1.
7//! See <https://systemprompt.io> for licensing details.
8
9use anyhow::{Context, Result, bail};
10use clap::{Args, Subcommand};
11use std::fs;
12use systemprompt_config::ProfileBootstrap;
13use systemprompt_logging::CliService;
14use systemprompt_models::Profile;
15use systemprompt_models::profile::{Environment, LogLevel, OutputFormat as ProfileOutputFormat};
16
17use super::types::{RuntimeConfigOutput, RuntimeSetOutput};
18use crate::CliConfig;
19use crate::cli_settings::OutputFormat;
20use crate::shared::{CommandOutput, render_result};
21
22#[derive(Debug, Subcommand)]
23pub enum RuntimeCommands {
24    #[command(about = "Show runtime configuration", alias = "list")]
25    Show,
26
27    #[command(about = "Set runtime configuration value")]
28    Set(SetArgs),
29}
30
31#[derive(Debug, Clone, Args)]
32pub struct SetArgs {
33    #[arg(long, help = "Environment: development, test, staging, production")]
34    pub environment: Option<String>,
35
36    #[arg(long, help = "Log level: quiet, normal, verbose, debug")]
37    pub log_level: Option<String>,
38
39    #[arg(long, help = "Output format: text, json, yaml")]
40    pub output_format: Option<String>,
41
42    #[arg(long, help = "Disable colored output")]
43    pub no_color: Option<bool>,
44}
45
46pub fn execute(command: RuntimeCommands, config: &CliConfig) -> Result<()> {
47    match command {
48        RuntimeCommands::Show => execute_show(config),
49        RuntimeCommands::Set(args) => execute_set(args, config),
50    }
51}
52
53pub(super) fn execute_show(config: &CliConfig) -> Result<()> {
54    let profile = ProfileBootstrap::get()?;
55
56    let output = RuntimeConfigOutput {
57        environment: profile.runtime.environment.to_string(),
58        log_level: profile.runtime.log_level.to_string(),
59        output_format: profile.runtime.output_format.to_string(),
60        no_color: profile.runtime.no_color,
61        non_interactive: profile.runtime.non_interactive,
62    };
63
64    render_result(
65        &CommandOutput::card_value("Runtime Configuration", &output),
66        config,
67    );
68
69    Ok(())
70}
71
72pub(super) fn execute_set(args: SetArgs, config: &CliConfig) -> Result<()> {
73    if args.environment.is_none()
74        && args.log_level.is_none()
75        && args.output_format.is_none()
76        && args.no_color.is_none()
77    {
78        bail!(
79            "Must specify at least one option: --environment, --log-level, --output-format, \
80             --no-color"
81        );
82    }
83
84    let profile_path = ProfileBootstrap::get_path()?;
85    let mut profile = load_profile(profile_path)?;
86
87    let mut changes: Vec<RuntimeSetOutput> = Vec::new();
88
89    if let Some(env_str) = args.environment {
90        let env: Environment = env_str.parse().map_err(|e: String| anyhow::anyhow!(e))?;
91        let old = profile.runtime.environment.to_string();
92        profile.runtime.environment = env;
93        changes.push(RuntimeSetOutput {
94            field: "environment".to_owned(),
95            old_value: old,
96            new_value: env.to_string(),
97            message: format!("Updated environment to {}", env),
98        });
99    }
100
101    if let Some(level_str) = args.log_level {
102        let level: LogLevel = level_str.parse().map_err(|e: String| anyhow::anyhow!(e))?;
103        let old = profile.runtime.log_level.to_string();
104        profile.runtime.log_level = level;
105        changes.push(RuntimeSetOutput {
106            field: "log_level".to_owned(),
107            old_value: old,
108            new_value: level.to_string(),
109            message: format!("Updated log_level to {}", level),
110        });
111    }
112
113    if let Some(format_str) = args.output_format {
114        let format: ProfileOutputFormat =
115            format_str.parse().map_err(|e: String| anyhow::anyhow!(e))?;
116        let old = profile.runtime.output_format.to_string();
117        profile.runtime.output_format = format;
118        changes.push(RuntimeSetOutput {
119            field: "output_format".to_owned(),
120            old_value: old,
121            new_value: format.to_string(),
122            message: format!("Updated output_format to {}", format),
123        });
124    }
125
126    if let Some(no_color) = args.no_color {
127        let old = profile.runtime.no_color;
128        profile.runtime.no_color = no_color;
129        changes.push(RuntimeSetOutput {
130            field: "no_color".to_owned(),
131            old_value: old.to_string(),
132            new_value: no_color.to_string(),
133            message: format!("Updated no_color to {}", no_color),
134        });
135    }
136
137    save_profile(&profile, profile_path)?;
138
139    for change in &changes {
140        render_result(
141            &CommandOutput::card_value("Runtime Updated", change),
142            config,
143        );
144    }
145
146    if config.output_format() == OutputFormat::Table {
147        CliService::warning("Restart services for changes to take effect");
148    }
149
150    Ok(())
151}
152
153fn load_profile(path: &str) -> Result<Profile> {
154    let content =
155        fs::read_to_string(path).with_context(|| format!("Failed to read profile: {}", path))?;
156    let profile: Profile = serde_yaml::from_str(&content)
157        .with_context(|| format!("Failed to parse profile: {}", path))?;
158    Ok(profile)
159}
160
161pub(super) fn save_profile(profile: &Profile, path: &str) -> Result<()> {
162    let content = serde_yaml::to_string(profile).context("Failed to serialize profile")?;
163    fs::write(path, content).with_context(|| format!("Failed to write profile: {}", path))?;
164    Ok(())
165}