systemprompt-cli 0.21.1

Unified CLI for systemprompt.io AI governance: agent orchestration, MCP governance, analytics, profiles, cloud deploy, and self-hosted operations.
Documentation
//! `cloud tenant edit` command for local tenant database settings.
//!
//! Copyright (c) systemprompt.io — Business Source License 1.1.
//! See <https://systemprompt.io> for licensing details.

use anyhow::{Result, anyhow, bail};
use systemprompt_cloud::{CloudPath, StoredTenant, TenantStore, TenantType, get_cloud_paths};
use systemprompt_logging::CliService;

use super::select::select_tenant;
use crate::cli_settings::CliConfig;
use crate::cloud::types::TenantDetailOutput;
use crate::interactive::Prompter;
use crate::shared::CommandOutput;

pub fn edit_tenant(
    id: Option<String>,
    prompter: &dyn Prompter,
    config: &CliConfig,
) -> Result<CommandOutput> {
    if !config.is_interactive() {
        return Err(anyhow::anyhow!(
            "Tenant edit requires interactive mode.\nUse specific commands to modify tenant \
             settings in non-interactive mode."
        ));
    }

    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| {
        CliService::warning(&format!("Failed to load tenant store: {}", e));
        TenantStore::default()
    });

    let tenant_id = if let Some(id) = id {
        systemprompt_identifiers::TenantId::new(id)
    } else {
        if store.tenants.is_empty() {
            bail!("No tenants configured.");
        }
        select_tenant(prompter, &store.tenants)?.id.clone()
    };

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

    CliService::section(&format!("Edit Tenant: {}", tenant.name));

    let new_name = prompter.input_with_default("Tenant name", &tenant.name)?;

    if new_name.is_empty() {
        bail!("Tenant name cannot be empty");
    }
    tenant.name.clone_from(&new_name);

    if tenant.tenant_type == TenantType::Local {
        edit_local_tenant_database(tenant, prompter)?;
    }

    if tenant.tenant_type == TenantType::Cloud {
        display_readonly_cloud_fields(tenant);
    }

    let output = TenantDetailOutput {
        id: tenant.id.as_str().to_owned(),
        name: tenant.name.clone(),
        tenant_type: format!("{:?}", tenant.tenant_type).to_lowercase(),
        app_id: tenant.app_id.clone(),
        hostname: tenant.hostname.clone(),
        region: tenant.region.clone(),
        has_database: tenant.has_database_url(),
    };

    store.save_to_path(&tenants_path)?;
    CliService::success(&format!("Tenant '{}' updated", new_name));

    Ok(CommandOutput::card_value(format!("Tenant: {}", new_name), &output).with_skip_render())
}

fn edit_local_tenant_database(tenant: &mut StoredTenant, prompter: &dyn Prompter) -> Result<()> {
    if let Some(current_url) = tenant.database_url.clone() {
        let edit_db = prompter.confirm("Edit database URL?", false)?;

        if edit_db {
            let new_url = prompter.input_with_default("Database URL", &current_url)?;
            tenant.database_url = if new_url.is_empty() {
                None
            } else {
                Some(new_url)
            };
        }
    }
    Ok(())
}

fn display_readonly_cloud_fields(tenant: &StoredTenant) {
    if let Some(ref region) = tenant.region {
        CliService::info(&format!("Region: {} (cannot be changed)", region));
    }
    if let Some(ref hostname) = tenant.hostname {
        CliService::info(&format!("Hostname: {} (cannot be changed)", hostname));
    }
}