rok-cli 0.6.1

Developer CLI for rok-based Axum applications
//! `rok doctor` — full project diagnostics (M7.1 + M7.2).

use std::path::Path;

pub fn run(fix: bool) -> anyhow::Result<()> {
    let checks: &[(&str, fn() -> Check)] = &[
        ("Cargo.toml present",        check_cargo_toml),
        (".env present",              check_env),
        ("DATABASE_URL configured",   check_database_url),
        ("src/ directory present",    check_src_dir),
        ("cargo-watch installed",     check_cargo_watch),
        ("sqlx-cli installed",        check_sqlx_cli),
        ("Rust toolchain up-to-date", check_rust_toolchain),
        ("rok version check",         check_rok_version),
    ];

    let mut passed = 0usize;
    let mut failed = 0usize;
    let mut warned = 0usize;

    println!();
    println!("rok doctor — project diagnostics");
    println!("{}", "".repeat(50));

    for (label, check_fn) in checks {
        let result = check_fn();
        let (icon, count_fn): (&str, fn(&mut usize)) = match result.status {
            CheckStatus::Pass => ("", |c| *c += 1),
            CheckStatus::Fail => ("", |c| *c += 1),
            CheckStatus::Warn => ("!", |c| *c += 1),
        };
        let _ = count_fn; // used below

        match result.status {
            CheckStatus::Pass => {
                passed += 1;
                println!("  {icon} {label}");
            }
            CheckStatus::Warn => {
                warned += 1;
                println!("  {icon} {label}{}", result.message.as_deref().unwrap_or(""));
            }
            CheckStatus::Fail => {
                failed += 1;
                println!("  {icon} {label}{}", result.message.as_deref().unwrap_or("failed"));
                if let Some(fix_msg) = &result.fix_hint {
                    if fix {
                        if let Some(fix_fn) = result.auto_fix {
                            match fix_fn() {
                                Ok(msg) => println!("    → fixed: {msg}"),
                                Err(e) => println!("    → fix failed: {e}"),
                            }
                        } else {
                            println!("    → hint: {fix_msg}");
                        }
                    } else {
                        println!("    → fix: {fix_msg}");
                    }
                }
            }
        }
    }

    println!("{}", "".repeat(50));
    println!("  {passed} passed  {warned} warnings  {failed} failed");
    println!();

    if failed > 0 && !fix {
        println!("Run `rok doctor --fix` to auto-resolve safe issues.");
    }
    Ok(())
}

// ── Check type ────────────────────────────────────────────────────────────────

enum CheckStatus { Pass, Fail, Warn }

struct Check {
    status: CheckStatus,
    message: Option<String>,
    fix_hint: Option<String>,
    auto_fix: Option<fn() -> anyhow::Result<String>>,
}

impl Check {
    fn pass() -> Self { Self { status: CheckStatus::Pass, message: None, fix_hint: None, auto_fix: None } }
    fn fail(msg: impl Into<String>) -> Self {
        Self { status: CheckStatus::Fail, message: Some(msg.into()), fix_hint: None, auto_fix: None }
    }
    fn warn(msg: impl Into<String>) -> Self {
        Self { status: CheckStatus::Warn, message: Some(msg.into()), fix_hint: None, auto_fix: None }
    }
    fn with_hint(mut self, hint: impl Into<String>) -> Self { self.fix_hint = Some(hint.into()); self }
    fn with_auto_fix(mut self, f: fn() -> anyhow::Result<String>) -> Self { self.auto_fix = Some(f); self }
}

// ── Individual checks ─────────────────────────────────────────────────────────

fn check_cargo_toml() -> Check {
    if Path::new("Cargo.toml").exists() { Check::pass() }
    else { Check::fail("Cargo.toml not found").with_hint("Run `cargo init` to create a Rust project") }
}

fn check_env() -> Check {
    if Path::new(".env").exists() { Check::pass() }
    else if Path::new(".env.example").exists() {
        Check::fail(".env not found")
            .with_hint("cp .env.example .env")
            .with_auto_fix(|| {
                std::fs::copy(".env.example", ".env")?;
                Ok(".env.example → .env".into())
            })
    } else {
        Check::warn(".env not found — run `rok env:generate`")
    }
}

fn check_database_url() -> Check {
    if std::env::var("DATABASE_URL").is_ok() { return Check::pass(); }
    if Path::new(".env").exists() {
        let content = std::fs::read_to_string(".env").unwrap_or_default();
        if content.contains("DATABASE_URL") {
            Check::warn("DATABASE_URL in .env but not exported to current shell")
        } else {
            Check::fail("DATABASE_URL not set in .env")
                .with_hint("Add DATABASE_URL=postgres://... to .env")
        }
    } else {
        Check::fail("DATABASE_URL not set").with_hint("Add DATABASE_URL=postgres://... to .env")
    }
}

fn check_src_dir() -> Check {
    if Path::new("src").exists() { Check::pass() }
    else { Check::fail("src/ directory missing") }
}

fn check_cargo_watch() -> Check {
    match std::process::Command::new("cargo-watch").arg("--version").output() {
        Ok(_) => Check::pass(),
        Err(_) => Check::warn("cargo-watch not installed — `rok dev` requires it")
            .with_hint("cargo install cargo-watch"),
    }
}

fn check_sqlx_cli() -> Check {
    match std::process::Command::new("sqlx").arg("--version").output() {
        Ok(_) => Check::pass(),
        Err(_) => Check::warn("sqlx-cli not installed — optional but recommended")
            .with_hint("cargo install sqlx-cli"),
    }
}

fn check_rust_toolchain() -> Check {
    match std::process::Command::new("rustup").args(["show", "active-toolchain"]).output() {
        Ok(o) => {
            let s = String::from_utf8_lossy(&o.stdout);
            let toolchain = s.split_whitespace().next().unwrap_or("unknown");
            Check::pass().with_hint(toolchain.to_string())
        }
        Err(_) => Check::warn("rustup not in PATH — cannot verify toolchain"),
    }
}

fn check_rok_version() -> Check {
    let current = env!("CARGO_PKG_VERSION");
    Check::pass().with_hint(format!("v{current} (run `rok self-update` to check for upgrades)"))
}