tldr_cli/commands/daemon/
query.rs1use std::path::PathBuf;
16
17use clap::Args;
18use serde::Serialize;
19
20use crate::output::OutputFormat;
21
22use super::error::{DaemonError, DaemonResult};
23use super::ipc::send_raw_command;
24
25#[derive(Debug, Clone, Args)]
31pub struct DaemonQueryArgs {
32 pub cmd: String,
34
35 #[arg(long, short = 'p', default_value = ".")]
37 pub project: PathBuf,
38
39 #[arg(long, short = 'j')]
41 pub json: Option<String>,
42}
43
44#[derive(Debug, Clone, Serialize)]
50pub struct DaemonQueryErrorOutput {
51 pub status: String,
53 pub error: String,
55}
56
57impl DaemonQueryArgs {
62 pub fn run(&self, format: OutputFormat, quiet: bool) -> anyhow::Result<()> {
64 let runtime = tokio::runtime::Runtime::new()?;
66 runtime.block_on(self.run_async(format, quiet))
67 }
68
69 async fn run_async(&self, format: OutputFormat, quiet: bool) -> anyhow::Result<()> {
71 let project = self.project.canonicalize().unwrap_or_else(|_| {
73 std::env::current_dir()
74 .unwrap_or_else(|_| PathBuf::from("."))
75 .join(&self.project)
76 });
77
78 let command_json = self.build_command_json()?;
80
81 match send_raw_command(&project, &command_json).await {
83 Ok(response) => {
84 if !quiet {
86 match format {
87 OutputFormat::Json => {
88 if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&response)
90 {
91 println!("{}", serde_json::to_string_pretty(&parsed)?);
92 } else {
93 println!("{}", response);
94 }
95 }
96 OutputFormat::Compact => {
97 println!("{}", response);
99 }
100 OutputFormat::Text | OutputFormat::Sarif | OutputFormat::Dot => {
101 if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&response)
103 {
104 self.print_text_output(&parsed);
105 } else {
106 println!("{}", response);
107 }
108 }
109 }
110 }
111 Ok(())
112 }
113 Err(DaemonError::NotRunning) | Err(DaemonError::ConnectionRefused) => {
114 let output = DaemonQueryErrorOutput {
115 status: "error".to_string(),
116 error: "Daemon not running".to_string(),
117 };
118
119 if !quiet {
120 match format {
121 OutputFormat::Json | OutputFormat::Compact => {
122 println!("{}", serde_json::to_string_pretty(&output)?);
123 }
124 OutputFormat::Text | OutputFormat::Sarif | OutputFormat::Dot => {
125 eprintln!("Error: Daemon not running");
126 }
127 }
128 }
129
130 Err(anyhow::anyhow!("Daemon not running"))
131 }
132 Err(DaemonError::InvalidMessage(msg)) => {
133 let output = DaemonQueryErrorOutput {
134 status: "error".to_string(),
135 error: format!("Invalid JSON parameters: {}", msg),
136 };
137
138 if !quiet {
139 match format {
140 OutputFormat::Json | OutputFormat::Compact => {
141 println!("{}", serde_json::to_string_pretty(&output)?);
142 }
143 OutputFormat::Text | OutputFormat::Sarif | OutputFormat::Dot => {
144 eprintln!("Error: Invalid JSON parameters: {}", msg);
145 }
146 }
147 }
148
149 Err(anyhow::anyhow!("Invalid JSON parameters: {}", msg))
150 }
151 Err(e) => {
152 let output = DaemonQueryErrorOutput {
153 status: "error".to_string(),
154 error: e.to_string(),
155 };
156
157 if !quiet {
158 match format {
159 OutputFormat::Json | OutputFormat::Compact => {
160 println!("{}", serde_json::to_string_pretty(&output)?);
161 }
162 OutputFormat::Text | OutputFormat::Sarif | OutputFormat::Dot => {
163 eprintln!("Error: {}", e);
164 }
165 }
166 }
167
168 Err(anyhow::anyhow!("Query failed: {}", e))
169 }
170 }
171 }
172
173 fn build_command_json(&self) -> anyhow::Result<String> {
175 let mut cmd_obj = serde_json::json!({
177 "cmd": self.cmd.to_lowercase()
178 });
179
180 if let Some(json_str) = &self.json {
182 let params: serde_json::Value = serde_json::from_str(json_str)
183 .map_err(|e| anyhow::anyhow!("Invalid JSON parameters: {}", e))?;
184
185 if let serde_json::Value::Object(params_obj) = params {
186 if let serde_json::Value::Object(ref mut cmd_map) = cmd_obj {
187 for (key, value) in params_obj {
188 cmd_map.insert(key, value);
189 }
190 }
191 }
192 }
193
194 Ok(serde_json::to_string(&cmd_obj)?)
195 }
196
197 fn print_text_output(&self, response: &serde_json::Value) {
199 if let Some(error) = response.get("error") {
201 eprintln!("Error: {}", error);
202 return;
203 }
204
205 if let Some(status) = response.get("status") {
207 println!("Status: {}", status);
208 if let Some(message) = response.get("message") {
209 println!("{}", message);
210 }
211 }
212
213 if response.as_object().map(|o| o.len() > 2).unwrap_or(false) {
215 println!(
216 "{}",
217 serde_json::to_string_pretty(response).unwrap_or_default()
218 );
219 }
220 }
221}
222
223pub async fn cmd_query(args: DaemonQueryArgs) -> DaemonResult<()> {
227 let project = args.project.canonicalize().unwrap_or_else(|_| {
229 std::env::current_dir()
230 .unwrap_or_else(|_| PathBuf::from("."))
231 .join(&args.project)
232 });
233
234 let mut cmd_obj = serde_json::json!({
236 "cmd": args.cmd.to_lowercase()
237 });
238
239 if let Some(json_str) = &args.json {
241 let params: serde_json::Value = serde_json::from_str(json_str)
242 .map_err(|e| DaemonError::InvalidMessage(format!("Invalid JSON: {}", e)))?;
243
244 if let serde_json::Value::Object(params_obj) = params {
245 if let serde_json::Value::Object(ref mut cmd_map) = cmd_obj {
246 for (key, value) in params_obj {
247 cmd_map.insert(key, value);
248 }
249 }
250 }
251 }
252
253 let command_json =
254 serde_json::to_string(&cmd_obj).map_err(|e| DaemonError::InvalidMessage(e.to_string()))?;
255
256 let response = send_raw_command(&project, &command_json).await?;
258
259 println!("{}", response);
261
262 Ok(())
263}
264
265#[cfg(test)]
270mod tests {
271 use super::*;
272 use tempfile::TempDir;
273
274 #[test]
275 fn test_daemon_query_args_default() {
276 let args = DaemonQueryArgs {
277 cmd: "ping".to_string(),
278 project: PathBuf::from("."),
279 json: None,
280 };
281
282 assert_eq!(args.cmd, "ping");
283 assert_eq!(args.project, PathBuf::from("."));
284 assert!(args.json.is_none());
285 }
286
287 #[test]
288 fn test_daemon_query_args_with_json() {
289 let args = DaemonQueryArgs {
290 cmd: "search".to_string(),
291 project: PathBuf::from("/test/project"),
292 json: Some(r#"{"pattern": "fn main"}"#.to_string()),
293 };
294
295 assert_eq!(args.cmd, "search");
296 assert!(args.json.is_some());
297 }
298
299 #[test]
300 fn test_build_command_json_simple() {
301 let args = DaemonQueryArgs {
302 cmd: "ping".to_string(),
303 project: PathBuf::from("."),
304 json: None,
305 };
306
307 let json = args.build_command_json().unwrap();
308 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
309
310 assert_eq!(parsed.get("cmd").unwrap(), "ping");
311 }
312
313 #[test]
314 fn test_build_command_json_with_params() {
315 let args = DaemonQueryArgs {
316 cmd: "search".to_string(),
317 project: PathBuf::from("."),
318 json: Some(r#"{"pattern": "fn main", "max_results": 10}"#.to_string()),
319 };
320
321 let json = args.build_command_json().unwrap();
322 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
323
324 assert_eq!(parsed.get("cmd").unwrap(), "search");
325 assert_eq!(parsed.get("pattern").unwrap(), "fn main");
326 assert_eq!(parsed.get("max_results").unwrap(), 10);
327 }
328
329 #[test]
330 fn test_build_command_json_invalid_params() {
331 let args = DaemonQueryArgs {
332 cmd: "search".to_string(),
333 project: PathBuf::from("."),
334 json: Some("not valid json".to_string()),
335 };
336
337 let result = args.build_command_json();
338 assert!(result.is_err());
339 }
340
341 #[test]
342 fn test_build_command_json_lowercases_cmd() {
343 let args = DaemonQueryArgs {
344 cmd: "PING".to_string(),
345 project: PathBuf::from("."),
346 json: None,
347 };
348
349 let json = args.build_command_json().unwrap();
350 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
351
352 assert_eq!(parsed.get("cmd").unwrap(), "ping");
353 }
354
355 #[test]
356 fn test_daemon_query_error_output_serialization() {
357 let output = DaemonQueryErrorOutput {
358 status: "error".to_string(),
359 error: "Daemon not running".to_string(),
360 };
361
362 let json = serde_json::to_string(&output).unwrap();
363 assert!(json.contains("error"));
364 assert!(json.contains("Daemon not running"));
365 }
366
367 #[tokio::test]
368 async fn test_daemon_query_not_running() {
369 let temp = TempDir::new().unwrap();
370 let args = DaemonQueryArgs {
371 cmd: "ping".to_string(),
372 project: temp.path().to_path_buf(),
373 json: None,
374 };
375
376 let result = cmd_query(args).await;
378 assert!(result.is_err());
379 }
380}