browser_automation_cli/doctor/
mod.rs1use serde_json::json;
4
5use crate::envelope::print_success_json;
6use crate::install;
7use crate::native::cdp::chrome;
8
9#[derive(Default, Clone, Copy)]
11pub struct DoctorOptions {
12 pub offline: bool,
14 pub quick: bool,
16 pub fix: bool,
18 pub json: bool,
20}
21
22pub 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 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}