use std::path::PathBuf;
use clap::Parser;
#[derive(Parser, Debug, Clone)]
#[command(name = "claude-code-acp-rs")]
#[command(version, about, long_about = None)]
pub struct Cli {
#[arg(long)]
pub acp: bool,
#[arg(short = 'p', long, value_name = "PROMPT")]
pub prompt: Option<String>,
#[arg(short, long)]
pub diagnostic: bool,
#[arg(short = 'l', long, value_name = "DIR")]
pub log_dir: Option<PathBuf>,
#[arg(short = 'f', long, value_name = "FILE")]
pub log_file: Option<String>,
#[arg(short, long, action = clap::ArgAction::Count)]
pub verbose: u8,
#[arg(short, long)]
pub quiet: bool,
#[arg(long, value_name = "URL", env = "OTEL_EXPORTER_OTLP_ENDPOINT")]
pub otel_endpoint: Option<String>,
#[arg(long, value_name = "NAME", default_value = "claude-code-acp-rs")]
pub otel_service_name: String,
}
#[allow(clippy::derivable_impls)]
impl Default for Cli {
fn default() -> Self {
Self {
acp: false,
prompt: None,
diagnostic: false,
log_dir: None,
log_file: None,
verbose: 0,
quiet: false,
otel_endpoint: None,
otel_service_name: "claude-code-acp-rs".to_string(),
}
}
}
impl Cli {
pub fn is_diagnostic(&self) -> bool {
self.diagnostic || self.log_dir.is_some() || self.log_file.is_some()
}
#[cfg(feature = "otel")]
pub fn is_otel_enabled(&self) -> bool {
self.otel_endpoint.is_some()
}
#[cfg(not(feature = "otel"))]
pub fn is_otel_enabled(&self) -> bool {
if self.otel_endpoint.is_some() {
tracing::warn!("--otel-endpoint specified but otel feature is not enabled, ignoring");
}
false
}
pub fn log_level(&self) -> tracing::Level {
if self.quiet {
tracing::Level::ERROR
} else {
match self.verbose {
0 => tracing::Level::INFO,
1 => tracing::Level::DEBUG,
_ => tracing::Level::TRACE,
}
}
}
pub fn log_path(&self) -> PathBuf {
let dir = self.log_dir.clone().unwrap_or_else(std::env::temp_dir);
let filename = self.log_file.clone().unwrap_or_else(|| {
let timestamp = chrono::Local::now().format("%Y%m%d-%H%M%S");
format!("claude-code-acp-rs-{timestamp}.log")
});
dir.join(filename)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_cli() {
let cli = Cli::default();
assert!(!cli.is_diagnostic());
assert_eq!(cli.log_level(), tracing::Level::INFO);
}
#[test]
fn test_diagnostic_mode() {
let cli = Cli {
diagnostic: true,
..Default::default()
};
assert!(cli.is_diagnostic());
}
#[test]
fn test_log_dir_implies_diagnostic() {
let cli = Cli {
log_dir: Some(PathBuf::from("/tmp")),
..Default::default()
};
assert!(cli.is_diagnostic());
}
#[test]
fn test_log_file_implies_diagnostic() {
let cli = Cli {
log_file: Some("test.log".to_string()),
..Default::default()
};
assert!(cli.is_diagnostic());
}
#[test]
fn test_log_levels() {
let cli = Cli {
quiet: true,
..Default::default()
};
assert_eq!(cli.log_level(), tracing::Level::ERROR);
let cli = Cli::default();
assert_eq!(cli.log_level(), tracing::Level::INFO);
let cli = Cli {
verbose: 1,
..Default::default()
};
assert_eq!(cli.log_level(), tracing::Level::DEBUG);
let cli = Cli {
verbose: 2,
..Default::default()
};
assert_eq!(cli.log_level(), tracing::Level::TRACE);
}
#[test]
fn test_log_path_custom_dir() {
let cli = Cli {
log_dir: Some(PathBuf::from("/var/log")),
log_file: Some("test.log".to_string()),
..Default::default()
};
assert_eq!(cli.log_path(), PathBuf::from("/var/log/test.log"));
}
#[test]
fn test_log_path_default_generates_timestamp() {
let cli = Cli::default();
let path = cli.log_path();
assert!(path.starts_with(std::env::temp_dir()));
let filename = path.file_name().unwrap().to_str().unwrap();
assert!(filename.starts_with("claude-code-acp-rs-"));
assert!(
std::path::Path::new(filename)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("log"))
);
}
#[test]
fn test_cli_acp_mode() {
let cli = Cli::parse_from(["claude-code-acp-rs", "--acp"]);
assert!(cli.acp);
assert!(cli.prompt.is_none());
}
#[test]
fn test_cli_headless_mode() {
let cli = Cli::parse_from(["claude-code-acp-rs", "-p", "test prompt"]);
assert!(!cli.acp);
assert_eq!(cli.prompt, Some("test prompt".to_string()));
}
#[test]
fn test_cli_headless_mode_long() {
let cli = Cli::parse_from(["claude-code-acp-rs", "--prompt", "test prompt"]);
assert!(!cli.acp);
assert_eq!(cli.prompt, Some("test prompt".to_string()));
}
#[test]
fn test_cli_no_mode() {
let cli = Cli::parse_from(["claude-code-acp-rs"]);
assert!(!cli.acp);
assert!(cli.prompt.is_none());
}
}