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