use serde_json::json;
use crate::envelope::print_success_json;
use crate::install;
use crate::native::cdp::chrome;
#[derive(Default, Clone, Copy)]
pub struct DoctorOptions {
pub offline: bool,
pub quick: bool,
pub fix: bool,
pub json: bool,
}
pub fn run_doctor(opts: DoctorOptions) -> i32 {
let mut checks = Vec::new();
let mut failed = false;
let chrome = chrome::find_chrome();
match &chrome {
Some(p) => checks.push(
json!({"id":"chrome","status":"pass","message": format!("found {}", p.display())}),
),
None => {
failed = true;
checks.push(json!({
"id":"chrome",
"status":"fail",
"message":"Chrome/Chromium not found on PATH or cache",
"fix":"install Chromium or set executable path"
}));
}
}
let cache = install::get_browsers_dir();
checks.push(json!({
"id":"browsers_dir",
"status":"info",
"message": format!("cache dir {}", cache.display())
}));
if !opts.quick && chrome.is_some() {
match crate::browser::block_on_browser(async {
let mut s = crate::browser::OneShotSession::launch_headless().await?;
let _ = s
.goto("about:blank", crate::robots::RobotsPolicy::Ignore)
.await?;
let _ = s.shutdown().await;
Ok::<_, crate::error::CliError>(())
}) {
Ok(()) => checks
.push(json!({"id":"launch","status":"pass","message":"headless about:blank ok"})),
Err(e) => {
failed = true;
checks.push(json!({"id":"launch","status":"fail","message": e.message()}));
}
}
}
let lh_resolved = resolve_lighthouse_for_doctor();
let lighthouse_present = lh_resolved.is_some();
match &lh_resolved {
Some((path, source)) => checks.push(json!({
"id": "lighthouse",
"status": "pass",
"message": format!("found {path} (source={source})"),
"lighthouse_present": true,
"lighthouse_resolved": path,
"lighthouse_source": source,
})),
None => {
let mut fix_msg = None;
if opts.fix {
fix_msg = Some(
"browser-automation-cli config set lighthouse_path /absolute/path/to/lighthouse",
);
}
let xdg_path = crate::xdg::lighthouse_path_from_config().filter(|s| !s.is_empty());
let mut entry = json!({
"id": "lighthouse",
"status": "info",
"message": "lighthouse not on PATH or XDG (optional external binary; e2e may use mock-lighthouse.sh)",
"lighthouse_present": false,
"lighthouse_resolved": null,
"lighthouse_source": "missing",
"lighthouse_path_xdg": xdg_path,
"suggestion": "browser-automation-cli config set lighthouse_path /absolute/path/to/lighthouse",
});
if let Some(fix) = fix_msg {
entry
.as_object_mut()
.unwrap()
.insert("fix".into(), json!(fix));
}
checks.push(entry);
}
}
checks.push(cache_redis_check());
let ffmpeg_present = which_bin("ffmpeg");
checks.push(json!({
"id": "ffmpeg",
"status": if ffmpeg_present.is_some() { "pass" } else { "info" },
"message": ffmpeg_present
.as_deref()
.map(|p| format!("found {p}"))
.unwrap_or_else(|| "ffmpeg not on PATH (optional for media pipelines)".into()),
"ffmpeg_present": ffmpeg_present.is_some(),
}));
checks.push(json!({
"id": "windows_job_object",
"status": "info",
"message": crate::win_job::capability_summary(),
"supported": crate::win_job::platform_supports_job_objects(),
}));
if opts.offline {
checks.push(json!({
"id": "offline",
"status": "pass",
"message": "offline mode: network probes skipped",
"offline": true,
}));
}
if opts.fix && chrome.is_none() {
if let Some(c) = checks
.iter_mut()
.find(|c| c.get("id").and_then(|v| v.as_str()) == Some("chrome"))
{
if let Some(obj) = c.as_object_mut() {
obj.insert(
"fix".into(),
json!("install system Chrome/Chromium or: browser-automation-cli config set chrome_path /path/to/chrome"),
);
}
}
}
let data = json!({
"schema_version": 1,
"checks": checks,
"lighthouse_present": lighthouse_present,
"ffmpeg_present": ffmpeg_present.is_some(),
"offline": opts.offline,
"fix_requested": opts.fix,
"ok": !failed,
});
if opts.json {
let _ = print_success_json(data);
} else {
for c in checks {
println!(
"[{}] {} — {}",
c.get("status").and_then(|s| s.as_str()).unwrap_or("?"),
c.get("id").and_then(|s| s.as_str()).unwrap_or("?"),
c.get("message").and_then(|s| s.as_str()).unwrap_or("")
);
}
}
if failed {
1
} else {
0
}
}
fn resolve_lighthouse_for_doctor() -> Option<(String, &'static str)> {
if let Some(xdg) = crate::xdg::lighthouse_path_from_config().filter(|s| !s.is_empty()) {
let p = std::path::Path::new(&xdg);
if p.is_file() {
let source = if xdg.contains("mock-lighthouse") {
"mock"
} else {
"xdg"
};
return Some((xdg, source));
}
}
which_bin("lighthouse").map(|p| (p, "path"))
}
fn cache_redis_check() -> serde_json::Value {
let cfg = crate::xdg::load_config().unwrap_or_default();
let backend = cfg
.cache_backend
.as_deref()
.unwrap_or("sqlite")
.to_ascii_lowercase();
if backend != "redis" {
return json!({
"id": "cache_redis",
"status": "info",
"backend": backend,
"message": format!("redis not active (cache_backend={backend})"),
});
}
let url = cfg.cache_redis_url.as_deref().unwrap_or("");
match crate::cache::RedisCache::connect(url) {
Ok(_) => json!({
"id": "cache_redis",
"status": "pass",
"backend": "redis",
"message": "redis PING ok (XDG cache_redis_url)",
}),
Err(e) => json!({
"id": "cache_redis",
"status": "fail",
"backend": "redis",
"message": e.message(),
"suggestion": "Start redis-server or: browser-automation-cli config set cache_backend sqlite",
}),
}
}
fn which_bin(name: &str) -> Option<String> {
std::env::var_os("PATH").and_then(|paths| {
for dir in std::env::split_paths(&paths) {
let candidate = dir.join(name);
if candidate.is_file() {
return Some(candidate.display().to_string());
}
#[cfg(windows)]
{
let with_exe = dir.join(format!("{name}.exe"));
if with_exe.is_file() {
return Some(with_exe.display().to_string());
}
}
}
None
})
}