use std::path::PathBuf;
use crate::compiler::{Compiler, Origin, Skew};
use crate::probe::{self, DetectOpts, Probe, Toolbox};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Capability {
Compile,
Test,
Deploy,
Editor,
BuildFromSource,
}
impl Capability {
pub fn token(self) -> &'static str {
match self {
Capability::Compile => "compile",
Capability::Test => "test",
Capability::Deploy => "deploy",
Capability::Editor => "editor",
Capability::BuildFromSource => "build",
}
}
pub fn is_optional(self) -> bool {
matches!(self, Capability::Editor | Capability::BuildFromSource)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Level {
Ok,
Warn,
Fail,
}
#[derive(Debug, Clone)]
pub struct Row {
pub label: String,
pub level: Level,
pub detail: String,
pub remedy: Option<String>,
}
#[derive(Debug, Clone)]
pub struct CapabilityReport {
pub capability: Capability,
pub optional: bool,
pub rows: Vec<Row>,
pub level: Level,
}
#[derive(Debug, Clone)]
pub struct Report {
pub driver_version: String,
pub compiler: Compiler,
pub capabilities: Vec<CapabilityReport>,
}
#[derive(Debug, Clone, Default)]
pub struct DoctorOptions {
pub only: Option<Capability>,
pub strict: bool,
}
#[derive(Debug, Clone)]
pub struct Context {
pub project_root: Option<PathBuf>,
pub in_repo: bool,
pub node_floor: u32,
}
impl Report {
pub fn exit_nonzero(&self, opts: &DoctorOptions) -> bool {
for cap in &self.capabilities {
let required =
cap.capability == Capability::Compile || opts.only == Some(cap.capability);
if required && cap.level == Level::Fail {
return true;
}
}
if opts.strict && self.capabilities.iter().any(|c| c.level != Level::Ok) {
return true;
}
false
}
pub fn is_all_ok(&self) -> bool {
self.capabilities.iter().all(|c| c.level == Level::Ok)
}
}
pub fn diagnose(
tb: &dyn Toolbox,
compiler: &Compiler,
ctx: &Context,
opts: &DoctorOptions,
) -> Report {
let root = ctx.project_root.as_deref();
let mut capabilities = vec![compile_report(compiler)];
let want = |cap: Capability| opts.only.is_none() || opts.only == Some(cap);
if want(Capability::Test) {
let node = detect_node(tb, root, ctx.node_floor);
let runner = detect_runner(tb, root);
capabilities.push(capability(Capability::Test, vec![node, runner]));
}
if want(Capability::Deploy) {
let node = detect_node(tb, root, ctx.node_floor);
let wrangler = detect_npm_tool(tb, root, "wrangler", "npm install -g wrangler");
capabilities.push(capability(Capability::Deploy, vec![node, wrangler]));
}
if want(Capability::Editor) {
let lsp = detect_plain(
tb,
"bynkc-lsp",
"install bynkc-lsp (or download from releases)",
);
capabilities.push(capability(Capability::Editor, vec![lsp]));
}
if ctx.in_repo && want(Capability::BuildFromSource) {
let cargo = detect_plain(tb, "cargo", "install Rust via https://rustup.rs");
capabilities.push(capability(Capability::BuildFromSource, vec![cargo]));
}
Report {
driver_version: crate::DRIVER_VERSION.to_string(),
compiler: compiler.clone(),
capabilities,
}
}
fn compile_report(compiler: &Compiler) -> CapabilityReport {
let mut rows = vec![Row {
label: "compiler".into(),
level: Level::Ok,
detail: "in-process".into(),
remedy: None,
}];
if matches!(compiler.origin, Some(Origin::Override)) {
let ver = compiler
.version
.map(|v| v.to_string())
.unwrap_or_else(|| "unknown".into());
let row = match (&compiler.path, compiler.skew) {
(None, _) => Row {
label: "bynkc (override)".into(),
level: Level::Fail,
detail: "$BYNK_BYNKC set but not found".into(),
remedy: Some("fix BYNK_BYNKC, or unset it to use the in-process compiler".into()),
},
(Some(_), Some(Skew::Major)) => Row {
label: "bynkc (override)".into(),
level: Level::Fail,
detail: format!("{ver} — major skew vs driver"),
remedy: Some("align the override bynkc with bynk, or unset BYNK_BYNKC".into()),
},
(Some(_), Some(Skew::Minor)) => Row {
label: "bynkc (override)".into(),
level: Level::Warn,
detail: format!("{ver} — minor skew vs driver"),
remedy: Some("align the override bynkc with bynk, or unset BYNK_BYNKC".into()),
},
(Some(_), _) => Row {
label: "bynkc (override)".into(),
level: Level::Ok,
detail: format!("{ver} (override)"),
remedy: None,
},
};
rows.push(row);
}
let level = rows.iter().map(|r| r.level).max().unwrap_or(Level::Ok);
CapabilityReport {
capability: Capability::Compile,
optional: false,
rows,
level,
}
}
fn capability(cap: Capability, rows: Vec<Row>) -> CapabilityReport {
let level = rows.iter().map(|r| r.level).max().unwrap_or(Level::Ok);
CapabilityReport {
capability: cap,
optional: cap.is_optional(),
rows,
level,
}
}
fn detect_node(tb: &dyn Toolbox, root: Option<&std::path::Path>, floor: u32) -> Row {
let probe = probe::detect(
tb,
"node",
DetectOpts {
project_root: root,
allow_npx: false,
},
);
let remedy = format!("install Node.js ≥ {floor} from https://nodejs.org");
if probe.is_missing() {
return Row {
label: "node".into(),
level: Level::Fail,
detail: "missing".into(),
remedy: Some(remedy),
};
}
let below = probe.version.map(|v| v.major < floor).unwrap_or(false);
if below {
let v = probe.version.unwrap();
return Row {
label: "node".into(),
level: Level::Warn,
detail: format!("v{v} below floor (≥ {floor})"),
remedy: Some(remedy),
};
}
Row {
label: "node".into(),
level: Level::Ok,
detail: present_detail(&probe),
remedy: None,
}
}
fn detect_runner(tb: &dyn Toolbox, root: Option<&std::path::Path>) -> Row {
let tsc = probe::detect(
tb,
"tsc",
DetectOpts {
project_root: root,
allow_npx: true,
},
);
let tsx = probe::detect(
tb,
"tsx",
DetectOpts {
project_root: root,
allow_npx: true,
},
);
let best = pick_better(&tsc, &tsx);
let remedy = "npm install -g tsx (or: npm install -g typescript)".to_string();
match best {
Some(p) if p.is_present() => Row {
label: "tsc | tsx".into(),
level: Level::Ok,
detail: format!("{} {}", p.tool, present_detail(p)),
remedy: None,
},
Some(p) => Row {
label: "tsc | tsx".into(),
level: Level::Warn,
detail: format!("{} provisionable via npx (not installed)", p.tool),
remedy: Some(remedy),
},
None => Row {
label: "tsc | tsx".into(),
level: Level::Fail,
detail: "missing".into(),
remedy: Some(remedy),
},
}
}
fn detect_npm_tool(
tb: &dyn Toolbox,
root: Option<&std::path::Path>,
tool: &str,
remedy: &str,
) -> Row {
let probe = probe::detect(
tb,
tool,
DetectOpts {
project_root: root,
allow_npx: true,
},
);
npm_row(tool, &probe, remedy)
}
fn detect_plain(tb: &dyn Toolbox, tool: &str, remedy: &str) -> Row {
let probe = probe::detect(
tb,
tool,
DetectOpts {
project_root: None,
allow_npx: false,
},
);
if probe.is_present() {
Row {
label: tool.into(),
level: Level::Ok,
detail: present_detail(&probe),
remedy: None,
}
} else {
Row {
label: tool.into(),
level: Level::Fail,
detail: "missing".into(),
remedy: Some(remedy.into()),
}
}
}
fn npm_row(tool: &str, probe: &Probe, remedy: &str) -> Row {
if probe.is_present() {
Row {
label: tool.into(),
level: Level::Ok,
detail: present_detail(probe),
remedy: None,
}
} else if probe.is_provisionable() {
Row {
label: tool.into(),
level: Level::Warn,
detail: "provisionable via npx (not installed)".into(),
remedy: Some(remedy.into()),
}
} else {
Row {
label: tool.into(),
level: Level::Fail,
detail: "missing".into(),
remedy: Some(remedy.into()),
}
}
}
fn pick_better<'a>(a: &'a Probe, b: &'a Probe) -> Option<&'a Probe> {
fn rank(p: &Probe) -> u8 {
if p.is_present() {
2
} else if p.is_provisionable() {
1
} else {
0
}
}
let (ra, rb) = (rank(a), rank(b));
if ra == 0 && rb == 0 {
None
} else if ra >= rb {
Some(a)
} else {
Some(b)
}
}
fn present_detail(probe: &Probe) -> String {
let ver = probe
.version
.map(|v| format!("v{v}"))
.unwrap_or_else(|| "installed".into());
format!("{ver} ({})", probe.provenance.token())
}