use std::path::PathBuf;
use clap::Args;
use serde::Serialize;
use crate::output::OutputFormat;
use super::error::{DaemonError, DaemonResult};
use super::ipc::send_raw_command;
#[derive(Debug, Clone, Args)]
pub struct DaemonQueryArgs {
pub cmd: String,
#[arg(long, short = 'p', default_value = ".")]
pub project: PathBuf,
#[arg(long, short = 'j')]
pub json: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct DaemonQueryErrorOutput {
pub status: String,
pub error: String,
}
impl DaemonQueryArgs {
pub fn run(&self, format: OutputFormat, quiet: bool) -> anyhow::Result<()> {
let runtime = tokio::runtime::Runtime::new()?;
runtime.block_on(self.run_async(format, quiet))
}
async fn run_async(&self, format: OutputFormat, quiet: bool) -> anyhow::Result<()> {
let project = self.project.canonicalize().unwrap_or_else(|_| {
std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join(&self.project)
});
let command_json = self.build_command_json()?;
match send_raw_command(&project, &command_json).await {
Ok(response) => {
if !quiet {
match format {
OutputFormat::Json => {
if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&response)
{
println!("{}", serde_json::to_string_pretty(&parsed)?);
} else {
println!("{}", response);
}
}
OutputFormat::Compact => {
println!("{}", response);
}
OutputFormat::Text | OutputFormat::Sarif | OutputFormat::Dot => {
if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&response)
{
self.print_text_output(&parsed);
} else {
println!("{}", response);
}
}
}
}
Ok(())
}
Err(DaemonError::NotRunning) | Err(DaemonError::ConnectionRefused) => {
let output = DaemonQueryErrorOutput {
status: "error".to_string(),
error: "Daemon not running".to_string(),
};
if !quiet {
match format {
OutputFormat::Json | OutputFormat::Compact => {
println!("{}", serde_json::to_string_pretty(&output)?);
}
OutputFormat::Text | OutputFormat::Sarif | OutputFormat::Dot => {
eprintln!("Error: Daemon not running");
}
}
}
Err(anyhow::anyhow!("Daemon not running"))
}
Err(DaemonError::InvalidMessage(msg)) => {
let output = DaemonQueryErrorOutput {
status: "error".to_string(),
error: format!("Invalid JSON parameters: {}", msg),
};
if !quiet {
match format {
OutputFormat::Json | OutputFormat::Compact => {
println!("{}", serde_json::to_string_pretty(&output)?);
}
OutputFormat::Text | OutputFormat::Sarif | OutputFormat::Dot => {
eprintln!("Error: Invalid JSON parameters: {}", msg);
}
}
}
Err(anyhow::anyhow!("Invalid JSON parameters: {}", msg))
}
Err(e) => {
let output = DaemonQueryErrorOutput {
status: "error".to_string(),
error: e.to_string(),
};
if !quiet {
match format {
OutputFormat::Json | OutputFormat::Compact => {
println!("{}", serde_json::to_string_pretty(&output)?);
}
OutputFormat::Text | OutputFormat::Sarif | OutputFormat::Dot => {
eprintln!("Error: {}", e);
}
}
}
Err(anyhow::anyhow!("Query failed: {}", e))
}
}
}
fn build_command_json(&self) -> anyhow::Result<String> {
let mut cmd_obj = serde_json::json!({
"cmd": self.cmd.to_lowercase()
});
if let Some(json_str) = &self.json {
let params: serde_json::Value = serde_json::from_str(json_str)
.map_err(|e| anyhow::anyhow!("Invalid JSON parameters: {}", e))?;
if let serde_json::Value::Object(params_obj) = params {
if let serde_json::Value::Object(ref mut cmd_map) = cmd_obj {
for (key, value) in params_obj {
cmd_map.insert(key, value);
}
}
}
}
Ok(serde_json::to_string(&cmd_obj)?)
}
fn print_text_output(&self, response: &serde_json::Value) {
if let Some(error) = response.get("error") {
eprintln!("Error: {}", error);
return;
}
if let Some(status) = response.get("status") {
println!("Status: {}", status);
if let Some(message) = response.get("message") {
println!("{}", message);
}
}
if response.as_object().map(|o| o.len() > 2).unwrap_or(false) {
println!(
"{}",
serde_json::to_string_pretty(response).unwrap_or_default()
);
}
}
}
pub async fn cmd_query(args: DaemonQueryArgs) -> DaemonResult<()> {
let project = args.project.canonicalize().unwrap_or_else(|_| {
std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join(&args.project)
});
let mut cmd_obj = serde_json::json!({
"cmd": args.cmd.to_lowercase()
});
if let Some(json_str) = &args.json {
let params: serde_json::Value = serde_json::from_str(json_str)
.map_err(|e| DaemonError::InvalidMessage(format!("Invalid JSON: {}", e)))?;
if let serde_json::Value::Object(params_obj) = params {
if let serde_json::Value::Object(ref mut cmd_map) = cmd_obj {
for (key, value) in params_obj {
cmd_map.insert(key, value);
}
}
}
}
let command_json =
serde_json::to_string(&cmd_obj).map_err(|e| DaemonError::InvalidMessage(e.to_string()))?;
let response = send_raw_command(&project, &command_json).await?;
println!("{}", response);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_daemon_query_args_default() {
let args = DaemonQueryArgs {
cmd: "ping".to_string(),
project: PathBuf::from("."),
json: None,
};
assert_eq!(args.cmd, "ping");
assert_eq!(args.project, PathBuf::from("."));
assert!(args.json.is_none());
}
#[test]
fn test_daemon_query_args_with_json() {
let args = DaemonQueryArgs {
cmd: "search".to_string(),
project: PathBuf::from("/test/project"),
json: Some(r#"{"pattern": "fn main"}"#.to_string()),
};
assert_eq!(args.cmd, "search");
assert!(args.json.is_some());
}
#[test]
fn test_build_command_json_simple() {
let args = DaemonQueryArgs {
cmd: "ping".to_string(),
project: PathBuf::from("."),
json: None,
};
let json = args.build_command_json().unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.get("cmd").unwrap(), "ping");
}
#[test]
fn test_build_command_json_with_params() {
let args = DaemonQueryArgs {
cmd: "search".to_string(),
project: PathBuf::from("."),
json: Some(r#"{"pattern": "fn main", "max_results": 10}"#.to_string()),
};
let json = args.build_command_json().unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.get("cmd").unwrap(), "search");
assert_eq!(parsed.get("pattern").unwrap(), "fn main");
assert_eq!(parsed.get("max_results").unwrap(), 10);
}
#[test]
fn test_build_command_json_invalid_params() {
let args = DaemonQueryArgs {
cmd: "search".to_string(),
project: PathBuf::from("."),
json: Some("not valid json".to_string()),
};
let result = args.build_command_json();
assert!(result.is_err());
}
#[test]
fn test_build_command_json_lowercases_cmd() {
let args = DaemonQueryArgs {
cmd: "PING".to_string(),
project: PathBuf::from("."),
json: None,
};
let json = args.build_command_json().unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.get("cmd").unwrap(), "ping");
}
#[test]
fn test_daemon_query_error_output_serialization() {
let output = DaemonQueryErrorOutput {
status: "error".to_string(),
error: "Daemon not running".to_string(),
};
let json = serde_json::to_string(&output).unwrap();
assert!(json.contains("error"));
assert!(json.contains("Daemon not running"));
}
#[tokio::test]
async fn test_daemon_query_not_running() {
let temp = TempDir::new().unwrap();
let args = DaemonQueryArgs {
cmd: "ping".to_string(),
project: temp.path().to_path_buf(),
json: None,
};
let result = cmd_query(args).await;
assert!(result.is_err());
}
}