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