systemprompt-cli 0.1.22

systemprompt.io OS - CLI for agent orchestration, AI operations, and system management
Documentation
use anyhow::Result;
use clap::{Args, Subcommand};

use super::types::{
    ConfigSection, ProviderInfo, ProviderListOutput, ProviderSetOutput, read_yaml_file,
    write_yaml_file,
};
use crate::CliConfig;
use crate::shared::{CommandResult, render_result};

#[derive(Debug, Subcommand)]
pub enum ProviderCommands {
    #[command(about = "List AI providers")]
    List(ListArgs),

    #[command(about = "Set default provider")]
    Set(SetArgs),

    #[command(about = "Enable a provider")]
    Enable(EnableArgs),

    #[command(about = "Disable a provider")]
    Disable(DisableArgs),
}

#[derive(Debug, Clone, Copy, Args)]
pub struct ListArgs;

#[derive(Debug, Clone, Args)]
pub struct SetArgs {
    #[arg(value_name = "PROVIDER")]
    pub provider: String,
}

#[derive(Debug, Clone, Args)]
pub struct EnableArgs {
    #[arg(value_name = "PROVIDER")]
    pub provider: String,
}

#[derive(Debug, Clone, Args)]
pub struct DisableArgs {
    #[arg(value_name = "PROVIDER")]
    pub provider: String,
}

pub fn execute(cmd: ProviderCommands, _config: &CliConfig) -> Result<()> {
    match cmd {
        ProviderCommands::List(_args) => {
            let result = list_providers()?;
            render_result(
                &CommandResult::table(serde_json::to_value(result)?).with_title("AI Providers"),
            );
        },
        ProviderCommands::Set(args) => {
            let result = set_default_provider(&args.provider)?;
            render_result(
                &CommandResult::card(serde_json::to_value(result)?).with_title("Provider Updated"),
            );
        },
        ProviderCommands::Enable(args) => {
            let result = set_provider_enabled(&args.provider, true)?;
            render_result(
                &CommandResult::card(serde_json::to_value(result)?).with_title("Provider Enabled"),
            );
        },
        ProviderCommands::Disable(args) => {
            let result = set_provider_enabled(&args.provider, false)?;
            render_result(
                &CommandResult::card(serde_json::to_value(result)?).with_title("Provider Disabled"),
            );
        },
    }
    Ok(())
}

fn get_ai_config_path() -> Result<std::path::PathBuf> {
    ConfigSection::Ai.file_path()
}

fn list_providers() -> Result<ProviderListOutput> {
    let file_path = get_ai_config_path()?;
    let content = read_yaml_file(&file_path)?;

    let ai = content
        .get("ai")
        .ok_or_else(|| anyhow::anyhow!("Missing 'ai' section in config"))?;

    let default_provider = ai
        .get("default_provider")
        .and_then(|v| v.as_str())
        .unwrap_or("unknown")
        .to_string();

    let providers_section = ai.get("providers");

    let mut providers = Vec::new();

    if let Some(serde_yaml::Value::Mapping(providers_map)) = providers_section {
        for (name, config) in providers_map {
            let name_str = name.as_str().unwrap_or("unknown").to_string();

            let enabled = config
                .get("enabled")
                .and_then(serde_yaml::Value::as_bool)
                .unwrap_or(true);

            let model = config
                .get("default_model")
                .and_then(|v| v.as_str())
                .unwrap_or("unknown")
                .to_string();

            let endpoint = config
                .get("endpoint")
                .and_then(|v| v.as_str())
                .map(String::from);

            providers.push(ProviderInfo {
                name: name_str.clone(),
                enabled,
                is_default: name_str == default_provider,
                model,
                endpoint,
            });
        }
    }

    Ok(ProviderListOutput {
        providers,
        default_provider,
    })
}

fn set_default_provider(provider: &str) -> Result<ProviderSetOutput> {
    let file_path = get_ai_config_path()?;
    let mut content = read_yaml_file(&file_path)?;

    let providers = content
        .get("ai")
        .and_then(|ai| ai.get("providers"))
        .ok_or_else(|| anyhow::anyhow!("Missing providers section"))?;

    if !providers
        .as_mapping()
        .is_some_and(|m| m.contains_key(serde_yaml::Value::String(provider.to_string())))
    {
        let available: Vec<String> = providers.as_mapping().map_or_else(Vec::new, |m| {
            m.keys()
                .filter_map(|k| k.as_str().map(String::from))
                .collect()
        });
        anyhow::bail!(
            "Unknown provider: '{}'. Available providers: {:?}",
            provider,
            available
        );
    }

    if let Some(serde_yaml::Value::Mapping(ai_map)) = content.get_mut("ai") {
        ai_map.insert(
            serde_yaml::Value::String("default_provider".to_string()),
            serde_yaml::Value::String(provider.to_string()),
        );
    }

    write_yaml_file(&file_path, &content)?;

    Ok(ProviderSetOutput {
        provider: provider.to_string(),
        action: "set_default".to_string(),
        message: format!("Default provider set to '{}'", provider),
    })
}

fn set_provider_enabled(provider: &str, enabled: bool) -> Result<ProviderSetOutput> {
    let file_path = get_ai_config_path()?;
    let mut content = read_yaml_file(&file_path)?;

    let ai = content
        .get_mut("ai")
        .ok_or_else(|| anyhow::anyhow!("Missing 'ai' section"))?;

    let providers = ai
        .get_mut("providers")
        .ok_or_else(|| anyhow::anyhow!("Missing 'providers' section"))?;

    let provider_config = providers
        .get_mut(provider)
        .ok_or_else(|| anyhow::anyhow!("Unknown provider: '{}'", provider))?;

    if let serde_yaml::Value::Mapping(config_map) = provider_config {
        config_map.insert(
            serde_yaml::Value::String("enabled".to_string()),
            serde_yaml::Value::Bool(enabled),
        );
    }

    write_yaml_file(&file_path, &content)?;

    let action = if enabled { "enabled" } else { "disabled" };

    Ok(ProviderSetOutput {
        provider: provider.to_string(),
        action: action.to_string(),
        message: format!("Provider '{}' {}", provider, action),
    })
}