1use std::{
2 collections::BTreeMap,
3 future::Future,
4 path::PathBuf,
5 pin::Pin,
6 sync::Arc,
7 task::{Context, Poll},
8 time::Duration,
9};
10
11use futures_core::Stream;
12use tokio::{
13 io::{AsyncBufReadExt, AsyncRead, AsyncReadExt, AsyncWriteExt, BufReader},
14 process::Command,
15 sync::{mpsc, oneshot},
16 time,
17};
18
19use crate::{
20 builder::ClaudeClientBuilder,
21 commands::command::ClaudeCommandRequest,
22 commands::doctor::ClaudeDoctorRequest,
23 commands::mcp::{
24 McpAddFromClaudeDesktopRequest, McpAddJsonRequest, McpAddRequest, McpGetRequest,
25 McpRemoveRequest, McpServeRequest,
26 },
27 commands::plugin::{
28 PluginDisableRequest, PluginEnableRequest, PluginInstallRequest, PluginListRequest,
29 PluginManifestMarketplaceRequest, PluginManifestRequest, PluginMarketplaceAddRequest,
30 PluginMarketplaceListRequest, PluginMarketplaceRemoveRequest, PluginMarketplaceRepoRequest,
31 PluginMarketplaceRequest, PluginMarketplaceUpdateRequest, PluginRequest,
32 PluginUninstallRequest, PluginUpdateRequest, PluginValidateRequest,
33 },
34 commands::print::{ClaudeOutputFormat, ClaudePrintRequest},
35 commands::update::ClaudeUpdateRequest,
36 home::{ClaudeHomeLayout, ClaudeHomeSeedRequest},
37 parse_stream_json_lines, process, ClaudeCodeError, ClaudePrintStreamJsonControlHandle,
38 ClaudePrintStreamJsonHandle, ClaudeStreamJsonEvent, ClaudeStreamJsonParseError,
39 ClaudeStreamJsonParser, ClaudeTerminationHandle, CommandOutput, DynClaudeStreamJsonCompletion,
40 DynClaudeStreamJsonEventStream, StreamJsonLineOutcome,
41};
42
43mod setup_token;
44
45pub use setup_token::ClaudeSetupTokenSession;
46
47#[derive(Debug, Clone)]
48pub struct ClaudeClient {
49 pub(crate) binary: Option<PathBuf>,
50 pub(crate) working_dir: Option<PathBuf>,
51 pub(crate) env: BTreeMap<String, String>,
52 pub(crate) claude_home: Option<ClaudeHomeLayout>,
53 pub(crate) create_home_dirs: bool,
54 pub(crate) home_seed: Option<ClaudeHomeSeedRequest>,
55 pub(crate) home_materialize_status: Arc<std::sync::OnceLock<Result<(), String>>>,
56 pub(crate) home_seed_status: Arc<std::sync::OnceLock<Result<(), String>>>,
57 pub(crate) timeout: Option<Duration>,
58 pub(crate) mirror_stdout: bool,
59 pub(crate) mirror_stderr: bool,
60}
61
62impl ClaudeClient {
63 pub fn builder() -> ClaudeClientBuilder {
64 ClaudeClientBuilder::default()
65 }
66
67 pub async fn help(&self) -> Result<CommandOutput, ClaudeCodeError> {
68 self.run_command(ClaudeCommandRequest::root().arg("--help"))
69 .await
70 }
71
72 pub async fn version(&self) -> Result<CommandOutput, ClaudeCodeError> {
73 self.run_command(ClaudeCommandRequest::root().arg("--version"))
74 .await
75 }
76
77 pub async fn run_command(
78 &self,
79 request: ClaudeCommandRequest,
80 ) -> Result<CommandOutput, ClaudeCodeError> {
81 self.ensure_home_prepared()?;
82 let binary = self.resolve_binary();
83 let mut cmd = Command::new(&binary);
84 cmd.args(request.argv());
85
86 if let Some(dir) = self.working_dir.as_ref() {
87 cmd.current_dir(dir);
88 }
89
90 process::apply_env(&mut cmd, &self.env);
91
92 let timeout = request.timeout.or(self.timeout);
93 process::run_command(
94 cmd,
95 &binary,
96 request.stdin.as_deref(),
97 timeout,
98 self.mirror_stdout,
99 self.mirror_stderr,
100 )
101 .await
102 }
103
104 pub async fn print(
105 &self,
106 request: ClaudePrintRequest,
107 ) -> Result<ClaudePrintResult, ClaudeCodeError> {
108 let allow_missing_prompt = request.stdin.is_some()
109 || request.continue_session
110 || request.resume
111 || request.resume_value.is_some()
112 || request.from_pr
113 || request.from_pr_value.is_some();
114 if request.prompt.is_none() && !allow_missing_prompt {
115 return Err(ClaudeCodeError::InvalidRequest(
116 "either prompt, stdin_bytes, or a continuation flag must be provided".to_string(),
117 ));
118 }
119
120 self.ensure_home_prepared()?;
121 let binary = self.resolve_binary();
122 let mut cmd = Command::new(&binary);
123 cmd.args(request.argv());
124
125 if let Some(dir) = self.working_dir.as_ref() {
126 cmd.current_dir(dir);
127 }
128
129 process::apply_env(&mut cmd, &self.env);
130
131 let timeout = request.timeout.or(self.timeout);
132 let output = process::run_command(
133 cmd,
134 &binary,
135 request.stdin.as_deref(),
136 timeout,
137 self.mirror_stdout,
138 self.mirror_stderr,
139 )
140 .await?;
141
142 let parsed = match request.output_format {
143 ClaudeOutputFormat::Json => {
144 let v = serde_json::from_slice(&output.stdout)?;
145 Some(ClaudeParsedOutput::Json(v))
146 }
147 ClaudeOutputFormat::StreamJson => {
148 let s = String::from_utf8_lossy(&output.stdout);
149 Some(ClaudeParsedOutput::StreamJson(parse_stream_json_lines(&s)))
150 }
151 ClaudeOutputFormat::Text => None,
152 };
153
154 Ok(ClaudePrintResult { output, parsed })
155 }
156
157 pub fn print_stream_json(
158 &self,
159 request: ClaudePrintRequest,
160 ) -> Pin<
161 Box<dyn Future<Output = Result<ClaudePrintStreamJsonHandle, ClaudeCodeError>> + Send + '_>,
162 > {
163 Box::pin(async move {
164 let (events, completion, _termination) = self.spawn_print_stream_json(request).await?;
165 Ok(ClaudePrintStreamJsonHandle { events, completion })
166 })
167 }
168
169 pub fn print_stream_json_control(
170 &self,
171 request: ClaudePrintRequest,
172 ) -> Pin<
173 Box<
174 dyn Future<Output = Result<ClaudePrintStreamJsonControlHandle, ClaudeCodeError>>
175 + Send
176 + '_,
177 >,
178 > {
179 Box::pin(async move {
180 let (events, completion, termination) = self.spawn_print_stream_json(request).await?;
181 Ok(ClaudePrintStreamJsonControlHandle {
182 events,
183 completion,
184 termination,
185 })
186 })
187 }
188
189 async fn spawn_print_stream_json(
190 &self,
191 request: ClaudePrintRequest,
192 ) -> Result<
193 (
194 DynClaudeStreamJsonEventStream,
195 DynClaudeStreamJsonCompletion,
196 ClaudeTerminationHandle,
197 ),
198 ClaudeCodeError,
199 > {
200 let allow_missing_prompt = request.stdin.is_some()
201 || request.continue_session
202 || request.resume
203 || request.resume_value.is_some()
204 || request.from_pr
205 || request.from_pr_value.is_some();
206 if request.prompt.is_none() && !allow_missing_prompt {
207 return Err(ClaudeCodeError::InvalidRequest(
208 "either prompt, stdin_bytes, or a continuation flag must be provided".to_string(),
209 ));
210 }
211
212 self.ensure_home_prepared()?;
213 let binary = self.resolve_binary();
214
215 let mut request = request;
216 request.output_format = ClaudeOutputFormat::StreamJson;
217 let stdin_bytes = request.stdin.take();
218 let mirror_stdout = self.mirror_stdout;
219 let mirror_stderr = self.mirror_stderr;
220 let timeout = request.timeout.or(self.timeout);
221
222 let mut cmd = Command::new(&binary);
223 cmd.args(request.argv());
224
225 if let Some(dir) = self.working_dir.as_ref() {
226 cmd.current_dir(dir);
227 }
228
229 process::apply_env(&mut cmd, &self.env);
230
231 cmd.kill_on_drop(true);
232 cmd.stdin(if stdin_bytes.is_some() {
233 std::process::Stdio::piped()
234 } else {
235 std::process::Stdio::null()
236 });
237 cmd.stdout(std::process::Stdio::piped());
238 cmd.stderr(if mirror_stderr {
239 std::process::Stdio::piped()
240 } else {
241 std::process::Stdio::null()
242 });
243
244 let mut child = process::spawn_with_retry(&mut cmd, &binary)?;
245
246 if let Some(bytes) = stdin_bytes {
247 if let Some(mut stdin) = child.stdin.take() {
248 stdin
249 .write_all(&bytes)
250 .await
251 .map_err(ClaudeCodeError::StdinWrite)?;
252 }
253 }
254
255 let stdout = child.stdout.take().ok_or(ClaudeCodeError::MissingStdout)?;
256 let stderr = if mirror_stderr {
257 Some(child.stderr.take().ok_or(ClaudeCodeError::MissingStderr)?)
258 } else {
259 None
260 };
261
262 let termination = ClaudeTerminationHandle::new();
263 let termination_for_runner = termination.clone();
264
265 let (events_tx, events_rx) = mpsc::channel(32);
266 let (completion_tx, completion_rx) = oneshot::channel();
267
268 tokio::spawn(async move {
269 let res = run_print_stream_json_child(
270 child,
271 stdout,
272 stderr,
273 events_tx,
274 mirror_stdout,
275 timeout,
276 termination_for_runner,
277 )
278 .await;
279 let _ = completion_tx.send(res);
280 });
281
282 let events: DynClaudeStreamJsonEventStream =
283 Box::pin(ClaudeStreamJsonEventChannelStream::new(events_rx));
284
285 let completion: DynClaudeStreamJsonCompletion = Box::pin(async move {
286 completion_rx
287 .await
288 .map_err(|_| ClaudeCodeError::Join("stream-json task dropped".to_string()))?
289 });
290
291 Ok((events, completion, termination))
292 }
293
294 pub async fn mcp_list(&self) -> Result<CommandOutput, ClaudeCodeError> {
295 self.run_command(ClaudeCommandRequest::new(["mcp", "list"]))
296 .await
297 }
298
299 pub async fn mcp_reset_project_choices(&self) -> Result<CommandOutput, ClaudeCodeError> {
300 self.run_command(ClaudeCommandRequest::new(["mcp", "reset-project-choices"]))
301 .await
302 }
303
304 pub async fn mcp_get(&self, req: McpGetRequest) -> Result<CommandOutput, ClaudeCodeError> {
305 self.run_command(req.into_command()).await
306 }
307
308 pub async fn mcp_add(&self, req: McpAddRequest) -> Result<CommandOutput, ClaudeCodeError> {
309 self.run_command(req.into_command()).await
310 }
311
312 pub async fn mcp_remove(
313 &self,
314 req: McpRemoveRequest,
315 ) -> Result<CommandOutput, ClaudeCodeError> {
316 self.run_command(req.into_command()).await
317 }
318
319 pub async fn mcp_add_json(
320 &self,
321 req: McpAddJsonRequest,
322 ) -> Result<CommandOutput, ClaudeCodeError> {
323 self.run_command(req.into_command()).await
324 }
325
326 pub async fn mcp_serve(&self, req: McpServeRequest) -> Result<CommandOutput, ClaudeCodeError> {
327 self.run_command(req.into_command()).await
328 }
329
330 pub async fn mcp_add_from_claude_desktop(
331 &self,
332 req: McpAddFromClaudeDesktopRequest,
333 ) -> Result<CommandOutput, ClaudeCodeError> {
334 self.run_command(req.into_command()).await
335 }
336
337 pub async fn doctor(&self) -> Result<CommandOutput, ClaudeCodeError> {
338 self.doctor_with(ClaudeDoctorRequest::new()).await
339 }
340
341 pub async fn doctor_with(
342 &self,
343 req: ClaudeDoctorRequest,
344 ) -> Result<CommandOutput, ClaudeCodeError> {
345 self.run_command(req.into_command()).await
346 }
347
348 pub async fn plugin_list(
349 &self,
350 req: PluginListRequest,
351 ) -> Result<CommandOutput, ClaudeCodeError> {
352 self.run_command(req.into_command()).await
353 }
354
355 pub async fn plugin(&self, req: PluginRequest) -> Result<CommandOutput, ClaudeCodeError> {
356 self.run_command(req.into_command()).await
357 }
358
359 pub async fn plugin_enable(
360 &self,
361 req: PluginEnableRequest,
362 ) -> Result<CommandOutput, ClaudeCodeError> {
363 self.run_command(req.into_command()).await
364 }
365
366 pub async fn plugin_disable(
367 &self,
368 req: PluginDisableRequest,
369 ) -> Result<CommandOutput, ClaudeCodeError> {
370 self.run_command(req.into_command()).await
371 }
372
373 pub async fn plugin_install(
374 &self,
375 req: PluginInstallRequest,
376 ) -> Result<CommandOutput, ClaudeCodeError> {
377 self.run_command(req.into_command()).await
378 }
379
380 pub async fn plugin_uninstall(
381 &self,
382 req: PluginUninstallRequest,
383 ) -> Result<CommandOutput, ClaudeCodeError> {
384 self.run_command(req.into_command()).await
385 }
386
387 pub async fn plugin_update(
388 &self,
389 req: PluginUpdateRequest,
390 ) -> Result<CommandOutput, ClaudeCodeError> {
391 self.run_command(req.into_command()).await
392 }
393
394 pub async fn plugin_validate(
395 &self,
396 req: PluginValidateRequest,
397 ) -> Result<CommandOutput, ClaudeCodeError> {
398 self.run_command(req.into_command()).await
399 }
400
401 pub async fn plugin_manifest(
402 &self,
403 req: PluginManifestRequest,
404 ) -> Result<CommandOutput, ClaudeCodeError> {
405 self.run_command(req.into_command()).await
406 }
407
408 pub async fn plugin_manifest_marketplace(
409 &self,
410 req: PluginManifestMarketplaceRequest,
411 ) -> Result<CommandOutput, ClaudeCodeError> {
412 self.run_command(req.into_command()).await
413 }
414
415 pub async fn plugin_marketplace_repo(
416 &self,
417 req: PluginMarketplaceRepoRequest,
418 ) -> Result<CommandOutput, ClaudeCodeError> {
419 self.run_command(req.into_command()).await
420 }
421
422 pub async fn plugin_marketplace(
423 &self,
424 req: PluginMarketplaceRequest,
425 ) -> Result<CommandOutput, ClaudeCodeError> {
426 self.run_command(req.into_command()).await
427 }
428
429 pub async fn plugin_marketplace_add(
430 &self,
431 req: PluginMarketplaceAddRequest,
432 ) -> Result<CommandOutput, ClaudeCodeError> {
433 self.run_command(req.into_command()).await
434 }
435
436 pub async fn plugin_marketplace_list(
437 &self,
438 req: PluginMarketplaceListRequest,
439 ) -> Result<CommandOutput, ClaudeCodeError> {
440 self.run_command(req.into_command()).await
441 }
442
443 pub async fn plugin_marketplace_remove(
444 &self,
445 req: PluginMarketplaceRemoveRequest,
446 ) -> Result<CommandOutput, ClaudeCodeError> {
447 self.run_command(req.into_command()).await
448 }
449
450 pub async fn plugin_marketplace_update(
451 &self,
452 req: PluginMarketplaceUpdateRequest,
453 ) -> Result<CommandOutput, ClaudeCodeError> {
454 self.run_command(req.into_command()).await
455 }
456
457 pub async fn update(&self) -> Result<CommandOutput, ClaudeCodeError> {
458 self.update_with(ClaudeUpdateRequest::new()).await
459 }
460
461 pub async fn update_with(
462 &self,
463 req: ClaudeUpdateRequest,
464 ) -> Result<CommandOutput, ClaudeCodeError> {
465 self.run_command(req.into_command()).await
466 }
467
468 pub fn claude_home_layout(&self) -> Option<ClaudeHomeLayout> {
469 self.claude_home.clone()
470 }
471
472 fn resolve_binary(&self) -> PathBuf {
473 if let Some(b) = self.binary.as_ref() {
474 return b.clone();
475 }
476 if let Ok(v) = std::env::var("CLAUDE_BINARY") {
477 if !v.trim().is_empty() {
478 return PathBuf::from(v);
479 }
480 }
481 PathBuf::from("claude")
482 }
483
484 fn ensure_home_prepared(&self) -> Result<(), ClaudeCodeError> {
485 if self.claude_home.is_none() {
486 return Ok(());
487 }
488
489 let materialize = self.home_materialize_status.get_or_init(|| {
490 let Some(layout) = self.claude_home.as_ref() else {
491 return Ok(());
492 };
493 layout
494 .materialize(self.create_home_dirs)
495 .map_err(|e| e.to_string())
496 });
497 if let Err(msg) = materialize {
498 return Err(ClaudeCodeError::ClaudeHomePrepareFailed(msg.clone()));
499 }
500
501 let seeded = self.home_seed_status.get_or_init(|| {
502 let Some(layout) = self.claude_home.as_ref() else {
503 return Ok(());
504 };
505 let Some(seed_req) = self.home_seed.as_ref() else {
506 return Ok(());
507 };
508 let _ = layout.materialize(true);
510 layout
511 .seed_from_user_home(&seed_req.seed_user_home, seed_req.level)
512 .map(|_| ())
513 .map_err(|e| e.to_string())
514 });
515 if let Err(msg) = seeded {
516 return Err(ClaudeCodeError::ClaudeHomeSeedFailed(msg.clone()));
517 }
518
519 Ok(())
520 }
521}
522
523struct ClaudeStreamJsonEventChannelStream {
524 rx: mpsc::Receiver<Result<ClaudeStreamJsonEvent, ClaudeStreamJsonParseError>>,
525}
526
527impl ClaudeStreamJsonEventChannelStream {
528 fn new(rx: mpsc::Receiver<Result<ClaudeStreamJsonEvent, ClaudeStreamJsonParseError>>) -> Self {
529 Self { rx }
530 }
531}
532
533impl Stream for ClaudeStreamJsonEventChannelStream {
534 type Item = Result<ClaudeStreamJsonEvent, ClaudeStreamJsonParseError>;
535
536 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
537 let this = self.get_mut();
538 this.rx.poll_recv(cx)
539 }
540}
541
542#[derive(Debug, Clone, Copy)]
543enum ChildExit {
544 Exited(std::process::ExitStatus),
545 TimedOut,
546}
547
548async fn mirror_child_stream_to_parent_stderr<R>(mut reader: R) -> Result<(), std::io::Error>
549where
550 R: AsyncRead + Unpin,
551{
552 let mut out = tokio::io::stderr();
553 let mut chunk = [0u8; 4096];
554 loop {
555 let n = reader.read(&mut chunk).await?;
556 if n == 0 {
557 break;
558 }
559 out.write_all(&chunk[..n]).await?;
560 out.flush().await?;
561 }
562 Ok(())
563}
564
565async fn run_print_stream_json_child(
566 mut child: tokio::process::Child,
567 stdout: tokio::process::ChildStdout,
568 stderr: Option<tokio::process::ChildStderr>,
569 events_tx: mpsc::Sender<Result<ClaudeStreamJsonEvent, ClaudeStreamJsonParseError>>,
570 mirror_stdout: bool,
571 timeout: Option<Duration>,
572 termination: ClaudeTerminationHandle,
573) -> Result<std::process::ExitStatus, ClaudeCodeError> {
574 let mut parser = ClaudeStreamJsonParser::new();
575 let mut lines = BufReader::new(stdout).lines();
576 let mut stdout_mirror = mirror_stdout.then(tokio::io::stdout);
577
578 let stderr_task =
579 stderr.map(|stderr| tokio::spawn(mirror_child_stream_to_parent_stderr(stderr)));
580
581 let started = time::Instant::now();
582 let deadline = timeout.map(|dur| started + dur);
583 let mut timeout_sleep: Option<Pin<Box<time::Sleep>>> =
584 deadline.map(|deadline| Box::pin(time::sleep_until(deadline)));
585
586 let mut timed_out = false;
587 let mut cancelled = false;
588 let mut io_error: Option<ClaudeCodeError> = None;
589
590 let closed_tx = events_tx.clone();
591
592 loop {
593 let next = tokio::select! {
594 _ = closed_tx.closed() => {
595 cancelled = true;
596 break;
597 }
598 _ = termination.requested() => {
599 cancelled = true;
600 break;
601 }
602 _ = async {
603 if let Some(sleep) = timeout_sleep.as_mut() {
604 sleep.as_mut().await;
605 } else {
606 std::future::pending::<()>().await;
607 }
608 } => {
609 timed_out = timeout.is_some();
610 break;
611 }
612 res = lines.next_line() => res,
613 };
614
615 let line = match next {
616 Ok(Some(line)) => line,
617 Ok(None) => break,
618 Err(err) => {
619 io_error = Some(ClaudeCodeError::StdoutRead(err));
620 break;
621 }
622 };
623
624 if line.trim().is_empty() {
625 continue;
626 }
627
628 if let Some(out) = stdout_mirror.as_mut() {
629 let res: Result<(), std::io::Error> = async {
630 use tokio::io::AsyncWriteExt as _;
631 out.write_all(line.as_bytes()).await?;
632 out.write_all(b"\n").await?;
633 out.flush().await
634 }
635 .await;
636
637 if let Err(err) = res {
638 io_error = Some(ClaudeCodeError::StdoutRead(err));
639 break;
640 }
641 }
642
643 let outcome = match parser.parse_line(&line) {
644 Ok(Some(event)) => Some(Ok(event)),
645 Ok(None) => None,
646 Err(err) => Some(Err(err)),
647 };
648 let Some(outcome) = outcome else {
649 continue;
650 };
651
652 let send_fut = events_tx.send(outcome);
653 tokio::select! {
654 _ = closed_tx.closed() => {
655 cancelled = true;
656 break;
657 }
658 _ = termination.requested() => {
659 cancelled = true;
660 break;
661 }
662 _ = async {
663 if let Some(sleep) = timeout_sleep.as_mut() {
664 sleep.as_mut().await;
665 } else {
666 std::future::pending::<()>().await;
667 }
668 } => {
669 timed_out = timeout.is_some();
670 break;
671 }
672 res = send_fut => {
673 if res.is_err() {
674 cancelled = true;
675 break;
676 }
677 }
678 }
679 }
680
681 drop(events_tx);
684 drop(closed_tx);
685 drop(lines);
686
687 if cancelled || io_error.is_some() {
690 let _ = child.start_kill();
691 }
692
693 let status: Result<std::process::ExitStatus, ClaudeCodeError> = match io_error {
694 Some(err) => {
695 let _ = child.wait().await;
696 Err(err)
697 }
698 None if cancelled => child.wait().await.map_err(ClaudeCodeError::Wait),
699 None if timed_out => match wait_for_child_exit(&mut child, timeout, deadline).await? {
700 ChildExit::Exited(status) => Ok(status),
701 ChildExit::TimedOut => Err(ClaudeCodeError::Timeout {
702 timeout: timeout.expect("timed_out implies timeout"),
703 }),
704 },
705 None => match wait_for_child_exit(&mut child, timeout, deadline).await? {
706 ChildExit::Exited(status) => Ok(status),
707 ChildExit::TimedOut => Err(ClaudeCodeError::Timeout {
708 timeout: timeout.expect("deadline implies timeout"),
709 }),
710 },
711 };
712
713 if let Some(task) = stderr_task {
714 match task.await {
715 Ok(Ok(())) => {}
716 Ok(Err(err)) => {
717 if status.is_ok() {
718 return Err(ClaudeCodeError::StderrRead(err));
719 }
720 }
721 Err(err) => {
722 if status.is_ok() {
723 return Err(ClaudeCodeError::Join(err.to_string()));
724 }
725 }
726 }
727 }
728
729 status
730}
731
732async fn wait_for_child_exit(
733 child: &mut tokio::process::Child,
734 timeout: Option<Duration>,
735 deadline: Option<time::Instant>,
736) -> Result<ChildExit, ClaudeCodeError> {
737 match deadline {
738 None => child
739 .wait()
740 .await
741 .map(ChildExit::Exited)
742 .map_err(ClaudeCodeError::Wait),
743 Some(deadline) => {
744 let remaining = deadline.saturating_duration_since(time::Instant::now());
745 if remaining.is_zero() {
746 match child.try_wait().map_err(ClaudeCodeError::Wait)? {
747 Some(status) => Ok(ChildExit::Exited(status)),
748 None => {
749 timeout.expect("deadline implies timeout");
750 let _ = child.start_kill();
751 match child.wait().await.map_err(ClaudeCodeError::Wait) {
752 Ok(_status) => Ok(ChildExit::TimedOut),
753 Err(err) => Err(err),
754 }
755 }
756 }
757 } else {
758 match time::timeout(remaining, child.wait()).await {
759 Ok(res) => res.map(ChildExit::Exited).map_err(ClaudeCodeError::Wait),
760 Err(_) => match child.try_wait().map_err(ClaudeCodeError::Wait)? {
761 Some(status) => Ok(ChildExit::Exited(status)),
762 None => {
763 timeout.expect("deadline implies timeout");
764 let _ = child.start_kill();
765 match child.wait().await.map_err(ClaudeCodeError::Wait) {
766 Ok(_status) => Ok(ChildExit::TimedOut),
767 Err(err) => Err(err),
768 }
769 }
770 },
771 }
772 }
773 }
774 }
775}
776
777#[derive(Debug, Clone)]
778pub struct ClaudePrintResult {
779 pub output: CommandOutput,
780 pub parsed: Option<ClaudeParsedOutput>,
781}
782
783#[derive(Debug, Clone)]
784pub enum ClaudeParsedOutput {
785 Json(serde_json::Value),
786 StreamJson(Vec<StreamJsonLineOutcome>),
787}
788
789#[cfg(test)]
790mod tests;