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 std::path::PathBuf;

use anyhow::{Context, Result, bail};
use dialoguer::Select;
use dialoguer::theme::ColorfulTheme;
use systemprompt_cloud::{CloudPath, ProfilePath, ProjectContext, TenantStore, get_cloud_paths};
use systemprompt_loader::ProfileLoader;
use systemprompt_logging::CliService;
use systemprompt_models::Profile;

use crate::cli_settings::CliConfig;
use crate::shared::profile::{DiscoveredProfile, discover_profiles};

#[derive(Debug)]
pub struct DeployableProfile {
    pub name: String,
    pub path: PathBuf,
    pub profile: Profile,
    pub tenant_name: Option<String>,
    pub hostname: Option<String>,
}

pub fn discover_deployable_profiles(tenant_store: &TenantStore) -> Result<Vec<DeployableProfile>> {
    let profiles = discover_profiles()?;

    let deployable = profiles
        .into_iter()
        .filter_map(|p| to_deployable_profile(p, tenant_store))
        .collect();

    Ok(deployable)
}

fn to_deployable_profile(
    discovered: DiscoveredProfile,
    tenant_store: &TenantStore,
) -> Option<DeployableProfile> {
    if discovered.profile.target != systemprompt_models::ProfileType::Cloud {
        return None;
    }

    let cloud = discovered.profile.cloud.as_ref()?;
    let tenant_id = cloud.tenant_id.as_ref()?;
    let tenant = tenant_store.find_tenant(tenant_id);

    Some(DeployableProfile {
        name: discovered.name,
        path: discovered.path,
        profile: discovered.profile,
        tenant_name: tenant.map(|t| t.name.clone()),
        hostname: tenant.and_then(|t| t.hostname.clone()),
    })
}

pub fn select_profile_interactive(profiles: &[DeployableProfile]) -> Result<usize> {
    let options: Vec<String> = profiles
        .iter()
        .map(|p| {
            let target = p.hostname.as_deref().unwrap_or("unknown");
            let tenant = p.tenant_name.as_deref().unwrap_or("unknown");
            format!("{}{} ({})", p.name, tenant, target)
        })
        .collect();

    Select::with_theme(&ColorfulTheme::default())
        .with_prompt("Select profile to deploy")
        .items(&options)
        .default(0)
        .interact()
        .context("Failed to select profile")
}

pub fn resolve_profile(
    profile_name: Option<&str>,
    config: &CliConfig,
) -> Result<(Profile, PathBuf)> {
    if let Some(name) = profile_name {
        return resolve_profile_by_name(name);
    }

    if !config.is_interactive() {
        bail!("--profile is required in non-interactive mode for deploy");
    }

    resolve_profile_interactive()
}

fn resolve_profile_by_name(name: &str) -> Result<(Profile, PathBuf)> {
    let ctx = ProjectContext::discover();
    let profile_path = ctx.profile_path(name, ProfilePath::Config);

    if !profile_path.exists() {
        bail!("Profile '{}' not found at {}", name, profile_path.display());
    }

    let profile = ProfileLoader::load_from_path(&profile_path)
        .with_context(|| format!("Failed to load profile: {}", name))?;

    Ok((profile, profile_path))
}

fn resolve_profile_interactive() -> Result<(Profile, PathBuf)> {
    let cloud_paths = get_cloud_paths();
    let tenants_path = cloud_paths.resolve(CloudPath::Tenants);
    let tenant_store = TenantStore::load_from_path(&tenants_path).unwrap_or_else(|e| {
        CliService::warning(&format!("Failed to load tenant store: {}", e));
        TenantStore::default()
    });

    let profiles = discover_deployable_profiles(&tenant_store)?;

    if profiles.is_empty() {
        bail!(
            "No deployable profiles found.\nCreate a cloud profile with: systemprompt cloud \
             profile create <name>"
        );
    }

    CliService::section("Select Profile");
    let selection = select_profile_interactive(&profiles)?;
    let selected = &profiles[selection];

    Ok((selected.profile.clone(), selected.path.clone()))
}