systemprompt-cli 0.14.3

Unified CLI for systemprompt.io AI governance: agent orchestration, MCP governance, analytics, profiles, cloud deploy, and self-hosted operations.
Documentation
//! Profile assembly, persistence, and migration for the setup wizard.
//!
//! [`build`] constructs a `Profile` for the chosen environment, [`save`]
//! writes it as YAML with a credential warning header, and [`run_migrations`]
//! re-invokes the CLI against the new profile to apply the schema.

use anyhow::{Context, Result};
use std::path::Path;
use systemprompt_cloud::ProjectContext;
use systemprompt_identifiers::ProviderId;
use systemprompt_logging::CliService;
use systemprompt_models::profile::{SecretsConfig, SecretsSource, SecretsValidationMode};
use systemprompt_models::services::SystemAdminConfig;
use systemprompt_models::{
    CliPaths, CloudConfig, CloudValidationMode, Environment, ExtensionsConfig, Profile,
    ProfileDatabaseConfig, ProfileType, RateLimitsConfig, SiteConfig,
};

use super::profile_sections as sections;
use super::secrets::SecretsData;
use crate::shared::profile::generate_display_name;

fn determine_environment(env_name: &str) -> Environment {
    match env_name.to_lowercase().as_str() {
        "prod" | "production" => Environment::Production,
        "staging" | "stage" => Environment::Staging,
        "test" | "testing" => Environment::Test,
        _ => Environment::Development,
    }
}

pub(super) struct ProfileBuildParams<'a> {
    pub env_name: &'a str,
    pub secrets_path: &'a str,
    pub project_root: &'a Path,
    pub bin_path: Option<&'a Path>,
    pub secrets: &'a SecretsData,
    pub default_provider: Option<&'a ProviderId>,
}

pub(super) fn build(params: &ProfileBuildParams<'_>) -> Result<Profile> {
    let ProfileBuildParams {
        env_name,
        secrets_path,
        project_root,
        bin_path,
        secrets,
        default_provider,
    } = *params;
    let ctx = ProjectContext::new(project_root.to_path_buf());
    let runtime_env = determine_environment(env_name);
    let is_prod = matches!(runtime_env, Environment::Production);
    let server = sections::server(is_prod);
    let governance = sections::governance(&server.api_internal_url);

    let profile = Profile {
        name: env_name.to_owned(),
        display_name: generate_display_name(env_name),
        target: ProfileType::Local,
        site: SiteConfig {
            name: "systemprompt.io".to_owned(),
            github_link: None,
        },
        database: ProfileDatabaseConfig {
            db_type: "postgres".to_owned(),
            external_db_access: false,
        },
        server,
        paths: sections::paths(project_root, bin_path, &ctx),
        security: sections::security(env_name),
        rate_limits: RateLimitsConfig {
            disabled: !is_prod,
            ..Default::default()
        },
        runtime: sections::runtime(runtime_env, is_prod),
        cloud: Some(CloudConfig {
            tenant_id: None,
            validation: CloudValidationMode::Skip,
        }),
        secrets: Some(SecretsConfig {
            secrets_path: secrets_path.to_owned(),
            validation: SecretsValidationMode::Warn,
            source: SecretsSource::File,
        }),
        extensions: ExtensionsConfig::default(),
        providers: sections::providers(secrets),
        gateway: Some(sections::gateway(secrets, default_provider)),
        governance: Some(governance),
        system_admin: SystemAdminConfig {
            username: "admin".to_owned(),
        },
    };

    profile
        .validate()
        .context("generated profile failed validation")?;
    Ok(profile)
}

pub(super) fn save(profile: &Profile, profile_path: &Path) -> Result<()> {
    let header = format!(
        "# systemprompt.io Profile: {}\n#\n# Generated by 'systemprompt setup'\n#\n# WARNING: \
         This file contains database credentials.\n# DO NOT commit this file to version control.",
        profile.display_name
    );

    crate::shared::profile::save_profile_yaml(profile, profile_path, Some(&header))?;

    CliService::success(&format!("Saved profile to {}", profile_path.display()));

    Ok(())
}

pub(super) fn profile_dir(systemprompt_dir: &Path, env_name: &str) -> std::path::PathBuf {
    systemprompt_dir.join("profiles").join(env_name)
}

pub(super) fn default_path(systemprompt_dir: &Path, env_name: &str) -> std::path::PathBuf {
    profile_dir(systemprompt_dir, env_name).join("profile.yaml")
}

pub(super) fn run_migrations(profile_path: &Path) -> Result<()> {
    CliService::info("Running database migrations...");

    let current_exe = std::env::current_exe().context("Failed to get executable path")?;
    let profile_path_str = profile_path.to_string_lossy();

    let output = std::process::Command::new(&current_exe)
        .args(CliPaths::db_migrate_args())
        .env("SYSTEMPROMPT_PROFILE", profile_path_str.as_ref())
        .output()
        .context("Failed to run migrations")?;

    if output.status.success() {
        CliService::success("Migrations completed successfully");

        let stdout = String::from_utf8_lossy(&output.stdout);
        for line in stdout.lines().filter(|l| !l.is_empty()) {
            CliService::info(&format!("    {}", line));
        }
        return Ok(());
    }

    let stderr = String::from_utf8_lossy(&output.stderr);
    let stdout = String::from_utf8_lossy(&output.stdout);

    CliService::error("Migrations failed");

    if !stdout.is_empty() {
        CliService::info(&stdout);
    }
    if !stderr.is_empty() {
        CliService::error(&stderr);
    }

    CliService::info("Run manually with:");
    CliService::info(&format!(
        "  SYSTEMPROMPT_PROFILE={} systemprompt {}",
        profile_path_str,
        CliPaths::db_migrate_cmd()
    ));

    Ok(())
}