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;
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(())
}
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 }
}
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)"))
}