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 dialoguer::theme::ColorfulTheme;
use dialoguer::{Input, Password, Select};
use std::path::Path;
use systemprompt_cloud::ProfilePath;
use systemprompt_logging::CliService;
use systemprompt_models::Profile;

pub fn edit_api_keys(profile_dir: &Path) -> Result<()> {
    CliService::section("API Keys (secrets.json)");

    let secrets_path = ProfilePath::Secrets.resolve(profile_dir);

    if !secrets_path.exists() {
        CliService::warning("No secrets.json found in this profile.");
        return Ok(());
    }

    let content = std::fs::read_to_string(&secrets_path)?;
    let mut secrets: serde_json::Value = serde_json::from_str(&content)?;

    let key_options = vec![
        "Gemini API Key",
        "Anthropic API Key",
        "OpenAI API Key",
        "Database URL",
        "Done",
    ];

    loop {
        let selection = Select::with_theme(&ColorfulTheme::default())
            .with_prompt("Select key to edit")
            .items(&key_options)
            .default(0)
            .interact()?;

        match selection {
            0 => edit_gemini_key(&mut secrets)?,
            1 => edit_anthropic_key(&mut secrets)?,
            2 => edit_openai_key(&mut secrets)?,
            3 => edit_database_url(&mut secrets)?,
            4 => break,
            _ => unreachable!(),
        }
    }

    let content = serde_json::to_string_pretty(&secrets)?;
    std::fs::write(&secrets_path, content)?;

    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let permissions = std::fs::Permissions::from_mode(0o600);
        std::fs::set_permissions(&secrets_path, permissions)?;
    }

    CliService::success("API keys updated");
    Ok(())
}

fn edit_gemini_key(secrets: &mut serde_json::Value) -> Result<()> {
    let current = secrets.get("gemini").and_then(|v| v.as_str()).unwrap_or("");
    let masked = if current.is_empty() {
        "(not set)"
    } else {
        "***"
    };
    CliService::info(&format!("Current: {}", masked));

    let new_key = Password::with_theme(&ColorfulTheme::default())
        .with_prompt("New Gemini API Key (empty to skip)")
        .allow_empty_password(true)
        .interact()?;

    if !new_key.is_empty() {
        secrets["gemini"] = serde_json::Value::String(new_key);
    }
    Ok(())
}

fn edit_anthropic_key(secrets: &mut serde_json::Value) -> Result<()> {
    let current = secrets
        .get("anthropic")
        .and_then(|v| v.as_str())
        .unwrap_or("");
    let masked = if current.is_empty() {
        "(not set)"
    } else {
        "***"
    };
    CliService::info(&format!("Current: {}", masked));

    let new_key = Password::with_theme(&ColorfulTheme::default())
        .with_prompt("New Anthropic API Key (empty to skip)")
        .allow_empty_password(true)
        .interact()?;

    if !new_key.is_empty() {
        secrets["anthropic"] = serde_json::Value::String(new_key);
    }
    Ok(())
}

fn edit_openai_key(secrets: &mut serde_json::Value) -> Result<()> {
    let current = secrets.get("openai").and_then(|v| v.as_str()).unwrap_or("");
    let masked = if current.is_empty() {
        "(not set)"
    } else {
        "***"
    };
    CliService::info(&format!("Current: {}", masked));

    let new_key = Password::with_theme(&ColorfulTheme::default())
        .with_prompt("New OpenAI API Key (empty to skip)")
        .allow_empty_password(true)
        .interact()?;

    if !new_key.is_empty() {
        secrets["openai"] = serde_json::Value::String(new_key);
    }
    Ok(())
}

fn edit_database_url(secrets: &mut serde_json::Value) -> Result<()> {
    let current = secrets
        .get("database_url")
        .and_then(|v| v.as_str())
        .unwrap_or("");
    CliService::info(&format!("Current: {}", Profile::mask_database_url(current)));

    let new_url: String = Input::with_theme(&ColorfulTheme::default())
        .with_prompt("New Database URL (empty to skip)")
        .allow_empty(true)
        .interact_text()?;

    if !new_url.is_empty() {
        secrets["database_url"] = serde_json::Value::String(new_url);
    }
    Ok(())
}