systemprompt-cli 0.2.2

Unified CLI for systemprompt.io AI governance: agent orchestration, MCP governance, analytics, profiles, cloud deploy, and self-hosted operations.
Documentation
use anyhow::Result;
use std::collections::HashMap;
use systemprompt_loader::ConfigLoader;
use systemprompt_logging::CliService;
use systemprompt_models::{AiConfig, AppPaths, Config, ContentConfigRaw, SkillsConfig};

use super::ShowFilter;
use super::show_display::print_formatted_config;
use super::show_types::{FullConfig, SettingsOutput, build_env_config};
use crate::cli_settings::CliConfig;
use crate::shared::{CommandResult, render_result, resolve_profile_path};

pub fn execute(
    name: Option<&str>,
    filter: ShowFilter,
    json_output: bool,
    yaml_output: bool,
    _cli_config: &CliConfig,
) -> Result<()> {
    let profile_path = resolve_profile_path(name, None)?;

    CliService::section(&format!("Profile: {}", profile_path.display()));

    let config = Config::get().ok().or_else(|| {
        if initialize_config_from_profile(&profile_path).is_ok() {
            Config::get().ok()
        } else {
            None
        }
    });

    let services_config = ConfigLoader::load().ok();

    let full_config = build_config_for_filter(filter, config, services_config.as_ref());

    output_config(&full_config, json_output, yaml_output);
    Ok(())
}

fn initialize_config_from_profile(profile_path: &std::path::Path) -> Result<()> {
    use systemprompt_models::{ProfileBootstrap, SecretsBootstrap};

    ProfileBootstrap::init_from_path(profile_path)?;
    SecretsBootstrap::init()?;
    let profile = ProfileBootstrap::get()?;
    AppPaths::init(&profile.paths)?;
    Config::try_init()?;
    Ok(())
}

fn build_config_for_filter(
    filter: ShowFilter,
    config: Option<&Config>,
    services_config: Option<&systemprompt_models::ServicesConfig>,
) -> FullConfig {
    match filter {
        ShowFilter::All => build_full_config(config, services_config),
        ShowFilter::Agents => FullConfig::empty()
            .with_agents(services_config.map_or_else(HashMap::new, |s| s.agents.clone())),
        ShowFilter::Mcp => FullConfig::empty()
            .with_mcp_servers(services_config.map_or_else(HashMap::new, |s| s.mcp_servers.clone())),
        ShowFilter::Skills => {
            let mut full = FullConfig::empty();
            if let Some(cfg) = config {
                if let Some(skills) = load_skills_config(cfg) {
                    full = full.with_skills(skills);
                }
            }
            full
        },
        ShowFilter::Ai => FullConfig::empty()
            .with_ai(services_config.map_or_else(AiConfig::default, |s| s.ai.clone())),
        ShowFilter::Web => {
            FullConfig::empty().with_web(services_config.and_then(|s| s.web.clone()))
        },
        ShowFilter::Content => {
            let mut full = FullConfig::empty();
            if let Some(content) = load_content_config() {
                full = full.with_content(content);
            }
            full
        },
        ShowFilter::Env => config.map_or_else(FullConfig::empty, |cfg| {
            FullConfig::empty().with_environment(build_env_config(cfg))
        }),
        ShowFilter::Settings => {
            let mut full = FullConfig::empty();
            if let Some(settings) = services_config.map(build_settings_output) {
                full = full.with_settings(settings);
            }
            full
        },
    }
}

fn build_full_config(
    config: Option<&Config>,
    services_config: Option<&systemprompt_models::ServicesConfig>,
) -> FullConfig {
    let mut full = FullConfig::empty();

    if let Some(cfg) = config {
        full = full.with_environment(build_env_config(cfg));
        if let Some(skills) = load_skills_config(cfg) {
            full = full.with_skills(skills);
        }
    }

    if let Some(sc) = services_config {
        full = full
            .with_settings(build_settings_output(sc))
            .with_agents(sc.agents.clone())
            .with_mcp_servers(sc.mcp_servers.clone())
            .with_ai(sc.ai.clone())
            .with_web(sc.web.clone());
    }

    if let Some(content) = load_content_config() {
        full = full.with_content(content);
    }

    full
}

fn build_settings_output(services_config: &systemprompt_models::ServicesConfig) -> SettingsOutput {
    SettingsOutput {
        agent_port_range: services_config.settings.agent_port_range,
        mcp_port_range: services_config.settings.mcp_port_range,
        auto_start_enabled: services_config.settings.auto_start_enabled,
        validation_strict: services_config.settings.validation_strict,
        schema_validation_mode: services_config.settings.schema_validation_mode.clone(),
    }
}

fn load_skills_config(_config: &Config) -> Option<SkillsConfig> {
    let skills_path = AppPaths::get().ok()?.system().skills().to_path_buf();
    if !skills_path.exists() {
        return None;
    }
    let config_file = skills_path.join("skills.yaml");
    if !config_file.exists() {
        return None;
    }

    let content = match std::fs::read_to_string(&config_file) {
        Ok(c) => c,
        Err(e) => {
            tracing::warn!(path = %config_file.display(), error = %e, "Failed to read skills config");
            return None;
        },
    };

    match serde_yaml::from_str(&content) {
        Ok(config) => Some(config),
        Err(e) => {
            tracing::warn!(path = %config_file.display(), error = %e, "Failed to parse skills config");
            None
        },
    }
}

fn load_content_config() -> Option<ContentConfigRaw> {
    let path = AppPaths::get()
        .ok()?
        .system()
        .content_config()
        .to_path_buf();
    if !path.exists() {
        return None;
    }

    let content = match std::fs::read_to_string(&path) {
        Ok(c) => c,
        Err(e) => {
            tracing::warn!(path = %path.display(), error = %e, "Failed to read content config");
            return None;
        },
    };

    match serde_yaml::from_str(&content) {
        Ok(config) => Some(config),
        Err(e) => {
            tracing::warn!(path = %path.display(), error = %e, "Failed to parse content config");
            None
        },
    }
}

fn output_config(config: &FullConfig, json_output: bool, yaml_output: bool) {
    if json_output {
        let result = CommandResult::card(config).with_title("Profile Configuration");
        render_result(&result);
    } else if yaml_output {
        CliService::yaml(config);
    } else {
        print_formatted_config(config);
    }
}