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};
use clap::Args;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use systemprompt_runtime::AppContext;
use systemprompt_users::UserService;

use crate::CliConfig;
use crate::commands::admin::users::types::BulkUpdateOutput;
use crate::shared::CommandResult;

#[derive(Debug, Args)]
pub struct UpdateArgs {
    #[arg(long, help = "New status to set (active, inactive, suspended)")]
    pub set_status: String,

    #[arg(long, help = "Filter by current role (e.g., 'anonymous')")]
    pub role: Option<String>,

    #[arg(long, help = "Filter by current status")]
    pub status: Option<String>,

    #[arg(long, help = "Filter by age: users older than N days")]
    pub older_than: Option<i64>,

    #[arg(
        long,
        default_value = "100",
        help = "Maximum number of users to update"
    )]
    pub limit: i64,

    #[arg(
        long,
        help = "Dry run - show what would be updated without actually updating"
    )]
    pub dry_run: bool,

    #[arg(short = 'y', long)]
    pub yes: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct DryRunOutput {
    pub dry_run: bool,
    pub would_update: usize,
    pub new_status: String,
    pub message: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum UpdateResult {
    DryRun(DryRunOutput),
    Executed(BulkUpdateOutput),
}

pub async fn execute(args: UpdateArgs, _config: &CliConfig) -> Result<CommandResult<UpdateResult>> {
    if !args.yes && !args.dry_run {
        return Err(anyhow!(
            "This will update multiple users. Use --yes to confirm or --dry-run to preview."
        ));
    }

    if args.role.is_none() && args.status.is_none() && args.older_than.is_none() {
        return Err(anyhow!(
            "At least one filter is required: --role, --status, or --older-than"
        ));
    }

    let valid_statuses = ["active", "inactive", "suspended"];
    if !valid_statuses.contains(&args.set_status.as_str()) {
        return Err(anyhow!(
            "Invalid status '{}'. Must be one of: {}",
            args.set_status,
            valid_statuses.join(", ")
        ));
    }

    let ctx = AppContext::new().await?;
    let user_service = UserService::new(ctx.db_pool())?;

    let users = user_service
        .list_by_filter(
            args.status.as_deref(),
            args.role.as_deref(),
            args.older_than,
            args.limit,
        )
        .await?;

    if users.is_empty() {
        let output = BulkUpdateOutput {
            updated: 0,
            message: "No users match the specified filters".to_string(),
        };
        return Ok(CommandResult::text(UpdateResult::Executed(output)).with_title("Bulk Update"));
    }

    if args.dry_run {
        let output = DryRunOutput {
            dry_run: true,
            would_update: users.len(),
            new_status: args.set_status.clone(),
            message: format!(
                "Would update {} user(s) to status '{}'",
                users.len(),
                args.set_status
            ),
        };
        return Ok(
            CommandResult::text(UpdateResult::DryRun(output)).with_title("Bulk Update (Dry Run)")
        );
    }

    let user_ids: Vec<_> = users.iter().map(|u| u.id.clone()).collect();
    let updated = user_service
        .bulk_update_status(&user_ids, &args.set_status)
        .await?;

    let output = BulkUpdateOutput {
        updated,
        message: format!(
            "Updated {} user(s) to status '{}'",
            updated, args.set_status
        ),
    };

    Ok(CommandResult::text(UpdateResult::Executed(output)).with_title("Bulk Update"))
}