Skip to main content

systemprompt_cli/commands/infrastructure/services/
cleanup.rs

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