Skip to main content

browser_automation_cli/doctor/
mod.rs

1//! Local diagnostics for one-shot installs (no multi-process daemon).
2
3use serde_json::json;
4
5use crate::envelope::print_success_json;
6use crate::install;
7use crate::native::cdp::chrome;
8
9/// Options for local install diagnostics.
10#[derive(Default, Clone, Copy)]
11pub struct DoctorOptions {
12    /// Skip network checks.
13    pub offline: bool,
14    /// Run a reduced check set.
15    pub quick: bool,
16    /// Attempt automatic remediations when supported.
17    pub fix: bool,
18    /// Emit JSON envelope on stdout.
19    pub json: bool,
20}
21
22/// Run doctor checks and return a process exit code (`0` = all pass).
23pub fn run_doctor(opts: DoctorOptions) -> i32 {
24    let mut checks = Vec::new();
25    let mut failed = false;
26
27    let chrome = chrome::find_chrome();
28    match &chrome {
29        Some(p) => checks.push(
30            json!({"id":"chrome","status":"pass","message": format!("found {}", p.display())}),
31        ),
32        None => {
33            failed = true;
34            checks.push(json!({
35                "id":"chrome",
36                "status":"fail",
37                "message":"Chrome/Chromium not found on PATH or cache",
38                "fix":"install Chromium or set executable path"
39            }));
40        }
41    }
42
43    let cache = install::get_browsers_dir();
44    checks.push(json!({
45        "id":"browsers_dir",
46        "status":"info",
47        "message": format!("cache dir {}", cache.display())
48    }));
49
50    if !opts.quick && chrome.is_some() {
51        match crate::browser::block_on_browser(async {
52            let mut s = crate::browser::OneShotSession::launch_headless().await?;
53            let _ = s
54                .goto("about:blank", crate::robots::RobotsPolicy::Ignore)
55                .await?;
56            let _ = s.shutdown().await;
57            Ok::<_, crate::error::CliError>(())
58        }) {
59            Ok(()) => checks
60                .push(json!({"id":"launch","status":"pass","message":"headless about:blank ok"})),
61            Err(e) => {
62                failed = true;
63                checks.push(json!({"id":"launch","status":"fail","message": e.message()}));
64            }
65        }
66    }
67
68    // Lighthouse binary presence (external audit tool).
69    let lighthouse_present = which_bin("lighthouse");
70    match &lighthouse_present {
71        Some(p) => checks.push(json!({
72            "id": "lighthouse",
73            "status": "pass",
74            "message": format!("found {p}"),
75            "lighthouse_present": true,
76        })),
77        None => checks.push(json!({
78            "id": "lighthouse",
79            "status": "info",
80            "message": "lighthouse not on PATH (optional; use --lighthouse-path)",
81            "lighthouse_present": false,
82            "fix": "npm i -g lighthouse",
83        })),
84    }
85
86    let ffmpeg_present = which_bin("ffmpeg");
87    checks.push(json!({
88        "id": "ffmpeg",
89        "status": if ffmpeg_present.is_some() { "pass" } else { "info" },
90        "message": ffmpeg_present
91            .as_deref()
92            .map(|p| format!("found {p}"))
93            .unwrap_or_else(|| "ffmpeg not on PATH (optional for media pipelines)".into()),
94        "ffmpeg_present": ffmpeg_present.is_some(),
95    }));
96
97    let _ = opts.offline;
98    let _ = opts.fix;
99
100    let data = json!({
101        "schema_version": 1,
102        "checks": checks,
103        "lighthouse_present": lighthouse_present.is_some(),
104        "ffmpeg_present": ffmpeg_present.is_some(),
105        "ok": !failed,
106    });
107
108    if opts.json {
109        let _ = print_success_json(data);
110    } else {
111        for c in checks {
112            println!(
113                "[{}] {} — {}",
114                c.get("status").and_then(|s| s.as_str()).unwrap_or("?"),
115                c.get("id").and_then(|s| s.as_str()).unwrap_or("?"),
116                c.get("message").and_then(|s| s.as_str()).unwrap_or("")
117            );
118        }
119    }
120
121    if failed {
122        1
123    } else {
124        0
125    }
126}
127
128fn which_bin(name: &str) -> Option<String> {
129    std::env::var_os("PATH").and_then(|paths| {
130        for dir in std::env::split_paths(&paths) {
131            let candidate = dir.join(name);
132            if candidate.is_file() {
133                return Some(candidate.display().to_string());
134            }
135            #[cfg(windows)]
136            {
137                let with_exe = dir.join(format!("{name}.exe"));
138                if with_exe.is_file() {
139                    return Some(with_exe.display().to_string());
140                }
141            }
142        }
143        None
144    })
145}