Skip to main content

systemprompt_cli/commands/infrastructure/services/
cleanup.rs

1use crate::context::CommandContext;
2use crate::interactive::require_confirmation;
3use crate::shared::CommandOutput;
4use anyhow::Result;
5use systemprompt_logging::CliService;
6use systemprompt_scheduler::{
7    OrphanCleanupReport, OrphanDisposition, ProcessCleanup, ServiceManagementService,
8};
9
10use super::get_api_port;
11use super::types::CleanupOutput;
12
13pub(super) async fn execute(
14    yes: bool,
15    dry_run: bool,
16    ctx: &CommandContext,
17) -> Result<CommandOutput> {
18    let config = &ctx.cli;
19    let quiet = config.is_json_output();
20    if !quiet {
21        CliService::section("Cleaning Up Services");
22    }
23
24    let app = ctx.app_context().await?;
25    let service_mgmt = ServiceManagementService::new(app.db_pool())?;
26    let api_port = get_api_port();
27
28    if !quiet {
29        CliService::info("Finding running services...");
30    }
31    let running_services = service_mgmt.get_running_services_with_pid().await?;
32    let api_pid = ProcessCleanup::check_port(api_port);
33
34    if running_services.is_empty() && api_pid.is_none() {
35        return Ok(no_services_result(quiet, dry_run));
36    }
37
38    if dry_run {
39        return Ok(dry_run_result(&running_services, api_pid, api_port, quiet));
40    }
41
42    let service_count = running_services.len() + usize::from(api_pid.is_some());
43    require_confirmation(
44        ctx.prompter(),
45        &format!("This will stop {} service(s). Continue?", service_count),
46        yes,
47        config,
48    )?;
49
50    let report = service_mgmt.cleanup_all_orphans(api_port).await?;
51    render_cleanup_report(&report, quiet);
52
53    let cleaned = report.services_cleaned();
54    let stale_count = usize::try_from(report.stale_entries_removed).unwrap_or(usize::MAX);
55    let message = format_cleanup_message(cleaned, quiet);
56    let output = CleanupOutput {
57        services_cleaned: cleaned,
58        stale_entries_removed: stale_count,
59        dry_run: false,
60        message,
61    };
62    Ok(CommandOutput::card_value("Service Cleanup", &output))
63}
64
65pub fn render_cleanup_report(report: &OrphanCleanupReport, quiet: bool) {
66    if quiet {
67        return;
68    }
69    for outcome in &report.outcomes {
70        match outcome.disposition {
71            OrphanDisposition::StaleEntry => {
72                CliService::info(&format!(
73                    "Cleaning stale entry: {} (PID {} not running)",
74                    outcome.name, outcome.pid
75                ));
76            },
77            OrphanDisposition::Stopped => {
78                CliService::info(&format!(
79                    "Stopping {} (PID: {}, port: {})...",
80                    outcome.name, outcome.pid, outcome.port
81                ));
82            },
83        }
84    }
85    CliService::info("Stopping API server...");
86    if report.stale_entries_removed > 0 {
87        CliService::info(&format!(
88            "Cleaned {} stale database entries",
89            report.stale_entries_removed
90        ));
91    }
92}
93
94pub fn no_services_result(quiet: bool, dry_run: bool) -> CommandOutput {
95    let message = "No running services found".to_owned();
96    if !quiet {
97        CliService::info(&message);
98    }
99    CommandOutput::card_value(
100        "Service Cleanup",
101        &CleanupOutput {
102            services_cleaned: 0,
103            stale_entries_removed: 0,
104            dry_run,
105            message,
106        },
107    )
108}
109
110pub fn dry_run_result(
111    running_services: &[systemprompt_database::ServiceConfig],
112    api_pid: Option<u32>,
113    api_port: u16,
114    quiet: bool,
115) -> CommandOutput {
116    if !quiet {
117        CliService::section("Dry Run - Would clean the following:");
118        for service in running_services {
119            log_service_state(service);
120        }
121        if let Some(pid) = api_pid {
122            CliService::info(&format!(
123                "  [running] API server (PID: {}, port: {})",
124                pid, api_port
125            ));
126        }
127        CliService::info("Run without --dry-run to execute cleanup");
128    }
129    let service_count = running_services.len() + usize::from(api_pid.is_some());
130    CommandOutput::card_value(
131        "Service Cleanup (Dry Run)",
132        &CleanupOutput {
133            services_cleaned: service_count,
134            stale_entries_removed: 0,
135            dry_run: true,
136            message: format!("Would clean {} service(s)", service_count),
137        },
138    )
139}
140
141pub fn log_service_state(service: &systemprompt_database::ServiceConfig) {
142    let Some(pid) = service.pid else { return };
143    let pid_u32 = pid as u32;
144    if ProcessCleanup::process_exists(pid_u32) {
145        CliService::info(&format!(
146            "  [running] {} (PID: {}, port: {})",
147            service.name, pid, service.port
148        ));
149    } else {
150        CliService::info(&format!(
151            "  [stale] {} (PID {} not running)",
152            service.name, pid
153        ));
154    }
155}
156
157pub fn format_cleanup_message(cleaned: usize, quiet: bool) -> String {
158    if cleaned > 0 {
159        let msg = format!("Cleaned up {} services", cleaned);
160        if !quiet {
161            CliService::success(&msg);
162        }
163        msg
164    } else {
165        let msg = "No running services found".to_owned();
166        if !quiet {
167            CliService::info(&msg);
168        }
169        msg
170    }
171}