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_manifest::Profile;
15use systemprompt_manifest::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()?;
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()?;
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 = format_str.parse()?;
115        let old = profile.runtime.output_format.to_string();
116        profile.runtime.output_format = format;
117        changes.push(RuntimeSetOutput {
118            field: "output_format".to_owned(),
119            old_value: old,
120            new_value: format.to_string(),
121            message: format!("Updated output_format to {}", format),
122        });
123    }
124
125    if let Some(no_color) = args.no_color {
126        let old = profile.runtime.no_color;
127        profile.runtime.no_color = no_color;
128        changes.push(RuntimeSetOutput {
129            field: "no_color".to_owned(),
130            old_value: old.to_string(),
131            new_value: no_color.to_string(),
132            message: format!("Updated no_color to {}", no_color),
133        });
134    }
135
136    save_profile(&profile, profile_path)?;
137
138    for change in &changes {
139        render_result(
140            &CommandOutput::card_value("Runtime Updated", change),
141            config,
142        );
143    }
144
145    if config.output_format() == OutputFormat::Table {
146        CliService::warning("Restart services for changes to take effect");
147    }
148
149    Ok(())
150}
151
152fn load_profile(path: &str) -> Result<Profile> {
153    let content =
154        fs::read_to_string(path).with_context(|| format!("Failed to read profile: {}", path))?;
155    let profile: Profile = serde_yaml::from_str(&content)
156        .with_context(|| format!("Failed to parse profile: {}", path))?;
157    Ok(profile)
158}
159
160pub(super) fn save_profile(profile: &Profile, path: &str) -> Result<()> {
161    let content = serde_yaml::to_string(profile).context("Failed to serialize profile")?;
162    fs::write(path, content).with_context(|| format!("Failed to write profile: {}", path))?;
163    Ok(())
164}