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
#![allow(clippy::single_match_else)]

use anyhow::{Context, Result};
use systemprompt_cloud::{CredentialsBootstrap, ProfilePath, SessionKey, SessionStore};
use systemprompt_logging::CliService;
use systemprompt_models::{Profile, Secrets};

use super::login::{self, LoginArgs};
use super::types::SwitchOutput;
use crate::CliConfig;
use crate::paths::ResolvedPaths;
use crate::shared::{CommandResult, render_result};

pub async fn execute(
    profile_name: &str,
    _config: &CliConfig,
) -> Result<CommandResult<SwitchOutput>> {
    let paths = ResolvedPaths::discover();
    let profiles_dir = paths.profiles_dir();

    let target_dir = profiles_dir.join(profile_name);
    let profile_config_path = ProfilePath::Config.resolve(&target_dir);

    if !profile_config_path.exists() {
        anyhow::bail!(
            "Profile '{}' not found.\n\nAvailable profiles can be listed with: systemprompt admin \
             session list",
            profile_name
        );
    }

    let new_profile = load_profile(&profile_config_path)?;
    let new_tenant_id = new_profile.cloud.as_ref().and_then(|c| c.tenant_id.clone());
    let session_key = SessionKey::from_tenant_id(new_tenant_id.as_deref());

    let sessions_dir = paths.sessions_dir();
    let mut store = SessionStore::load_or_create(&sessions_dir)?;

    let previous_profile = store.active_profile_name.clone();

    store.set_active_with_profile_path(&session_key, profile_name, profile_config_path.clone());
    store.save(&sessions_dir)?;

    let has_session = store.get_valid_session(&session_key).is_some();
    if !has_session {
        CliService::info("No active session for this profile, logging in...");

        let email = CredentialsBootstrap::require()
            .context("Cloud credentials required for auto-login")?
            .user_email
            .clone();

        let secrets_path = ProfilePath::Secrets.resolve(&target_dir);
        let secrets = Secrets::load_from_path(&secrets_path)?;
        let profile_path_str = profile_config_path
            .to_str()
            .context("Invalid profile path")?;

        let args = LoginArgs {
            email: Some(email),
            duration_hours: 24,
            token_only: false,
            force_new: false,
        };

        let result =
            login::login_for_profile(&new_profile, profile_path_str, &secrets, &args).await?;
        render_result(&result);
    }

    let output = SwitchOutput {
        previous_profile,
        new_profile: profile_name.to_string(),
        session_key: session_key.as_storage_key(),
        tenant: new_tenant_id,
        message: format!("Switched to profile '{}'", profile_name),
    };

    Ok(CommandResult::text(output).with_title("Switch Profile"))
}

fn load_profile(path: &std::path::Path) -> Result<Profile> {
    let content = std::fs::read_to_string(path).context("Failed to read profile")?;
    Profile::parse(&content, path).context("Failed to parse profile")
}