Skip to main content

codex_wrapper/command/
doctor.rs

1//! Diagnose the local Codex installation (`codex doctor`).
2
3use crate::Codex;
4use crate::command::CodexCommand;
5use crate::error::Result;
6use crate::exec::{self, CommandOutput};
7
8/// Diagnose local Codex installation, config, auth, and runtime health.
9///
10/// Wraps `codex doctor`. Use [`json`](DoctorCommand::json) for a redacted
11/// machine-readable report on stdout.
12#[derive(Debug, Clone)]
13pub struct DoctorCommand {
14    json: bool,
15    summary: bool,
16    all: bool,
17    no_color: bool,
18    ascii: bool,
19    config_overrides: Vec<String>,
20    enabled_features: Vec<String>,
21    disabled_features: Vec<String>,
22    retry_policy: Option<crate::retry::RetryPolicy>,
23}
24
25impl DoctorCommand {
26    /// Create a new doctor command.
27    #[must_use]
28    pub fn new() -> Self {
29        Self {
30            json: false,
31            summary: false,
32            all: false,
33            no_color: false,
34            ascii: false,
35            config_overrides: Vec::new(),
36            enabled_features: Vec::new(),
37            disabled_features: Vec::new(),
38            retry_policy: None,
39        }
40    }
41
42    /// Emit a redacted machine-readable report (`--json`).
43    #[must_use]
44    pub fn json(mut self) -> Self {
45        self.json = true;
46        self
47    }
48
49    /// Only show grouped check rows and the final count summary (`--summary`).
50    #[must_use]
51    pub fn summary(mut self) -> Self {
52        self.summary = true;
53        self
54    }
55
56    /// Expand long lists in detailed human output (`--all`).
57    #[must_use]
58    pub fn all(mut self) -> Self {
59        self.all = true;
60        self
61    }
62
63    /// Disable ANSI color in human output (`--no-color`).
64    #[must_use]
65    pub fn no_color(mut self) -> Self {
66        self.no_color = true;
67        self
68    }
69
70    /// Use ASCII status labels and separators in human output (`--ascii`).
71    #[must_use]
72    pub fn ascii(mut self) -> Self {
73        self.ascii = true;
74        self
75    }
76
77    /// Override a config key (`-c key=value`). May be called multiple times.
78    #[must_use]
79    pub fn config(mut self, key_value: impl Into<String>) -> Self {
80        self.config_overrides.push(key_value.into());
81        self
82    }
83
84    /// Enable an optional feature flag (`--enable <feature>`).
85    #[must_use]
86    pub fn enable(mut self, feature: impl Into<String>) -> Self {
87        self.enabled_features.push(feature.into());
88        self
89    }
90
91    /// Disable an optional feature flag (`--disable <feature>`).
92    #[must_use]
93    pub fn disable(mut self, feature: impl Into<String>) -> Self {
94        self.disabled_features.push(feature.into());
95        self
96    }
97
98    /// Override the retry policy for this command.
99    #[must_use]
100    pub fn retry(mut self, policy: crate::retry::RetryPolicy) -> Self {
101        self.retry_policy = Some(policy);
102        self
103    }
104}
105
106impl Default for DoctorCommand {
107    fn default() -> Self {
108        Self::new()
109    }
110}
111
112impl CodexCommand for DoctorCommand {
113    type Output = CommandOutput;
114
115    fn args(&self) -> Vec<String> {
116        let mut args = vec!["doctor".to_string()];
117        for value in &self.config_overrides {
118            args.push("-c".into());
119            args.push(value.clone());
120        }
121        for value in &self.enabled_features {
122            args.push("--enable".into());
123            args.push(value.clone());
124        }
125        for value in &self.disabled_features {
126            args.push("--disable".into());
127            args.push(value.clone());
128        }
129        if self.json {
130            args.push("--json".into());
131        }
132        if self.summary {
133            args.push("--summary".into());
134        }
135        if self.all {
136            args.push("--all".into());
137        }
138        if self.no_color {
139            args.push("--no-color".into());
140        }
141        if self.ascii {
142            args.push("--ascii".into());
143        }
144        args
145    }
146
147    async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
148        exec::run_codex_with_retry(codex, self.args(), self.retry_policy.as_ref()).await
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn doctor_args_default() {
158        let args = DoctorCommand::new().args();
159        assert_eq!(args, vec!["doctor"]);
160    }
161
162    #[test]
163    fn doctor_args_flags() {
164        let args = DoctorCommand::new().json().summary().all().args();
165        assert_eq!(args, vec!["doctor", "--json", "--summary", "--all"]);
166    }
167
168    #[test]
169    fn doctor_args_config_and_output() {
170        let args = DoctorCommand::new()
171            .config("model=o3")
172            .no_color()
173            .ascii()
174            .args();
175        assert_eq!(
176            args,
177            vec!["doctor", "-c", "model=o3", "--no-color", "--ascii"]
178        );
179    }
180}