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 std::path::PathBuf;
use systemprompt_cloud::{ProfilePath, ProjectContext};
use systemprompt_logging::CliService;

use super::types::{ProfileDiscoveryResult, ProfileEntryResult, ProfileInfo, ProfileSkipReason};

fn process_profile_entry(ctx: &ProjectContext, path: PathBuf) -> ProfileEntryResult {
    if !path.is_dir() {
        return ProfileEntryResult::NotDirectory;
    }

    let name = match path.file_name().and_then(|n| n.to_str()) {
        Some(n) => n.to_string(),
        None => return ProfileEntryResult::Skip(ProfileSkipReason::InvalidDirectoryName { path }),
    };

    let profile_yaml = ctx.profile_path(&name, ProfilePath::Config);
    let secrets_json = ctx.profile_path(&name, ProfilePath::Secrets);

    if !profile_yaml.exists() {
        return ProfileEntryResult::Skip(ProfileSkipReason::MissingConfig { path: profile_yaml });
    }
    if !secrets_json.exists() {
        return ProfileEntryResult::Skip(ProfileSkipReason::MissingSecrets { path: secrets_json });
    }

    match load_database_url_from_secrets(&secrets_json, &name) {
        Ok(db_url) => ProfileEntryResult::Valid(ProfileInfo {
            name,
            display_name: None,
            database_url: Some(db_url),
            tenant_id: None,
            validation_mode: None,
            credentials_path: None,
            routing: None,
            is_active: None,
            session_status: None,
        }),
        Err(reason) => ProfileEntryResult::Skip(reason),
    }
}

fn load_database_url_from_secrets(
    secrets_json: &PathBuf,
    profile_name: &str,
) -> Result<String, ProfileSkipReason> {
    let content =
        std::fs::read_to_string(secrets_json).map_err(|e| ProfileSkipReason::SecretsReadError {
            path: secrets_json.clone(),
            error: e.to_string(),
        })?;

    let secrets: serde_json::Value =
        serde_json::from_str(&content).map_err(|e| ProfileSkipReason::SecretsParseError {
            path: secrets_json.clone(),
            error: e.to_string(),
        })?;

    secrets
        .get("database_url")
        .and_then(|v| v.as_str())
        .map(String::from)
        .ok_or_else(|| ProfileSkipReason::MissingDatabaseUrl {
            profile: profile_name.to_string(),
        })
}

pub fn discover_profiles() -> Result<ProfileDiscoveryResult> {
    let ctx = ProjectContext::discover();
    let profiles_dir = ctx.profiles_dir();

    if !profiles_dir.exists() {
        return Ok(ProfileDiscoveryResult {
            profiles: Vec::new(),
            skipped: Vec::new(),
        });
    }

    let mut profiles = Vec::new();
    let mut skipped = Vec::new();

    for entry in std::fs::read_dir(&profiles_dir)? {
        match process_profile_entry(&ctx, entry?.path()) {
            ProfileEntryResult::Valid(info) => profiles.push(info),
            ProfileEntryResult::Skip(reason) => skipped.push(reason),
            ProfileEntryResult::NotDirectory => {},
        }
    }

    Ok(ProfileDiscoveryResult { profiles, skipped })
}

pub fn print_discovery_summary(result: &ProfileDiscoveryResult, verbose: bool) {
    let found_count = result.profiles.len();
    let skipped_count = result.skipped.len();

    if found_count > 0 {
        CliService::info(&format!(
            "Found {} profile(s) with database configuration",
            found_count
        ));
    }

    if skipped_count > 0 {
        print_skipped_profiles(&result.skipped, verbose, skipped_count);
    }
}

fn print_skipped_profiles(skipped: &[ProfileSkipReason], verbose: bool, count: usize) {
    if verbose {
        CliService::warning(&format!("Skipped {} profile(s):", count));
        for reason in skipped {
            print_skip_reason(reason);
        }
    } else {
        CliService::info(&format!(
            "Skipped {} profile(s) (use -v for details)",
            count
        ));
    }
}

fn print_skip_reason(reason: &ProfileSkipReason) {
    match reason {
        ProfileSkipReason::MissingConfig { path } => {
            CliService::warning(&format!("  - Missing config: {}", path.display()));
        },
        ProfileSkipReason::MissingSecrets { path } => {
            CliService::warning(&format!("  - Missing secrets: {}", path.display()));
        },
        ProfileSkipReason::SecretsReadError { path, error } => {
            CliService::warning(&format!("  - Cannot read {}: {}", path.display(), error));
        },
        ProfileSkipReason::SecretsParseError { path, error } => {
            CliService::warning(&format!(
                "  - Invalid JSON in {}: {}",
                path.display(),
                error
            ));
        },
        ProfileSkipReason::MissingDatabaseUrl { profile } => {
            CliService::warning(&format!("  - No database_url in profile '{}'", profile));
        },
        ProfileSkipReason::InvalidDirectoryName { path } => {
            CliService::warning(&format!("  - Invalid directory name: {}", path.display()));
        },
    }
}