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.
This commit is contained in:
Eric Ratliff
2026-01-31 10:50:38 -06:00
parent 4e9575cc4f
commit df7ca091ec
3 changed files with 275 additions and 1 deletions

267
src/commands/doctor.rs Normal file
View File

@@ -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<String>,
pub ftc_sdk_ok: bool,
pub ftc_sdk_version: Option<String>,
pub android_sdk_ok: bool,
pub adb_ok: bool,
pub adb_version: Option<String>,
pub gradle_ok: bool,
pub gradle_version: Option<String>,
}
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 <project-name>".bright_cyan());
println!(" - Setup a cloned project: {}", "weevil setup <project-path>".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<SystemHealth> {
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<String> {
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<String> {
// 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<String> {
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"),
}
}

View File

@@ -3,4 +3,5 @@ pub mod upgrade;
pub mod deploy; pub mod deploy;
pub mod sdk; pub mod sdk;
pub mod config; pub mod config;
pub mod setup; pub mod setup;
pub mod doctor;

View File

@@ -33,6 +33,9 @@ enum Commands {
android_sdk: Option<String>, android_sdk: Option<String>,
}, },
/// Check system health and diagnose issues
Doctor,
/// Setup development environment (system or project) /// Setup development environment (system or project)
Setup { Setup {
/// Path to project directory (optional - without it, sets up system) /// 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 { name, ftc_sdk, android_sdk } => {
commands::new::create_project(&name, ftc_sdk.as_deref(), android_sdk.as_deref()) commands::new::create_project(&name, ftc_sdk.as_deref(), android_sdk.as_deref())
} }
Commands::Doctor => {
commands::doctor::run_diagnostics()
}
Commands::Setup { path } => { Commands::Setup { path } => {
commands::setup::setup_environment(path.as_deref()) commands::setup::setup_environment(path.as_deref())
} }