tldr_cli/commands/daemon/
status.rs1use std::path::PathBuf;
14
15use clap::Args;
16use serde::Serialize;
17
18use crate::output::OutputFormat;
19
20use super::error::DaemonError;
21use super::ipc::send_command;
22use super::types::{DaemonCommand, DaemonResponse, DaemonStatus, SalsaCacheStats};
23
24#[derive(Debug, Clone, Args)]
30pub struct DaemonStatusArgs {
31 #[arg(long, short = 'p', default_value = ".")]
33 pub project: PathBuf,
34
35 #[arg(long, short = 's')]
37 pub session: Option<String>,
38}
39
40#[derive(Debug, Clone, Serialize)]
46pub struct DaemonStatusOutput {
47 pub status: String,
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub uptime: Option<f64>,
52 #[serde(skip_serializing_if = "Option::is_none")]
54 pub uptime_human: Option<String>,
55 #[serde(skip_serializing_if = "Option::is_none")]
57 pub files: Option<usize>,
58 #[serde(skip_serializing_if = "Option::is_none")]
60 pub project: Option<PathBuf>,
61 #[serde(skip_serializing_if = "Option::is_none")]
63 pub salsa_stats: Option<SalsaCacheStats>,
64 #[serde(skip_serializing_if = "Option::is_none")]
66 pub message: Option<String>,
67}
68
69impl DaemonStatusArgs {
74 pub fn run(&self, format: OutputFormat, quiet: bool) -> anyhow::Result<()> {
76 let runtime = tokio::runtime::Runtime::new()?;
78 runtime.block_on(self.run_async(format, quiet))
79 }
80
81 async fn run_async(&self, format: OutputFormat, quiet: bool) -> anyhow::Result<()> {
83 let project = self.project.canonicalize().unwrap_or_else(|_| {
85 std::env::current_dir()
86 .unwrap_or_else(|_| PathBuf::from("."))
87 .join(&self.project)
88 });
89
90 let cmd = DaemonCommand::Status {
92 session: self.session.clone(),
93 };
94
95 match send_command(&project, &cmd).await {
96 Ok(response) => self.handle_response(response, format, quiet),
97 Err(DaemonError::NotRunning) | Err(DaemonError::ConnectionRefused) => {
98 let output = DaemonStatusOutput {
100 status: "not_running".to_string(),
101 uptime: None,
102 uptime_human: None,
103 files: None,
104 project: None,
105 salsa_stats: None,
106 message: Some("Daemon not running".to_string()),
107 };
108
109 if !quiet {
110 match format {
111 OutputFormat::Json | OutputFormat::Compact => {
112 println!("{}", serde_json::to_string_pretty(&output)?);
113 }
114 OutputFormat::Text | OutputFormat::Sarif | OutputFormat::Dot => {
115 println!("Daemon not running");
116 }
117 }
118 }
119
120 Ok(())
121 }
122 Err(e) => Err(anyhow::anyhow!("Failed to get daemon status: {}", e)),
123 }
124 }
125
126 fn handle_response(
128 &self,
129 response: DaemonResponse,
130 format: OutputFormat,
131 quiet: bool,
132 ) -> anyhow::Result<()> {
133 match response {
134 DaemonResponse::FullStatus {
135 status,
136 uptime,
137 files,
138 project,
139 salsa_stats,
140 ..
141 } => {
142 let status_str = format_status(status);
143 let uptime_human = format_uptime(uptime);
144
145 let output = DaemonStatusOutput {
146 status: status_str.clone(),
147 uptime: Some(uptime),
148 uptime_human: Some(uptime_human.clone()),
149 files: Some(files),
150 project: Some(project.clone()),
151 salsa_stats: Some(salsa_stats.clone()),
152 message: None,
153 };
154
155 if !quiet {
156 match format {
157 OutputFormat::Json | OutputFormat::Compact => {
158 println!("{}", serde_json::to_string_pretty(&output)?);
159 }
160 OutputFormat::Text | OutputFormat::Sarif | OutputFormat::Dot => {
161 println!("TLDR Daemon Status");
162 println!("==================");
163 println!("Status: {}", status_str);
164 println!("Uptime: {}", uptime_human);
165 println!("Project: {}", project.display());
166 println!("Files: {}", files);
167 println!();
168 println!("Cache Statistics");
169 println!("----------------");
170 println!("Hits: {}", format_number(salsa_stats.hits));
171 println!("Misses: {}", format_number(salsa_stats.misses));
172 println!("Hit Rate: {:.2}%", salsa_stats.hit_rate());
173 println!(
174 "Invalidations: {}",
175 format_number(salsa_stats.invalidations)
176 );
177 }
178 }
179 }
180
181 Ok(())
182 }
183 DaemonResponse::Status { status, message } => {
184 let output = DaemonStatusOutput {
185 status: status.clone(),
186 uptime: None,
187 uptime_human: None,
188 files: None,
189 project: None,
190 salsa_stats: None,
191 message,
192 };
193
194 if !quiet {
195 match format {
196 OutputFormat::Json | OutputFormat::Compact => {
197 println!("{}", serde_json::to_string_pretty(&output)?);
198 }
199 OutputFormat::Text | OutputFormat::Sarif | OutputFormat::Dot => {
200 println!("Status: {}", status);
201 if let Some(msg) = &output.message {
202 println!("{}", msg);
203 }
204 }
205 }
206 }
207
208 Ok(())
209 }
210 DaemonResponse::Error { error, .. } => Err(anyhow::anyhow!("Daemon error: {}", error)),
211 _ => Err(anyhow::anyhow!("Unexpected response from daemon")),
212 }
213 }
214}
215
216fn format_status(status: DaemonStatus) -> String {
222 match status {
223 DaemonStatus::Initializing => "initializing".to_string(),
224 DaemonStatus::Indexing => "indexing".to_string(),
225 DaemonStatus::Ready => "running".to_string(),
226 DaemonStatus::ShuttingDown => "shutting_down".to_string(),
227 DaemonStatus::Stopped => "stopped".to_string(),
228 }
229}
230
231fn format_uptime(secs: f64) -> String {
233 let total_secs = secs as u64;
234 let hours = total_secs / 3600;
235 let minutes = (total_secs % 3600) / 60;
236 let seconds = total_secs % 60;
237 format!("{}h {}m {}s", hours, minutes, seconds)
238}
239
240fn format_number(n: u64) -> String {
242 let s = n.to_string();
243 let bytes = s.as_bytes();
244 let mut result = String::new();
245 let len = bytes.len();
246
247 for (i, &b) in bytes.iter().enumerate() {
248 if i > 0 && (len - i).is_multiple_of(3) {
249 result.push(',');
250 }
251 result.push(b as char);
252 }
253
254 result
255}
256
257#[cfg(test)]
262mod tests {
263 use super::*;
264 use tempfile::TempDir;
265
266 #[test]
267 fn test_daemon_status_args_default() {
268 let args = DaemonStatusArgs {
269 project: PathBuf::from("."),
270 session: None,
271 };
272
273 assert_eq!(args.project, PathBuf::from("."));
274 assert!(args.session.is_none());
275 }
276
277 #[test]
278 fn test_daemon_status_args_with_session() {
279 let args = DaemonStatusArgs {
280 project: PathBuf::from("/test/project"),
281 session: Some("test-session".to_string()),
282 };
283
284 assert_eq!(args.session, Some("test-session".to_string()));
285 }
286
287 #[test]
288 fn test_format_status() {
289 assert_eq!(format_status(DaemonStatus::Ready), "running");
290 assert_eq!(format_status(DaemonStatus::Initializing), "initializing");
291 assert_eq!(format_status(DaemonStatus::Indexing), "indexing");
292 assert_eq!(format_status(DaemonStatus::ShuttingDown), "shutting_down");
293 assert_eq!(format_status(DaemonStatus::Stopped), "stopped");
294 }
295
296 #[test]
297 fn test_format_uptime() {
298 assert_eq!(format_uptime(0.0), "0h 0m 0s");
299 assert_eq!(format_uptime(61.0), "0h 1m 1s");
300 assert_eq!(format_uptime(3661.0), "1h 1m 1s");
301 assert_eq!(format_uptime(7200.0), "2h 0m 0s");
302 }
303
304 #[test]
305 fn test_format_number() {
306 assert_eq!(format_number(0), "0");
307 assert_eq!(format_number(999), "999");
308 assert_eq!(format_number(1000), "1,000");
309 assert_eq!(format_number(1234567), "1,234,567");
310 }
311
312 #[test]
313 fn test_daemon_status_output_serialization() {
314 let output = DaemonStatusOutput {
315 status: "running".to_string(),
316 uptime: Some(3600.0),
317 uptime_human: Some("1h 0m 0s".to_string()),
318 files: Some(100),
319 project: Some(PathBuf::from("/test/project")),
320 salsa_stats: Some(SalsaCacheStats {
321 hits: 90,
322 misses: 10,
323 invalidations: 5,
324 recomputations: 3,
325 }),
326 message: None,
327 };
328
329 let json = serde_json::to_string(&output).unwrap();
330 assert!(json.contains("running"));
331 assert!(json.contains("3600"));
332 assert!(json.contains("hits"));
333 }
334
335 #[test]
336 fn test_daemon_status_output_not_running() {
337 let output = DaemonStatusOutput {
338 status: "not_running".to_string(),
339 uptime: None,
340 uptime_human: None,
341 files: None,
342 project: None,
343 salsa_stats: None,
344 message: Some("Daemon not running".to_string()),
345 };
346
347 let json = serde_json::to_string(&output).unwrap();
348 assert!(json.contains("not_running"));
349 assert!(json.contains("not running"));
350 }
351
352 #[tokio::test]
353 async fn test_daemon_status_not_running() {
354 let temp = TempDir::new().unwrap();
355 let args = DaemonStatusArgs {
356 project: temp.path().to_path_buf(),
357 session: None,
358 };
359
360 let result = args.run_async(OutputFormat::Json, true).await;
362 assert!(result.is_ok());
363 }
364}