use crate::ui::{self, HealthStatus};
use anyhow::Result;
use std::path::PathBuf;
#[derive(Debug, Clone, Default)]
pub struct DoctorArgs {
pub repair: bool,
pub force: bool,
pub deep: bool,
}
pub async fn run_doctor(args: DoctorArgs) -> Result<()> {
ui::header("OpenClaw Doctor");
println!();
let mut issues_found = 0;
let mut repairs_made = 0;
ui::info("Checking configuration...");
match check_config() {
CheckResult::Ok => {
ui::health_check("Configuration", HealthStatus::Ok, None);
}
CheckResult::Warning(msg) => {
ui::health_check("Configuration", HealthStatus::Warning, Some(&msg));
issues_found += 1;
}
CheckResult::Error(msg) => {
ui::health_check("Configuration", HealthStatus::Error, Some(&msg));
issues_found += 1;
if args.repair {
ui::info(" → Creating default configuration...");
if create_default_config().is_ok() {
ui::success(" → Configuration created");
repairs_made += 1;
}
}
}
}
ui::info("Checking state directory...");
match check_state_dir() {
CheckResult::Ok => {
ui::health_check("State directory", HealthStatus::Ok, None);
}
CheckResult::Warning(msg) => {
ui::health_check("State directory", HealthStatus::Warning, Some(&msg));
issues_found += 1;
}
CheckResult::Error(msg) => {
ui::health_check("State directory", HealthStatus::Error, Some(&msg));
issues_found += 1;
if args.repair {
ui::info(" → Creating state directory...");
if create_state_dir().is_ok() {
ui::success(" → State directory created");
repairs_made += 1;
}
}
}
}
ui::info("Checking credentials...");
match check_credentials() {
CheckResult::Ok => {
ui::health_check("Credentials", HealthStatus::Ok, None);
}
CheckResult::Warning(msg) => {
ui::health_check("Credentials", HealthStatus::Warning, Some(&msg));
issues_found += 1;
}
CheckResult::Error(msg) => {
ui::health_check("Credentials", HealthStatus::Error, Some(&msg));
issues_found += 1;
}
}
ui::info("Checking sandbox...");
match check_sandbox() {
CheckResult::Ok => {
ui::health_check("Sandbox", HealthStatus::Ok, None);
}
CheckResult::Warning(msg) => {
ui::health_check("Sandbox", HealthStatus::Warning, Some(&msg));
issues_found += 1;
}
CheckResult::Error(msg) => {
ui::health_check("Sandbox", HealthStatus::Error, Some(&msg));
issues_found += 1;
}
}
ui::info("Checking gateway...");
match check_gateway().await {
CheckResult::Ok => {
ui::health_check("Gateway", HealthStatus::Ok, Some("running"));
}
CheckResult::Warning(msg) => {
ui::health_check("Gateway", HealthStatus::Warning, Some(&msg));
}
CheckResult::Error(msg) => {
ui::health_check("Gateway", HealthStatus::Error, Some(&msg));
}
}
ui::info("Checking shell completion...");
match check_shell_completion() {
CheckResult::Ok => {
ui::health_check("Shell completion", HealthStatus::Ok, None);
}
CheckResult::Warning(msg) => {
ui::health_check("Shell completion", HealthStatus::Warning, Some(&msg));
issues_found += 1;
}
CheckResult::Error(msg) => {
ui::health_check("Shell completion", HealthStatus::Error, Some(&msg));
issues_found += 1;
}
}
if args.deep {
ui::info("Running deep scan...");
match check_multiple_gateways() {
CheckResult::Ok => {
ui::health_check(
"Gateway instances",
HealthStatus::Ok,
Some("single instance"),
);
}
CheckResult::Warning(msg) => {
ui::health_check("Gateway instances", HealthStatus::Warning, Some(&msg));
issues_found += 1;
}
CheckResult::Error(msg) => {
ui::health_check("Gateway instances", HealthStatus::Error, Some(&msg));
issues_found += 1;
}
}
}
println!();
ui::header("Summary");
if issues_found == 0 {
ui::success("All checks passed!");
} else {
ui::warning(&format!("{issues_found} issue(s) found"));
if args.repair {
ui::info(&format!("{repairs_made} repair(s) made"));
} else {
ui::info("Run with --repair to fix issues automatically");
}
}
Ok(())
}
enum CheckResult {
Ok,
Warning(String),
Error(String),
}
fn check_config() -> CheckResult {
let config_path = get_config_path();
if !config_path.exists() {
return CheckResult::Error("Config file not found".to_string());
}
match openclaw_core::Config::load_default() {
Ok(_) => CheckResult::Ok,
Err(e) => CheckResult::Error(format!("Invalid config: {e}")),
}
}
fn check_state_dir() -> CheckResult {
let state_dir = get_state_dir();
if !state_dir.exists() {
return CheckResult::Error("State directory not found".to_string());
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(metadata) = std::fs::metadata(&state_dir) {
let mode = metadata.permissions().mode();
if mode & 0o077 != 0 {
return CheckResult::Warning("State directory has loose permissions".to_string());
}
}
}
CheckResult::Ok
}
fn check_credentials() -> CheckResult {
let cred_path = get_credentials_path();
if !cred_path.exists() {
return CheckResult::Warning("No credentials configured".to_string());
}
let entries = std::fs::read_dir(&cred_path).ok();
let has_creds = entries.is_some_and(|e| e.count() > 0);
if !has_creds {
return CheckResult::Warning("No API keys stored".to_string());
}
CheckResult::Ok
}
fn check_sandbox() -> CheckResult {
if openclaw_agents::sandbox::is_sandbox_available() {
CheckResult::Ok
} else {
#[cfg(target_os = "linux")]
{
CheckResult::Warning(
"bubblewrap (bwrap) not found - install for sandboxing".to_string(),
)
}
#[cfg(target_os = "macos")]
{
CheckResult::Ok }
#[cfg(target_os = "windows")]
{
CheckResult::Warning("Windows sandboxing not yet implemented".to_string())
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
CheckResult::Error("Sandboxing not supported on this platform".to_string())
}
}
}
async fn check_gateway() -> CheckResult {
let addr = "127.0.0.1:18789";
match tokio::net::TcpStream::connect(addr).await {
Ok(_) => {
let client = reqwest::Client::new();
match client
.get("http://127.0.0.1:18789/health")
.timeout(std::time::Duration::from_secs(2))
.send()
.await
{
Ok(resp) if resp.status().is_success() => CheckResult::Ok,
Ok(resp) => {
CheckResult::Warning(format!("Gateway returned status {}", resp.status()))
}
Err(_) => {
CheckResult::Warning("Gateway running but health check failed".to_string())
}
}
}
Err(_) => CheckResult::Warning("Gateway not running".to_string()),
}
}
fn check_shell_completion() -> CheckResult {
let completion_dir = get_state_dir().join("completions");
if !completion_dir.exists() {
return CheckResult::Warning(
"Shell completion not installed - run 'openclaw completion --install'".to_string(),
);
}
let has_completion = std::fs::read_dir(&completion_dir)
.ok()
.is_some_and(|e| e.count() > 0);
if has_completion {
CheckResult::Ok
} else {
CheckResult::Warning("Shell completion files missing".to_string())
}
}
const fn check_multiple_gateways() -> CheckResult {
CheckResult::Ok
}
fn create_default_config() -> Result<()> {
let config_path = get_config_path();
std::fs::create_dir_all(config_path.parent().unwrap())?;
let default_config = serde_json::json!({
"gateway": {
"mode": "local",
"port": 18789,
"bind": "loopback"
},
"agents": {
"defaults": {
"model": "claude-sonnet-4-20250514"
}
}
});
std::fs::write(&config_path, serde_json::to_string_pretty(&default_config)?)?;
Ok(())
}
fn create_state_dir() -> Result<()> {
let state_dir = get_state_dir();
std::fs::create_dir_all(&state_dir)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&state_dir, std::fs::Permissions::from_mode(0o700))?;
}
Ok(())
}
fn get_config_path() -> PathBuf {
if let Ok(path) = std::env::var("OPENCLAW_CONFIG_PATH") {
return PathBuf::from(path);
}
get_state_dir().join("openclaw.json")
}
fn get_state_dir() -> PathBuf {
if let Ok(state_dir) = std::env::var("OPENCLAW_STATE_DIR") {
return PathBuf::from(state_dir);
}
dirs::home_dir().map_or_else(|| PathBuf::from(".openclaw"), |h| h.join(".openclaw"))
}
fn get_credentials_path() -> PathBuf {
get_state_dir().join("credentials")
}