1use std::path::PathBuf;
6
7use clap::Parser;
8
9#[derive(Parser, Debug, Clone)]
11#[command(name = "claude-code-acp-rs")]
12#[command(version, about, long_about = None)]
13pub struct Cli {
14 #[arg(long)]
16 pub acp: bool,
17
18 #[arg(short = 'p', long, value_name = "PROMPT")]
20 pub prompt: Option<String>,
21
22 #[arg(short, long)]
24 pub diagnostic: bool,
25
26 #[arg(short = 'l', long, value_name = "DIR")]
28 pub log_dir: Option<PathBuf>,
29
30 #[arg(short = 'f', long, value_name = "FILE")]
32 pub log_file: Option<String>,
33
34 #[arg(short, long, action = clap::ArgAction::Count)]
37 pub verbose: u8,
38
39 #[arg(short, long)]
42 pub quiet: bool,
43
44 #[arg(long, value_name = "URL", env = "OTEL_EXPORTER_OTLP_ENDPOINT")]
48 pub otel_endpoint: Option<String>,
49
50 #[arg(long, value_name = "NAME", default_value = "claude-code-acp-rs")]
52 pub otel_service_name: String,
53}
54
55#[allow(clippy::derivable_impls)]
56impl Default for Cli {
57 fn default() -> Self {
58 Self {
59 acp: false,
60 prompt: None,
61 diagnostic: false,
62 log_dir: None,
63 log_file: None,
64 verbose: 0,
65 quiet: false,
66 otel_endpoint: None,
67 otel_service_name: "claude-code-acp-rs".to_string(),
68 }
69 }
70}
71
72impl Cli {
73 pub fn is_diagnostic(&self) -> bool {
77 self.diagnostic || self.log_dir.is_some() || self.log_file.is_some()
78 }
79
80 #[cfg(feature = "otel")]
84 pub fn is_otel_enabled(&self) -> bool {
85 self.otel_endpoint.is_some()
86 }
87
88 #[cfg(not(feature = "otel"))]
91 pub fn is_otel_enabled(&self) -> bool {
92 if self.otel_endpoint.is_some() {
93 tracing::warn!("--otel-endpoint specified but otel feature is not enabled, ignoring");
94 }
95 false
96 }
97
98 pub fn log_level(&self) -> tracing::Level {
105 if self.quiet {
106 tracing::Level::ERROR
107 } else {
108 match self.verbose {
109 0 => tracing::Level::INFO,
110 1 => tracing::Level::DEBUG,
111 _ => tracing::Level::TRACE,
112 }
113 }
114 }
115
116 pub fn log_path(&self) -> PathBuf {
122 let dir = self.log_dir.clone().unwrap_or_else(std::env::temp_dir);
123
124 let filename = self.log_file.clone().unwrap_or_else(|| {
125 let timestamp = chrono::Local::now().format("%Y%m%d-%H%M%S");
126 format!("claude-code-acp-rs-{timestamp}.log")
127 });
128
129 dir.join(filename)
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 #[test]
138 fn test_default_cli() {
139 let cli = Cli::default();
140 assert!(!cli.is_diagnostic());
141 assert_eq!(cli.log_level(), tracing::Level::INFO);
142 }
143
144 #[test]
145 fn test_diagnostic_mode() {
146 let cli = Cli {
147 diagnostic: true,
148 ..Default::default()
149 };
150 assert!(cli.is_diagnostic());
151 }
152
153 #[test]
154 fn test_log_dir_implies_diagnostic() {
155 let cli = Cli {
156 log_dir: Some(PathBuf::from("/tmp")),
157 ..Default::default()
158 };
159 assert!(cli.is_diagnostic());
160 }
161
162 #[test]
163 fn test_log_file_implies_diagnostic() {
164 let cli = Cli {
165 log_file: Some("test.log".to_string()),
166 ..Default::default()
167 };
168 assert!(cli.is_diagnostic());
169 }
170
171 #[test]
172 fn test_log_levels() {
173 let cli = Cli {
175 quiet: true,
176 ..Default::default()
177 };
178 assert_eq!(cli.log_level(), tracing::Level::ERROR);
179
180 let cli = Cli::default();
182 assert_eq!(cli.log_level(), tracing::Level::INFO);
183
184 let cli = Cli {
186 verbose: 1,
187 ..Default::default()
188 };
189 assert_eq!(cli.log_level(), tracing::Level::DEBUG);
190
191 let cli = Cli {
193 verbose: 2,
194 ..Default::default()
195 };
196 assert_eq!(cli.log_level(), tracing::Level::TRACE);
197 }
198
199 #[test]
200 fn test_log_path_custom_dir() {
201 let cli = Cli {
202 log_dir: Some(PathBuf::from("/var/log")),
203 log_file: Some("test.log".to_string()),
204 ..Default::default()
205 };
206 assert_eq!(cli.log_path(), PathBuf::from("/var/log/test.log"));
207 }
208
209 #[test]
210 fn test_log_path_default_generates_timestamp() {
211 let cli = Cli::default();
212 let path = cli.log_path();
213
214 assert!(path.starts_with(std::env::temp_dir()));
216
217 let filename = path.file_name().unwrap().to_str().unwrap();
219 assert!(filename.starts_with("claude-code-acp-rs-"));
220 assert!(
221 std::path::Path::new(filename)
222 .extension()
223 .is_some_and(|ext| ext.eq_ignore_ascii_case("log"))
224 );
225 }
226
227 #[test]
228 fn test_cli_acp_mode() {
229 let cli = Cli::parse_from(["claude-code-acp-rs", "--acp"]);
230 assert!(cli.acp);
231 assert!(cli.prompt.is_none());
232 }
233
234 #[test]
235 fn test_cli_headless_mode() {
236 let cli = Cli::parse_from(["claude-code-acp-rs", "-p", "test prompt"]);
237 assert!(!cli.acp);
238 assert_eq!(cli.prompt, Some("test prompt".to_string()));
239 }
240
241 #[test]
242 fn test_cli_headless_mode_long() {
243 let cli = Cli::parse_from(["claude-code-acp-rs", "--prompt", "test prompt"]);
244 assert!(!cli.acp);
245 assert_eq!(cli.prompt, Some("test prompt".to_string()));
246 }
247
248 #[test]
249 fn test_cli_no_mode() {
250 let cli = Cli::parse_from(["claude-code-acp-rs"]);
251 assert!(!cli.acp);
252 assert!(cli.prompt.is_none());
253 }
254}