systemprompt-cli 0.2.1

Unified CLI for systemprompt.io AI governance: agent orchestration, MCP governance, analytics, profiles, cloud deploy, and self-hosted operations.
Documentation
use anyhow::{Result, anyhow, bail};
use dialoguer::Confirm;
use dialoguer::theme::ColorfulTheme;
use systemprompt_cloud::{
    CloudApiClient, CloudPath, StoredTenant, TenantStore, TenantType, get_cloud_paths,
};
use systemprompt_logging::CliService;

use super::docker::{
    drop_database_for_tenant, load_shared_config, save_shared_config, stop_shared_container,
};
use super::select::{get_credentials, select_tenant};
use crate::cli_settings::CliConfig;
use crate::cloud::tenant::TenantDeleteArgs;
use crate::shared::{CommandResult, SuccessOutput};

pub async fn delete_tenant(
    args: TenantDeleteArgs,
    config: &CliConfig,
) -> Result<CommandResult<SuccessOutput>> {
    let cloud_paths = get_cloud_paths();
    let tenants_path = cloud_paths.resolve(CloudPath::Tenants);
    let mut store = TenantStore::load_from_path(&tenants_path).unwrap_or_else(|e| {
        if !config.is_json_output() {
            CliService::warning(&format!("Failed to load tenant store: {}", e));
        }
        TenantStore::default()
    });

    let tenant_id = if let Some(id) = args.id {
        id
    } else {
        if !config.is_interactive() {
            return Err(anyhow::anyhow!(
                "--id is required in non-interactive mode for tenant delete"
            ));
        }
        if store.tenants.is_empty() {
            bail!("No tenants configured.");
        }
        select_tenant(&store.tenants)?.id.clone()
    };

    let tenant = store
        .tenants
        .iter()
        .find(|t| t.id == tenant_id)
        .ok_or_else(|| anyhow!("Tenant not found: {}", tenant_id))?
        .clone();

    let is_cloud = tenant.tenant_type == TenantType::Cloud;

    if !args.yes {
        if !config.is_interactive() {
            return Err(anyhow::anyhow!(
                "--yes is required in non-interactive mode for tenant delete"
            ));
        }

        let prompt = if is_cloud {
            format!(
                "Delete cloud tenant '{}'? This will cancel your subscription and delete all data.",
                tenant.name
            )
        } else {
            format!("Delete tenant '{}'?", tenant.name)
        };

        let confirm = Confirm::with_theme(&ColorfulTheme::default())
            .with_prompt(prompt)
            .default(false)
            .interact()?;

        if !confirm {
            let output = SuccessOutput::new("Cancelled");
            if !config.is_json_output() {
                CliService::info("Cancelled");
            }
            return Ok(CommandResult::text(output).with_title("Delete Tenant"));
        }
    }

    if is_cloud {
        let creds = get_credentials()?;
        let client = CloudApiClient::new(&creds.api_url, &creds.api_token)?;

        if config.is_json_output() {
            client.delete_tenant(&tenant_id).await?;
        } else {
            let spinner = CliService::spinner("Deleting cloud tenant...");
            client.delete_tenant(&tenant_id).await?;
            spinner.finish_and_clear();
        }
    } else if tenant.uses_shared_container() {
        cleanup_shared_container_tenant(&tenant, config)?;
    }

    store.tenants.retain(|t| t.id != tenant_id);
    store.save_to_path(&tenants_path)?;

    let output = SuccessOutput::new(format!("Deleted tenant: {}", tenant_id));

    if !config.is_json_output() {
        CliService::success(&format!("Deleted tenant: {}", tenant_id));
    }

    Ok(CommandResult::text(output).with_title("Delete Tenant"))
}

fn cleanup_shared_container_tenant(tenant: &StoredTenant, config: &CliConfig) -> Result<()> {
    let Some(ref db_name) = tenant.shared_container_db else {
        return Ok(());
    };

    let Some(mut shared_config) = load_shared_config()? else {
        CliService::warning("Shared container config not found, skipping database cleanup");
        return Ok(());
    };

    let spinner = CliService::spinner(&format!("Dropping database '{}'...", db_name));
    match drop_database_for_tenant(&shared_config.admin_password, shared_config.port, db_name) {
        Ok(()) => {
            spinner.finish_and_clear();
            CliService::success(&format!("Database '{}' dropped", db_name));
        },
        Err(e) => {
            spinner.finish_and_clear();
            CliService::warning(&format!("Failed to drop database '{}': {}", db_name, e));
        },
    }

    shared_config.remove_tenant(tenant.id.as_str());
    save_shared_config(&shared_config)?;

    if shared_config.tenant_databases.is_empty() {
        let should_remove = if config.is_interactive() {
            Confirm::with_theme(&ColorfulTheme::default())
                .with_prompt("No local tenants remain. Remove shared PostgreSQL container?")
                .default(true)
                .interact()?
        } else {
            false
        };

        if should_remove {
            stop_shared_container()?;
        } else {
            CliService::info(
                "Shared container kept. Remove manually with 'docker compose -f \
                 .systemprompt/docker/shared.yaml down -v'",
            );
        }
    }

    Ok(())
}