systemprompt-cli 0.20.0

Unified CLI for systemprompt.io AI governance: agent orchestration, MCP governance, analytics, profiles, cloud deploy, and self-hosted operations.
Documentation
use crate::context::CommandContext;
use crate::interactive::require_confirmation;
use crate::shared::CommandOutput;
use anyhow::Result;
use systemprompt_logging::CliService;
use systemprompt_scheduler::{
    OrphanCleanupReport, OrphanDisposition, ProcessCleanup, ServiceManagementService,
};

use super::get_api_port;
use super::types::CleanupOutput;

pub(super) async fn execute(
    yes: bool,
    dry_run: bool,
    ctx: &CommandContext,
) -> Result<CommandOutput> {
    let config = &ctx.cli;
    let quiet = config.is_json_output();
    if !quiet {
        CliService::section("Cleaning Up Services");
    }

    let app = ctx.app_context().await?;
    let service_mgmt = ServiceManagementService::new(app.db_pool())?;
    let api_port = get_api_port();

    if !quiet {
        CliService::info("Finding running services...");
    }
    let running_services = service_mgmt.get_running_services_with_pid().await?;
    let api_pid = ProcessCleanup::check_port(api_port);

    if running_services.is_empty() && api_pid.is_none() {
        return Ok(no_services_result(quiet, dry_run));
    }

    if dry_run {
        return Ok(dry_run_result(&running_services, api_pid, api_port, quiet));
    }

    let service_count = running_services.len() + usize::from(api_pid.is_some());
    require_confirmation(
        ctx.prompter(),
        &format!("This will stop {} service(s). Continue?", service_count),
        yes,
        config,
    )?;

    let report = service_mgmt.cleanup_all_orphans(api_port).await?;
    render_cleanup_report(&report, quiet);

    let cleaned = report.services_cleaned();
    let stale_count = usize::try_from(report.stale_entries_removed).unwrap_or(usize::MAX);
    let message = format_cleanup_message(cleaned, quiet);
    let output = CleanupOutput {
        services_cleaned: cleaned,
        stale_entries_removed: stale_count,
        dry_run: false,
        message,
    };
    Ok(CommandOutput::card_value("Service Cleanup", &output))
}

pub fn render_cleanup_report(report: &OrphanCleanupReport, quiet: bool) {
    if quiet {
        return;
    }
    for outcome in &report.outcomes {
        match outcome.disposition {
            OrphanDisposition::StaleEntry => {
                CliService::info(&format!(
                    "Cleaning stale entry: {} (PID {} not running)",
                    outcome.name, outcome.pid
                ));
            },
            OrphanDisposition::Stopped => {
                CliService::info(&format!(
                    "Stopping {} (PID: {}, port: {})...",
                    outcome.name, outcome.pid, outcome.port
                ));
            },
        }
    }
    CliService::info("Stopping API server...");
    if report.stale_entries_removed > 0 {
        CliService::info(&format!(
            "Cleaned {} stale database entries",
            report.stale_entries_removed
        ));
    }
}

pub fn no_services_result(quiet: bool, dry_run: bool) -> CommandOutput {
    let message = "No running services found".to_owned();
    if !quiet {
        CliService::info(&message);
    }
    CommandOutput::card_value(
        "Service Cleanup",
        &CleanupOutput {
            services_cleaned: 0,
            stale_entries_removed: 0,
            dry_run,
            message,
        },
    )
}

pub fn dry_run_result(
    running_services: &[systemprompt_database::ServiceConfig],
    api_pid: Option<u32>,
    api_port: u16,
    quiet: bool,
) -> CommandOutput {
    if !quiet {
        CliService::section("Dry Run - Would clean the following:");
        for service in running_services {
            log_service_state(service);
        }
        if let Some(pid) = api_pid {
            CliService::info(&format!(
                "  [running] API server (PID: {}, port: {})",
                pid, api_port
            ));
        }
        CliService::info("Run without --dry-run to execute cleanup");
    }
    let service_count = running_services.len() + usize::from(api_pid.is_some());
    CommandOutput::card_value(
        "Service Cleanup (Dry Run)",
        &CleanupOutput {
            services_cleaned: service_count,
            stale_entries_removed: 0,
            dry_run: true,
            message: format!("Would clean {} service(s)", service_count),
        },
    )
}

pub fn log_service_state(service: &systemprompt_database::ServiceConfig) {
    let Some(pid) = service.pid else { return };
    let pid_u32 = pid as u32;
    if ProcessCleanup::process_exists(pid_u32) {
        CliService::info(&format!(
            "  [running] {} (PID: {}, port: {})",
            service.name, pid, service.port
        ));
    } else {
        CliService::info(&format!(
            "  [stale] {} (PID {} not running)",
            service.name, pid
        ));
    }
}

pub fn format_cleanup_message(cleaned: usize, quiet: bool) -> String {
    if cleaned > 0 {
        let msg = format!("Cleaned up {} services", cleaned);
        if !quiet {
            CliService::success(&msg);
        }
        msg
    } else {
        let msg = "No running services found".to_owned();
        if !quiet {
            CliService::info(&msg);
        }
        msg
    }
}