systemprompt_cli/commands/infrastructure/services/
cleanup.rs1use 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}