use colored::Colorize;
use std::process::Command;
use crate::error::Result;
#[derive(Debug)]
struct CheckResult {
name: String,
passed: bool,
message: String,
level: CheckLevel,
}
#[derive(Debug)]
enum CheckLevel {
Required,
Optional,
}
impl CheckResult {
fn pass(name: &str, message: String) -> Self {
Self {
name: name.to_string(),
passed: true,
message,
level: CheckLevel::Required,
}
}
fn fail(name: &str, message: String) -> Self {
Self {
name: name.to_string(),
passed: false,
message,
level: CheckLevel::Required,
}
}
fn optional(name: &str, passed: bool, message: String) -> Self {
Self {
name: name.to_string(),
passed,
message,
level: CheckLevel::Optional,
}
}
}
pub async fn doctor(fix: bool) -> Result<()> {
println!("\n{}\n", "Checking development environment...".cyan().bold());
let mut checks = vec![];
checks.push(check_rust_version());
checks.push(check_cargo());
checks.push(check_docker());
checks.push(check_docker_compose());
checks.push(check_git());
checks.push(check_kubectl());
checks.extend(check_env_vars());
checks.extend(check_ports());
display_results(&checks);
print_summary(&checks);
if fix {
println!("\n{}", "Auto-fix is not yet implemented.".yellow());
println!("Please install missing tools manually.");
}
Ok(())
}
fn check_rust_version() -> CheckResult {
match Command::new("rustc").arg("--version").output() {
Ok(output) => {
let version_str = String::from_utf8_lossy(&output.stdout);
let version = version_str.trim();
if let Some(ver) = version.split_whitespace().nth(1) {
if is_version_sufficient(ver, "1.75.0") {
CheckResult::pass("Rust", format!("{}", version))
} else {
CheckResult::fail("Rust", format!("{} (required: >= 1.75.0)", version))
}
} else {
CheckResult::pass("Rust", version.to_string())
}
}
Err(_) => CheckResult::fail("Rust", "Not found. Install from https://rustup.rs".to_string()),
}
}
fn check_cargo() -> CheckResult {
match Command::new("cargo").arg("--version").output() {
Ok(output) => {
let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
CheckResult::pass("Cargo", version)
}
Err(_) => CheckResult::fail("Cargo", "Not found".to_string()),
}
}
fn check_docker() -> CheckResult {
match Command::new("docker").arg("--version").output() {
Ok(output) => {
let version_str = String::from_utf8_lossy(&output.stdout);
let version = version_str.trim();
if let Some(ver) = extract_docker_version(version) {
if is_version_sufficient(&ver, "20.10.0") {
CheckResult::pass("Docker", version.to_string())
} else {
CheckResult::fail("Docker", format!("{} (required: >= 20.10)", version))
}
} else {
CheckResult::pass("Docker", version.to_string())
}
}
Err(_) => CheckResult::fail("Docker", "Not found. Install from https://docs.docker.com/get-docker/".to_string()),
}
}
fn check_docker_compose() -> CheckResult {
match Command::new("docker").args(&["compose", "version"]).output() {
Ok(output) => {
let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
CheckResult::pass("Docker Compose", version)
}
Err(_) => {
match Command::new("docker-compose").arg("--version").output() {
Ok(output) => {
let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
CheckResult::pass("Docker Compose", version)
}
Err(_) => CheckResult::fail(
"Docker Compose",
"Not found. Install from https://docs.docker.com/compose/install/".to_string(),
),
}
}
}
}
fn check_git() -> CheckResult {
match Command::new("git").arg("--version").output() {
Ok(output) => {
let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
CheckResult::pass("Git", version)
}
Err(_) => CheckResult::fail("Git", "Not found. Install from https://git-scm.com/".to_string()),
}
}
fn check_kubectl() -> CheckResult {
match Command::new("kubectl").arg("version").arg("--client").output() {
Ok(output) => {
let version = String::from_utf8_lossy(&output.stdout);
let version_line = version.lines().next().unwrap_or("unknown");
CheckResult::optional("kubectl", true, version_line.to_string())
}
Err(_) => CheckResult::optional(
"kubectl",
false,
"Not found (optional, needed for K8s deployments)".to_string(),
),
}
}
fn check_env_vars() -> Vec<CheckResult> {
let mut results = vec![];
let env_vars = vec![
("HETZNER_API_TOKEN", "Required for provisioning servers on Hetzner", CheckLevel::Optional),
("CLOUDFLARE_API_TOKEN", "Required for DNS management", CheckLevel::Optional),
("GITLAB_TOKEN", "Required for CI/CD setup", CheckLevel::Optional),
];
for (var, description, level) in env_vars {
match std::env::var(var) {
Ok(value) => {
let masked = mask_token(&value);
results.push(CheckResult::optional(var, true, format!("Set ({})", masked)));
}
Err(_) => {
results.push(CheckResult::optional(var, false, format!("Not set ({})", description)));
}
}
}
results
}
fn check_ports() -> Vec<CheckResult> {
let mut results = vec![];
let ports = vec![
(5432, "PostgreSQL"),
(5672, "RabbitMQ"),
(6379, "Redis"),
(8200, "Vault"),
(8080, "Default API port"),
];
for (port, service) in ports {
match is_port_available(port) {
true => {
results.push(CheckResult::optional(
&format!("Port {}", port),
true,
format!("Available ({})", service),
));
}
false => {
results.push(CheckResult::optional(
&format!("Port {}", port),
false,
format!("In use ({})", service),
));
}
}
}
results
}
fn display_results(checks: &[CheckResult]) {
for check in checks {
let symbol = if check.passed { "✓".green() } else { "✗".red() };
let name = format!("{:25}", check.name);
match check.level {
CheckLevel::Required => {
if check.passed {
println!("{} {} {}", symbol, name, check.message.green());
} else {
println!("{} {} {}", symbol, name, check.message.red());
}
}
CheckLevel::Optional => {
if check.passed {
println!("{} {} {}", symbol, name, check.message);
} else {
println!("{} {} {}", "⚠".yellow(), name, check.message.yellow());
}
}
}
}
}
fn print_summary(checks: &[CheckResult]) {
let total = checks.iter().filter(|c| matches!(c.level, CheckLevel::Required)).count();
let passed = checks
.iter()
.filter(|c| matches!(c.level, CheckLevel::Required) && c.passed)
.count();
let optional_passed = checks
.iter()
.filter(|c| matches!(c.level, CheckLevel::Optional) && c.passed)
.count();
println!("\n{}", "Summary:".cyan().bold());
println!(" Required checks: {}/{} passed", passed.to_string().green(), total);
println!(" Optional checks: {} passed", optional_passed.to_string().cyan());
if passed == total {
println!("\n{}", "✓ Ready for local development!".green().bold());
if optional_passed < (checks.len() - total) {
println!("{}", "⚠ Some optional features unavailable (missing credentials)".yellow());
}
} else {
println!("\n{}", "✗ Not ready - please install missing tools".red().bold());
println!("\nRun {} to install missing tools", "lmrc doctor --fix".cyan());
}
}
fn is_version_sufficient(current: &str, required: &str) -> bool {
let current_parts: Vec<u32> = current
.split('.')
.filter_map(|s| s.parse().ok())
.collect();
let required_parts: Vec<u32> = required
.split('.')
.filter_map(|s| s.parse().ok())
.collect();
for i in 0..3 {
let c = current_parts.get(i).unwrap_or(&0);
let r = required_parts.get(i).unwrap_or(&0);
if c > r {
return true;
}
if c < r {
return false;
}
}
true
}
fn extract_docker_version(version_str: &str) -> Option<String> {
version_str
.split_whitespace()
.nth(2)
.and_then(|v| v.trim_end_matches(',').parse::<String>().ok())
}
fn mask_token(token: &str) -> String {
if token.len() > 8 {
format!("{}...{}", &token[..4], &token[token.len()-4..])
} else {
"****".to_string()
}
}
fn is_port_available(port: u16) -> bool {
use std::net::TcpListener;
TcpListener::bind(("127.0.0.1", port)).is_ok()
}