use std::process::Command;
use anyhow::Result;
use clap::Args;
#[derive(Args, Debug)]
pub struct DoctorArgs {}
pub fn run(_args: DoctorArgs) -> Result<()> {
let toolchain_owned = crate::cli::find_rustc_shim()
.and_then(|d| Command::new(d).arg("--print-toolchain").output().ok())
.filter(|o| o.status.success())
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "nightly".to_string());
let toolchain = toolchain_owned.as_str();
let channel = channel_of(toolchain);
let install_tc = format!(
"rustup toolchain install {channel} --profile minimal \
--component rust-src --component rustc-dev --component llvm-tools"
);
println!(
"{}",
crate::style::brand("xark doctor — checking your setup\n")
);
let mut problems = 0u32;
let cargo = cmd_ok("cargo", &["--version"]);
problems += check(
cargo.is_some(),
"cargo",
cargo.as_deref(),
"install Rust from https://rustup.rs",
);
let rustup = cmd_ok("rustup", &["--version"]);
problems += check(
rustup.is_some(),
"rustup",
rustup.as_deref(),
"install Rust from https://rustup.rs",
);
let toolchains = cmd_ok("rustup", &["toolchain", "list"]).unwrap_or_default();
let tc_ok = toolchains
.lines()
.any(|l| l.starts_with(toolchain) || l.starts_with(channel.as_str()));
problems += check(
tc_ok,
&format!("toolchain {channel}"),
tc_ok.then_some(toolchain),
&install_tc,
);
let comps = if tc_ok {
cmd_ok(
"rustup",
&["component", "list", "--toolchain", toolchain, "--installed"],
)
.unwrap_or_default()
} else {
String::new()
};
for comp in ["rustc-dev", "rust-src", "llvm-tools"] {
let installed = tc_ok
&& comps
.lines()
.any(|l| l.trim() == comp || l.trim().starts_with(&format!("{comp}-")));
problems += check(
installed,
&format!("component {comp}"),
None,
&format!("rustup component add {comp} --toolchain {channel}"),
);
}
let onpath = cmd_ok("xark", &["--version"]);
info(
onpath.is_some(),
"xark on PATH",
"needed for `xark init` and rust-analyzer diagnostics",
"cargo +nightly-… install --path crates/lang --features cli",
);
let node = cmd_ok("node", &["--version"]);
info(
node.is_some(),
"node",
"optional — only for the `xark client` TypeScript output",
"install Node.js from https://nodejs.org",
);
println!();
if problems == 0 {
println!(
"{}",
crate::style::brand("✅ All required checks passed — you're ready to `xark init`.")
);
} else {
println!(
"{}",
crate::style::err(&format!(
"❌ {problems} issue(s) above — fix the ❌ lines, then re-run `xark doctor`."
))
);
std::process::exit(1);
}
Ok(())
}
fn cmd_ok(bin: &str, args: &[&str]) -> Option<String> {
let out = Command::new(bin).args(args).output().ok()?;
out.status
.success()
.then(|| String::from_utf8_lossy(&out.stdout).trim().to_string())
}
fn check(ok: bool, label: &str, detail: Option<&str>, fix: &str) -> u32 {
if ok {
let d = detail
.map(|d| format!(" {}", crate::style::dim(d)))
.unwrap_or_default();
println!(" {} {label}{d}", crate::style::brand("✅"));
0
} else {
println!(" {} {label}", crate::style::err("❌"));
println!(" {}", crate::style::dim(&format!("fix: {fix}")));
1
}
}
fn info(ok: bool, label: &str, note: &str, fix: &str) {
if ok {
println!(
" {} {label} {}",
crate::style::brand("✅"),
crate::style::dim(note)
);
} else {
println!(
" {} {label} {}",
crate::style::warn("•"),
crate::style::dim(note)
);
println!(" {}", crate::style::dim(&format!("fix: {fix}")));
}
}
fn channel_of(tc: &str) -> String {
let parts: Vec<&str> = tc.split('-').collect();
if parts.len() >= 4
&& parts[0] == "nightly"
&& parts[1].len() == 4
&& parts[1].bytes().all(|b| b.is_ascii_digit())
{
format!("nightly-{}-{}-{}", parts[1], parts[2], parts[3])
} else {
tc.to_string()
}
}