systemprompt-cli 0.15.2

Unified CLI for systemprompt.io AI governance: agent orchestration, MCP governance, analytics, profiles, cloud deploy, and self-hosted operations.
Documentation
use std::path::Path;

use anyhow::{Context, Result};

use systemprompt_cloud::{
    CliSession, CredentialsBootstrap, SessionIdentity, SessionKey, SessionStore,
};
use systemprompt_database::DbPool;
use systemprompt_identifiers::{ContextId, SessionId, UserId};
use systemprompt_logging::CliService;
use systemprompt_users::{User, UserRole, UserService};

use super::login::{LoginArgs, LoginOutput};
use crate::shared::CommandOutput;

pub(super) async fn try_use_existing_session(
    sessions_dir: &Path,
    session_key: &SessionKey,
    args: &LoginArgs,
    db_pool: &DbPool,
) -> Result<Option<CommandOutput>> {
    let mut store = SessionStore::load_or_create(sessions_dir)?;

    let Some(session) = store.get_valid_session(session_key) else {
        if !args.token_only {
            CliService::info("No valid session found, creating new session...");
        }
        return Ok(None);
    };

    let session_id = session.session_id.clone();
    let user_id = session.user_id.clone();
    let user_email = session.user_email.to_string();
    let session_token = session.session_token.clone();

    let user_service = UserService::new(db_pool)?;
    let exists = user_service
        .session_exists(&session_id)
        .await
        .unwrap_or(false);

    if !exists {
        if !args.token_only {
            CliService::info(
                "Cached session is stale (not found in database), creating new session...",
            );
        }
        store.remove_session(session_key);
        store.save(sessions_dir)?;
        return Ok(None);
    }

    let output = LoginOutput {
        status: "existing".to_owned(),
        user_id,
        email: user_email,
        session_id,
        expires_in_hours: 24,
    };

    if args.token_only {
        CliService::output(session_token.as_str());
        return Ok(Some(
            CommandOutput::card_value("Admin Session", &output).with_skip_render(),
        ));
    }

    CliService::success("Using existing valid session");
    Ok(Some(CommandOutput::card_value("Admin Session", &output)))
}

pub async fn fetch_admin_user(
    db_pool: &DbPool,
    admin_name: &str,
    is_cloud_profile: bool,
    email_override: Option<&str>,
) -> Result<User> {
    let user_service = UserService::new(db_pool)?;

    if let Some(user) = user_service
        .find_by_name(admin_name)
        .await
        .context("Failed to fetch admin user")?
    {
        if !user.is_admin() {
            anyhow::bail!(
                "User '{}' exists but is not an admin. Contact your administrator.",
                admin_name
            );
        }
        return Ok(user);
    }

    if !is_cloud_profile {
        anyhow::bail!(
            "Local admin user '{}' not found. Run 'systemprompt admin bootstrap --email \
             <email>' to create it.",
            admin_name
        );
    }

    let email = match email_override {
        Some(e) => e.to_owned(),
        None => cloud_credentials_email().await?,
    };

    CliService::info(&format!(
        "Admin user '{}' not found, creating it for cloud profile...",
        admin_name
    ));

    let user = user_service
        .create(admin_name, &email, None, None)
        .await
        .context("Failed to create user")?;

    let user = user_service
        .assign_roles(&user.id, &[UserRole::Admin.as_str().to_owned()])
        .await
        .context("Failed to assign admin role")?;

    CliService::success(&format!("Created admin user: {admin_name} <{email}>"));
    Ok(user)
}

async fn cloud_credentials_email() -> Result<String> {
    CredentialsBootstrap::try_init()
        .await
        .context("Failed to initialize credentials")?;
    let creds = CredentialsBootstrap::require().map_err(|_e| {
        anyhow::anyhow!(
            "No credentials found. Run 'systemprompt cloud auth login' first to authenticate."
        )
    })?;
    Ok(creds.user_email.clone())
}

pub(super) struct SessionStoreParams<'a> {
    pub sessions_dir: &'a Path,
    pub session_key: &'a SessionKey,
    pub profile_path: &'a str,
    pub session_token: systemprompt_identifiers::SessionToken,
    pub session_id: SessionId,
    pub context_id: ContextId,
    pub user_id: UserId,
    pub user_email: &'a str,
    pub user_type: systemprompt_models::auth::UserType,
}

pub(super) fn save_session_to_store(params: SessionStoreParams<'_>) -> Result<()> {
    let SessionStoreParams {
        sessions_dir,
        session_key,
        profile_path,
        session_token,
        session_id,
        context_id,
        user_id,
        user_email,
        user_type,
    } = params;
    let mut store = SessionStore::load_or_create(sessions_dir)?;

    let profile_dir = Path::new(profile_path).parent();
    let profile_name_str = profile_dir
        .and_then(|d| d.file_name())
        .and_then(|n| n.to_str())
        .context("Invalid profile path")?;

    let profile_name = systemprompt_identifiers::ProfileName::try_new(profile_name_str)
        .map_err(|e| anyhow::anyhow!("Invalid profile name: {}", e))?;

    let email = systemprompt_identifiers::Email::try_new(user_email)
        .map_err(|e| anyhow::anyhow!("Invalid email: {}", e))?;

    let cli_session = CliSession::builder(
        profile_name,
        session_token,
        session_id,
        context_id,
        SessionIdentity::new(user_id, email, user_type),
    )
    .with_session_key(session_key)
    .with_profile_path(profile_path)
    .build();

    store.upsert_session(session_key, cli_session);
    store.set_active_with_profile(session_key, profile_name_str);
    store.save(sessions_dir)?;

    tracing::debug!(sessions_dir = %sessions_dir.display(), "session saved to index.json");
    Ok(())
}