1use crate::error::{Error, Result};
38use std::path::PathBuf;
39use std::process::Stdio;
40use tokio::process::{Child, Command};
41use tracing::debug;
42use uuid::Uuid;
43
44#[derive(Debug, Clone, Copy)]
46pub enum PermissionMode {
47 AcceptEdits,
48 BypassPermissions,
49 Default,
50 Plan,
51}
52
53impl PermissionMode {
54 fn as_str(&self) -> &'static str {
55 match self {
56 PermissionMode::AcceptEdits => "acceptEdits",
57 PermissionMode::BypassPermissions => "bypassPermissions",
58 PermissionMode::Default => "default",
59 PermissionMode::Plan => "plan",
60 }
61 }
62}
63
64#[derive(Debug, Clone)]
72pub struct ClaudeCliBuilder {
73 command: PathBuf,
74 prompt: Option<String>,
75 debug: Option<String>,
76 verbose: bool,
77 dangerously_skip_permissions: bool,
78 allowed_tools: Vec<String>,
79 disallowed_tools: Vec<String>,
80 mcp_config: Vec<String>,
81 append_system_prompt: Option<String>,
82 permission_mode: Option<PermissionMode>,
83 continue_conversation: bool,
84 resume: Option<String>,
85 model: Option<String>,
86 fallback_model: Option<String>,
87 settings: Option<String>,
88 add_dir: Vec<PathBuf>,
89 ide: bool,
90 strict_mcp_config: bool,
91 session_id: Option<Uuid>,
92 oauth_token: Option<String>,
93 api_key: Option<String>,
94}
95
96impl Default for ClaudeCliBuilder {
97 fn default() -> Self {
98 Self::new()
99 }
100}
101
102impl ClaudeCliBuilder {
103 pub fn new() -> Self {
105 Self {
106 command: PathBuf::from("claude"),
107 prompt: None,
108 debug: None,
109 verbose: false,
110 dangerously_skip_permissions: false,
111 allowed_tools: Vec::new(),
112 disallowed_tools: Vec::new(),
113 mcp_config: Vec::new(),
114 append_system_prompt: None,
115 permission_mode: None,
116 continue_conversation: false,
117 resume: None,
118 model: None,
119 fallback_model: None,
120 settings: None,
121 add_dir: Vec::new(),
122 ide: false,
123 strict_mcp_config: false,
124 session_id: None,
125 oauth_token: None,
126 api_key: None,
127 }
128 }
129
130 pub fn command<P: Into<PathBuf>>(mut self, path: P) -> Self {
132 self.command = path.into();
133 self
134 }
135
136 pub fn prompt<S: Into<String>>(mut self, prompt: S) -> Self {
138 self.prompt = Some(prompt.into());
139 self
140 }
141
142 pub fn debug<S: Into<String>>(mut self, filter: Option<S>) -> Self {
144 self.debug = filter.map(|s| s.into());
145 self
146 }
147
148 pub fn verbose(mut self, verbose: bool) -> Self {
150 self.verbose = verbose;
151 self
152 }
153
154 pub fn dangerously_skip_permissions(mut self, skip: bool) -> Self {
156 self.dangerously_skip_permissions = skip;
157 self
158 }
159
160 pub fn allowed_tools<I, S>(mut self, tools: I) -> Self
162 where
163 I: IntoIterator<Item = S>,
164 S: Into<String>,
165 {
166 self.allowed_tools
167 .extend(tools.into_iter().map(|s| s.into()));
168 self
169 }
170
171 pub fn disallowed_tools<I, S>(mut self, tools: I) -> Self
173 where
174 I: IntoIterator<Item = S>,
175 S: Into<String>,
176 {
177 self.disallowed_tools
178 .extend(tools.into_iter().map(|s| s.into()));
179 self
180 }
181
182 pub fn mcp_config<I, S>(mut self, configs: I) -> Self
184 where
185 I: IntoIterator<Item = S>,
186 S: Into<String>,
187 {
188 self.mcp_config
189 .extend(configs.into_iter().map(|s| s.into()));
190 self
191 }
192
193 pub fn append_system_prompt<S: Into<String>>(mut self, prompt: S) -> Self {
195 self.append_system_prompt = Some(prompt.into());
196 self
197 }
198
199 pub fn permission_mode(mut self, mode: PermissionMode) -> Self {
201 self.permission_mode = Some(mode);
202 self
203 }
204
205 pub fn continue_conversation(mut self, continue_conv: bool) -> Self {
207 self.continue_conversation = continue_conv;
208 self
209 }
210
211 pub fn resume<S: Into<String>>(mut self, session_id: Option<S>) -> Self {
213 self.resume = session_id.map(|s| s.into());
214 self
215 }
216
217 pub fn model<S: Into<String>>(mut self, model: S) -> Self {
219 self.model = Some(model.into());
220 self
221 }
222
223 pub fn fallback_model<S: Into<String>>(mut self, model: S) -> Self {
225 self.fallback_model = Some(model.into());
226 self
227 }
228
229 pub fn settings<S: Into<String>>(mut self, settings: S) -> Self {
231 self.settings = Some(settings.into());
232 self
233 }
234
235 pub fn add_directories<I, P>(mut self, dirs: I) -> Self
237 where
238 I: IntoIterator<Item = P>,
239 P: Into<PathBuf>,
240 {
241 self.add_dir.extend(dirs.into_iter().map(|p| p.into()));
242 self
243 }
244
245 pub fn ide(mut self, ide: bool) -> Self {
247 self.ide = ide;
248 self
249 }
250
251 pub fn strict_mcp_config(mut self, strict: bool) -> Self {
253 self.strict_mcp_config = strict;
254 self
255 }
256
257 pub fn session_id(mut self, id: Uuid) -> Self {
259 self.session_id = Some(id);
260 self
261 }
262
263 pub fn oauth_token<S: Into<String>>(mut self, token: S) -> Self {
265 let token_str = token.into();
266 if !token_str.starts_with("sk-ant-oat") {
267 eprintln!("Warning: OAuth token should start with 'sk-ant-oat'");
268 }
269 self.oauth_token = Some(token_str);
270 self
271 }
272
273 pub fn api_key<S: Into<String>>(mut self, key: S) -> Self {
275 let key_str = key.into();
276 if !key_str.starts_with("sk-ant-api") {
277 eprintln!("Warning: API key should start with 'sk-ant-api'");
278 }
279 self.api_key = Some(key_str);
280 self
281 }
282
283 fn build_args(&self) -> Vec<String> {
285 let mut args = vec![
288 "--print".to_string(),
289 "--verbose".to_string(),
290 "--output-format".to_string(),
291 "stream-json".to_string(),
292 "--input-format".to_string(),
293 "stream-json".to_string(),
294 ];
295
296 if let Some(ref debug) = self.debug {
297 args.push("--debug".to_string());
298 if !debug.is_empty() {
299 args.push(debug.clone());
300 }
301 }
302
303 if self.dangerously_skip_permissions {
304 args.push("--dangerously-skip-permissions".to_string());
305 }
306
307 if !self.allowed_tools.is_empty() {
308 args.push("--allowed-tools".to_string());
309 args.extend(self.allowed_tools.clone());
310 }
311
312 if !self.disallowed_tools.is_empty() {
313 args.push("--disallowed-tools".to_string());
314 args.extend(self.disallowed_tools.clone());
315 }
316
317 if !self.mcp_config.is_empty() {
318 args.push("--mcp-config".to_string());
319 args.extend(self.mcp_config.clone());
320 }
321
322 if let Some(ref prompt) = self.append_system_prompt {
323 args.push("--append-system-prompt".to_string());
324 args.push(prompt.clone());
325 }
326
327 if let Some(ref mode) = self.permission_mode {
328 args.push("--permission-mode".to_string());
329 args.push(mode.as_str().to_string());
330 }
331
332 if self.continue_conversation {
333 args.push("--continue".to_string());
334 }
335
336 if let Some(ref session) = self.resume {
337 args.push("--resume".to_string());
338 args.push(session.clone());
339 }
340
341 if let Some(ref model) = self.model {
342 args.push("--model".to_string());
343 args.push(model.clone());
344 }
345
346 if let Some(ref model) = self.fallback_model {
347 args.push("--fallback-model".to_string());
348 args.push(model.clone());
349 }
350
351 if let Some(ref settings) = self.settings {
352 args.push("--settings".to_string());
353 args.push(settings.clone());
354 }
355
356 if !self.add_dir.is_empty() {
357 args.push("--add-dir".to_string());
358 for dir in &self.add_dir {
359 args.push(dir.to_string_lossy().to_string());
360 }
361 }
362
363 if self.ide {
364 args.push("--ide".to_string());
365 }
366
367 if self.strict_mcp_config {
368 args.push("--strict-mcp-config".to_string());
369 }
370
371 args.push("--session-id".to_string());
373 let session_uuid = self.session_id.unwrap_or_else(|| {
374 let uuid = Uuid::new_v4();
375 debug!("[CLI] Generated session UUID: {}", uuid);
376 uuid
377 });
378 args.push(session_uuid.to_string());
379
380 if let Some(ref prompt) = self.prompt {
382 args.push(prompt.clone());
383 }
384
385 args
386 }
387
388 pub async fn spawn(self) -> Result<Child> {
390 let args = self.build_args();
391
392 debug!(
394 "[CLI] Executing command: {} {}",
395 self.command.display(),
396 args.join(" ")
397 );
398
399 let mut cmd = Command::new(&self.command);
400 cmd.args(&args)
401 .stdin(Stdio::piped())
402 .stdout(Stdio::piped())
403 .stderr(Stdio::piped());
404
405 if let Some(ref token) = self.oauth_token {
407 cmd.env("CLAUDE_CODE_OAUTH_TOKEN", token);
408 debug!("[CLI] Setting CLAUDE_CODE_OAUTH_TOKEN environment variable");
409 }
410
411 if let Some(ref key) = self.api_key {
413 cmd.env("ANTHROPIC_API_KEY", key);
414 debug!("[CLI] Setting ANTHROPIC_API_KEY environment variable");
415 }
416
417 let child = cmd.spawn().map_err(Error::Io)?;
418
419 Ok(child)
420 }
421
422 pub fn build_command(self) -> Command {
424 let args = self.build_args();
425 let mut cmd = Command::new(&self.command);
426 cmd.args(&args)
427 .stdin(Stdio::piped())
428 .stdout(Stdio::piped())
429 .stderr(Stdio::piped());
430
431 if let Some(ref token) = self.oauth_token {
433 cmd.env("CLAUDE_CODE_OAUTH_TOKEN", token);
434 }
435
436 if let Some(ref key) = self.api_key {
438 cmd.env("ANTHROPIC_API_KEY", key);
439 }
440
441 cmd
442 }
443
444 pub fn spawn_sync(self) -> std::io::Result<std::process::Child> {
446 let args = self.build_args();
447
448 debug!(
450 "[CLI] Executing sync command: {} {}",
451 self.command.display(),
452 args.join(" ")
453 );
454
455 let mut cmd = std::process::Command::new(&self.command);
456 cmd.args(&args)
457 .stdin(Stdio::piped())
458 .stdout(Stdio::piped())
459 .stderr(Stdio::piped());
460
461 if let Some(ref token) = self.oauth_token {
463 cmd.env("CLAUDE_CODE_OAUTH_TOKEN", token);
464 debug!("[CLI] Setting CLAUDE_CODE_OAUTH_TOKEN environment variable");
465 }
466
467 if let Some(ref key) = self.api_key {
469 cmd.env("ANTHROPIC_API_KEY", key);
470 debug!("[CLI] Setting ANTHROPIC_API_KEY environment variable");
471 }
472
473 cmd.spawn()
474 }
475}
476
477#[cfg(test)]
478mod tests {
479 use super::*;
480
481 #[test]
482 fn test_streaming_flags_always_present() {
483 let builder = ClaudeCliBuilder::new();
484 let args = builder.build_args();
485
486 assert!(args.contains(&"--print".to_string()));
488 assert!(args.contains(&"--verbose".to_string())); assert!(args.contains(&"--output-format".to_string()));
490 assert!(args.contains(&"stream-json".to_string()));
491 assert!(args.contains(&"--input-format".to_string()));
492 }
493
494 #[test]
495 fn test_with_prompt() {
496 let builder = ClaudeCliBuilder::new().prompt("Hello, Claude!");
497 let args = builder.build_args();
498
499 assert_eq!(args.last().unwrap(), "Hello, Claude!");
500 }
501
502 #[test]
503 fn test_with_model() {
504 let builder = ClaudeCliBuilder::new()
505 .model("sonnet")
506 .fallback_model("opus");
507 let args = builder.build_args();
508
509 assert!(args.contains(&"--model".to_string()));
510 assert!(args.contains(&"sonnet".to_string()));
511 assert!(args.contains(&"--fallback-model".to_string()));
512 assert!(args.contains(&"opus".to_string()));
513 }
514
515 #[test]
516 fn test_with_debug() {
517 let builder = ClaudeCliBuilder::new().debug(Some("api"));
518 let args = builder.build_args();
519
520 assert!(args.contains(&"--debug".to_string()));
521 assert!(args.contains(&"api".to_string()));
522 }
523
524 #[test]
525 fn test_with_oauth_token() {
526 let valid_token = "sk-ant-oat-123456789";
527 let builder = ClaudeCliBuilder::new().oauth_token(valid_token);
528
529 let args = builder.clone().build_args();
531 assert!(!args.contains(&valid_token.to_string()));
532
533 assert_eq!(builder.oauth_token, Some(valid_token.to_string()));
535 }
536
537 #[test]
538 fn test_oauth_token_validation() {
539 let invalid_token = "invalid-token-123";
541 let builder = ClaudeCliBuilder::new().oauth_token(invalid_token);
542 assert_eq!(builder.oauth_token, Some(invalid_token.to_string()));
543 }
544
545 #[test]
546 fn test_with_api_key() {
547 let valid_key = "sk-ant-api-987654321";
548 let builder = ClaudeCliBuilder::new().api_key(valid_key);
549
550 let args = builder.clone().build_args();
552 assert!(!args.contains(&valid_key.to_string()));
553
554 assert_eq!(builder.api_key, Some(valid_key.to_string()));
556 }
557
558 #[test]
559 fn test_api_key_validation() {
560 let invalid_key = "invalid-api-key";
562 let builder = ClaudeCliBuilder::new().api_key(invalid_key);
563 assert_eq!(builder.api_key, Some(invalid_key.to_string()));
564 }
565
566 #[test]
567 fn test_both_auth_methods() {
568 let oauth = "sk-ant-oat-123";
569 let api_key = "sk-ant-api-456";
570 let builder = ClaudeCliBuilder::new().oauth_token(oauth).api_key(api_key);
571
572 assert_eq!(builder.oauth_token, Some(oauth.to_string()));
573 assert_eq!(builder.api_key, Some(api_key.to_string()));
574 }
575}