Skip to main content

claude_code/client/
mod.rs

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            // Seeding implies directories must exist even when the caller disabled auto-creation.
509            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    // Close the event channel as soon as we stop producing events so downstream consumers can
682    // observe stream finality independent of process-exit cleanup.
683    drop(events_tx);
684    drop(closed_tx);
685    drop(lines);
686
687    // `start_kill` may leave a zombie until `wait()` reaps the process (Tokio docs). For Agent API
688    // DR-0012 / CA-C02 invariants, completion must not resolve before backend process exit.
689    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;