From df7ca091ec8f6f5986726eca37639120f1d6c144 Mon Sep 17 00:00:00 2001 From: Eric Ratliff Date: Sat, 31 Jan 2026 10:50:38 -0600 Subject: [PATCH] feat: Add system diagnostics command Adds `weevil doctor` to check development environment health. Reports status of Java, FTC SDK, Android SDK, ADB, and Gradle. Provides clear next steps based on system state. --- src/commands/doctor.rs | 267 +++++++++++++++++++++++++++++++++++++++++ src/commands/mod.rs | 3 +- src/main.rs | 6 + 3 files changed, 275 insertions(+), 1 deletion(-) create mode 100644 src/commands/doctor.rs diff --git a/src/commands/doctor.rs b/src/commands/doctor.rs new file mode 100644 index 0000000..983b2ae --- /dev/null +++ b/src/commands/doctor.rs @@ -0,0 +1,267 @@ +use anyhow::Result; +use std::path::Path; +use std::process::Command; +use colored::*; + +use crate::sdk::SdkConfig; + +#[derive(Debug)] +pub struct SystemHealth { + pub java_ok: bool, + pub java_version: Option, + pub ftc_sdk_ok: bool, + pub ftc_sdk_version: Option, + pub android_sdk_ok: bool, + pub adb_ok: bool, + pub adb_version: Option, + pub gradle_ok: bool, + pub gradle_version: Option, +} + +impl SystemHealth { + pub fn is_healthy(&self) -> bool { + // Required: Java, FTC SDK, Android SDK + // Optional: ADB in PATH (can be in Android SDK), Gradle (projects have wrapper) + self.java_ok && self.ftc_sdk_ok && self.android_sdk_ok + } +} + +/// Run system diagnostics and report health status +pub fn run_diagnostics() -> Result<()> { + println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan()); + println!("{}", " 🩺 Weevil Doctor - System Diagnostics".bright_cyan().bold()); + println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan()); + println!(); + + let health = check_system_health()?; + print_diagnostics(&health); + + println!(); + println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan()); + + if health.is_healthy() { + println!("{}", " ✓ System is healthy and ready for FTC development".bright_green().bold()); + println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan()); + println!(); + println!("{}", "You can now:".bright_yellow().bold()); + println!(" - Create a new project: {}", "weevil new ".bright_cyan()); + println!(" - Setup a cloned project: {}", "weevil setup ".bright_cyan()); + } else { + println!("{}", " ⚠ Issues found - setup required".bright_yellow().bold()); + println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan()); + println!(); + println!("{}", "To fix issues, run:".bright_yellow().bold()); + println!(" {}", "weevil setup".bright_cyan()); + } + + println!(); + + Ok(()) +} + +/// Check system health and return a report +pub fn check_system_health() -> Result { + let sdk_config = SdkConfig::new()?; + + // Check Java + let (java_ok, java_version) = match check_java() { + Ok(version) => (true, Some(version)), + Err(_) => (false, None), + }; + + // Check FTC SDK + let (ftc_sdk_ok, ftc_sdk_version) = if sdk_config.ftc_sdk_path.exists() { + match crate::sdk::ftc::verify(&sdk_config.ftc_sdk_path) { + Ok(_) => { + let version = crate::sdk::ftc::get_version(&sdk_config.ftc_sdk_path) + .unwrap_or_else(|_| "unknown".to_string()); + (true, Some(version)) + } + Err(_) => (false, None), + } + } else { + (false, None) + }; + + // Check Android SDK + let android_sdk_ok = if sdk_config.android_sdk_path.exists() { + crate::sdk::android::verify(&sdk_config.android_sdk_path).is_ok() + } else { + false + }; + + // Check ADB + let (adb_ok, adb_version) = match check_adb(&sdk_config.android_sdk_path) { + Ok(version) => (true, Some(version)), + Err(_) => (false, None), + }; + + // Check Gradle (optional) + let (gradle_ok, gradle_version) = match check_gradle() { + Ok(version) => (true, Some(version)), + Err(_) => (false, None), + }; + + Ok(SystemHealth { + java_ok, + java_version, + ftc_sdk_ok, + ftc_sdk_version, + android_sdk_ok, + adb_ok, + adb_version, + gradle_ok, + gradle_version, + }) +} + +fn print_diagnostics(health: &SystemHealth) { + let sdk_config = SdkConfig::new().unwrap(); + + println!("{}", "Required Components:".bright_yellow().bold()); + println!(); + + // Java + if health.java_ok { + println!(" {} Java JDK {}", + "✓".green(), + health.java_version.as_ref().unwrap() + ); + } else { + println!(" {} Java JDK {}", + "✗".red(), + "not found".red() + ); + } + + // FTC SDK + if health.ftc_sdk_ok { + println!(" {} FTC SDK {} at {}", + "✓".green(), + health.ftc_sdk_version.as_ref().unwrap(), + sdk_config.ftc_sdk_path.display() + ); + } else { + println!(" {} FTC SDK {} (expected at {})", + "✗".red(), + "not found".red(), + sdk_config.ftc_sdk_path.display() + ); + } + + // Android SDK + if health.android_sdk_ok { + println!(" {} Android SDK at {}", + "✓".green(), + sdk_config.android_sdk_path.display() + ); + } else { + println!(" {} Android SDK {} (expected at {})", + "✗".red(), + "not found".red(), + sdk_config.android_sdk_path.display() + ); + } + + println!(); + println!("{}", "Optional Components:".bright_yellow().bold()); + println!(); + + // ADB + if health.adb_ok { + println!(" {} ADB {}", + "✓".green(), + health.adb_version.as_ref().unwrap() + ); + } else { + println!(" {} ADB {}", + "⚠".yellow(), + "not in PATH (included in Android SDK)".yellow() + ); + } + + // Gradle + if health.gradle_ok { + println!(" {} Gradle {}", + "✓".green(), + health.gradle_version.as_ref().unwrap() + ); + } else { + println!(" {} Gradle {}", + "⚠".yellow(), + "not in PATH (projects include wrapper)".yellow() + ); + } +} + +fn check_java() -> Result { + let output = Command::new("java") + .arg("-version") + .output(); + + match output { + Ok(out) => { + let stderr = String::from_utf8_lossy(&out.stderr); + for line in stderr.lines() { + if line.contains("version") { + if let Some(version_str) = line.split('"').nth(1) { + return Ok(version_str.to_string()); + } + } + } + Ok("installed (version unknown)".to_string()) + } + Err(_) => anyhow::bail!("Java JDK not found in PATH"), + } +} + +fn check_adb(android_sdk_path: &Path) -> Result { + // First try system PATH + let output = Command::new("adb") + .arg("version") + .output(); + + if let Ok(out) = output { + if out.status.success() { + let stdout = String::from_utf8_lossy(&out.stdout); + for line in stdout.lines() { + if line.starts_with("Android Debug Bridge version") { + return Ok(line.replace("Android Debug Bridge version ", "")); + } + } + return Ok("installed (version unknown)".to_string()); + } + } + + // Try Android SDK location + let adb_path = if cfg!(target_os = "windows") { + android_sdk_path.join("platform-tools").join("adb.exe") + } else { + android_sdk_path.join("platform-tools").join("adb") + }; + + if adb_path.exists() { + anyhow::bail!("ADB found in Android SDK but not in PATH") + } else { + anyhow::bail!("ADB not found") + } +} + +fn check_gradle() -> Result { + let output = Command::new("gradle") + .arg("--version") + .output(); + + match output { + Ok(out) => { + let stdout = String::from_utf8_lossy(&out.stdout); + for line in stdout.lines() { + if line.starts_with("Gradle") { + return Ok(line.replace("Gradle ", "")); + } + } + Ok("installed (version unknown)".to_string()) + } + Err(_) => anyhow::bail!("Gradle not found in PATH"), + } +} \ No newline at end of file diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 623c03f..9330d7a 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -3,4 +3,5 @@ pub mod upgrade; pub mod deploy; pub mod sdk; pub mod config; -pub mod setup; \ No newline at end of file +pub mod setup; +pub mod doctor; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index fe02881..d078719 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,6 +33,9 @@ enum Commands { android_sdk: Option, }, + /// Check system health and diagnose issues + Doctor, + /// Setup development environment (system or project) Setup { /// Path to project directory (optional - without it, sets up system) @@ -105,6 +108,9 @@ fn main() -> Result<()> { Commands::New { name, ftc_sdk, android_sdk } => { commands::new::create_project(&name, ftc_sdk.as_deref(), android_sdk.as_deref()) } + Commands::Doctor => { + commands::doctor::run_diagnostics() + } Commands::Setup { path } => { commands::setup::setup_environment(path.as_deref()) }