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::{Context, Result, bail};
use systemprompt_cloud::{
    CloudPath, ProfilePath, ProjectContext, TenantStore, TenantType, get_cloud_paths,
};
use systemprompt_logging::CliService;

use systemprompt_identifiers::TenantId;

use super::CreateArgs;
use super::api_keys::{ApiKeys, collect_api_keys};
use super::builders::{CloudProfileBuilder, LocalProfileBuilder};
use super::create_setup::{get_cloud_user, handle_local_tenant_setup};
use super::create_tenant::{get_tenants_by_type, select_tenant, select_tenant_type};
use super::profile_steps::{ensure_unmasked_credentials, resolve_tenant_from_args};
use super::templates::{
    DatabaseUrls, get_services_path, save_dockerfile, save_dockerignore, save_entrypoint,
    save_profile, save_secrets, update_ai_config_default_provider,
};
use crate::cli_settings::CliConfig;

pub use super::profile_steps::{CreatedProfile, create_profile_for_tenant};

pub async fn execute(args: &CreateArgs, config: &CliConfig) -> Result<()> {
    let name = &args.name;
    CliService::section(&format!("Create Profile: {}", name));

    let cloud_user = get_cloud_user()?;
    let ctx = ProjectContext::discover();
    let profile_dir = ctx.profile_dir(name);

    if profile_dir.exists() {
        bail!(
            "Profile '{}' already exists at {}\nUse 'systemprompt cloud profile delete {}' first.",
            name,
            profile_dir.display(),
            name
        );
    }

    std::fs::create_dir_all(ctx.profiles_dir())
        .with_context(|| format!("Failed to create {}", ctx.profiles_dir().display()))?;

    let cloud_paths = get_cloud_paths();
    let tenants_path = cloud_paths.resolve(CloudPath::Tenants);
    let store = TenantStore::load_from_path(&tenants_path).unwrap_or_else(|e| {
        CliService::warning(&format!("Failed to load tenant store: {}", e));
        TenantStore::default()
    });

    let (tenant, api_keys) = if config.is_interactive() && args.tenant.is_none() {
        let tenant_type = select_tenant_type(&store)?;
        let eligible_tenants = get_tenants_by_type(&store, tenant_type);
        let tenant = select_tenant(&eligible_tenants)?;

        if !tenant.has_database_url() {
            bail!(
                "Tenant '{}' does not have a database URL configured.\nFor local tenants, \
                 recreate with 'systemprompt cloud tenant create'.",
                tenant.name
            );
        }

        CliService::section("API Keys");
        let api_keys = collect_api_keys()?;
        (tenant, api_keys)
    } else {
        let tenant = resolve_tenant_from_args(args, &store)?;

        if !tenant.has_database_url() {
            bail!(
                "Tenant '{}' does not have a database URL configured.\nFor local tenants, \
                 recreate with 'systemprompt cloud tenant create'.",
                tenant.name
            );
        }

        let api_keys = ApiKeys::from_options(
            args.gemini_key.clone(),
            args.anthropic_key.clone(),
            args.openai_key.clone(),
        )?;
        (tenant, api_keys)
    };

    let tenant = ensure_unmasked_credentials(tenant, &tenants_path).await?;

    std::fs::create_dir_all(&profile_dir)
        .with_context(|| format!("Failed to create directory {}", profile_dir.display()))?;

    std::fs::create_dir_all(ctx.storage_dir()).with_context(|| {
        format!(
            "Failed to create storage directory {}",
            ctx.storage_dir().display()
        )
    })?;

    let secrets_path = ProfilePath::Secrets.resolve(&profile_dir);
    let external_url = tenant
        .get_local_database_url()
        .ok_or_else(|| anyhow::anyhow!("Tenant database URL is required"))?;
    let db_urls = DatabaseUrls {
        external: external_url,
        internal: tenant.internal_database_url.as_deref(),
    };
    save_secrets(
        &db_urls,
        &api_keys,
        tenant.sync_token.as_deref(),
        &secrets_path,
        tenant.tenant_type == TenantType::Cloud,
    )?;
    CliService::success(&format!("Created: {}", secrets_path.display()));

    update_ai_config_default_provider(api_keys.selected_provider())?;

    let services_path = get_services_path()?;
    let profile_path = ProfilePath::Config.resolve(&profile_dir);
    let relative_secrets_path = "./secrets.json";

    let built_profile = match tenant.tenant_type {
        TenantType::Local => LocalProfileBuilder::new(name, relative_secrets_path, &services_path)
            .with_tenant_id(TenantId::new(&tenant.id))
            .build(),
        TenantType::Cloud => {
            let mut builder = CloudProfileBuilder::new(name)
                .with_tenant_id(TenantId::new(&tenant.id))
                .with_external_db_access(tenant.external_db_access)
                .with_secrets_path(relative_secrets_path);
            if let Some(hostname) = &tenant.hostname {
                builder = builder.with_external_url(format!("https://{}", hostname));
            }
            builder.build()
        },
    };

    save_profile(&built_profile, &profile_path)?;
    CliService::success(&format!("Created: {}", profile_path.display()));

    let docker_dir = ctx.profile_docker_dir(name);
    std::fs::create_dir_all(&docker_dir)
        .with_context(|| format!("Failed to create docker directory {}", docker_dir.display()))?;

    let dockerfile_path = ctx.profile_dockerfile(name);
    save_dockerfile(&dockerfile_path, name, ctx.root())?;
    CliService::success(&format!("Created: {}", dockerfile_path.display()));

    let entrypoint_path = ctx.profile_entrypoint(name);
    save_entrypoint(&entrypoint_path)?;
    CliService::success(&format!("Created: {}", entrypoint_path.display()));

    let dockerignore_path = ctx.profile_dockerignore(name);
    save_dockerignore(&dockerignore_path)?;
    CliService::success(&format!("Created: {}", dockerignore_path.display()));

    match built_profile.validate() {
        Ok(()) => CliService::success("Profile validated"),
        Err(e) => CliService::warning(&format!("Validation warning: {}", e)),
    }

    if tenant.tenant_type == TenantType::Local {
        let db_url = tenant
            .get_local_database_url()
            .ok_or_else(|| anyhow::anyhow!("Tenant database URL is required"))?;
        handle_local_tenant_setup(&cloud_user, db_url, &tenant.name, &profile_path).await?;
    }

    CliService::section("Next Steps");
    CliService::info(&format!(
        "  export SYSTEMPROMPT_PROFILE={}",
        profile_path.display()
    ));

    match tenant.tenant_type {
        TenantType::Local => CliService::info("  just start"),
        TenantType::Cloud => CliService::info("  just deploy"),
    }

    Ok(())
}