1use std::path::PathBuf;
17
18use crate::compiler::{Compiler, Origin, Skew};
19use crate::probe::{self, DetectOpts, Probe, Toolbox};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum Capability {
24 Compile,
27 Test,
29 Deploy,
31 Editor,
33 BuildFromSource,
36}
37
38impl Capability {
39 pub fn token(self) -> &'static str {
40 match self {
41 Capability::Compile => "compile",
42 Capability::Test => "test",
43 Capability::Deploy => "deploy",
44 Capability::Editor => "editor",
45 Capability::BuildFromSource => "build",
46 }
47 }
48
49 pub fn is_optional(self) -> bool {
52 matches!(self, Capability::Editor | Capability::BuildFromSource)
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
58pub enum Level {
59 Ok,
60 Warn,
61 Fail,
62}
63
64#[derive(Debug, Clone)]
67pub struct Row {
68 pub label: String,
69 pub level: Level,
70 pub detail: String,
71 pub remedy: Option<String>,
72}
73
74#[derive(Debug, Clone)]
76pub struct CapabilityReport {
77 pub capability: Capability,
78 pub optional: bool,
79 pub rows: Vec<Row>,
80 pub level: Level,
81}
82
83#[derive(Debug, Clone)]
85pub struct Report {
86 pub driver_version: String,
87 pub compiler: Compiler,
88 pub capabilities: Vec<CapabilityReport>,
89}
90
91#[derive(Debug, Clone, Default)]
93pub struct DoctorOptions {
94 pub only: Option<Capability>,
96 pub strict: bool,
98}
99
100#[derive(Debug, Clone)]
103pub struct Context {
104 pub project_root: Option<PathBuf>,
106 pub in_repo: bool,
108 pub node_floor: u32,
110}
111
112impl Report {
113 pub fn exit_nonzero(&self, opts: &DoctorOptions) -> bool {
119 for cap in &self.capabilities {
120 let required =
121 cap.capability == Capability::Compile || opts.only == Some(cap.capability);
122 if required && cap.level == Level::Fail {
123 return true;
124 }
125 }
126 if opts.strict && self.capabilities.iter().any(|c| c.level != Level::Ok) {
127 return true;
128 }
129 false
130 }
131
132 pub fn is_all_ok(&self) -> bool {
134 self.capabilities.iter().all(|c| c.level == Level::Ok)
135 }
136}
137
138pub fn diagnose(
140 tb: &dyn Toolbox,
141 compiler: &Compiler,
142 ctx: &Context,
143 opts: &DoctorOptions,
144) -> Report {
145 let root = ctx.project_root.as_deref();
146 let mut capabilities = vec![compile_report(compiler)];
147
148 let want = |cap: Capability| opts.only.is_none() || opts.only == Some(cap);
152
153 if want(Capability::Test) {
154 let node = detect_node(tb, root, ctx.node_floor);
155 let runner = detect_runner(tb, root);
156 capabilities.push(capability(Capability::Test, vec![node, runner]));
157 }
158 if want(Capability::Deploy) {
159 let node = detect_node(tb, root, ctx.node_floor);
160 let wrangler = detect_npm_tool(tb, root, "wrangler", "npm install -g wrangler");
161 capabilities.push(capability(Capability::Deploy, vec![node, wrangler]));
162 }
163 if want(Capability::Editor) {
164 let lsp = detect_plain(
165 tb,
166 "bynkc-lsp",
167 "install bynkc-lsp (or download from releases)",
168 );
169 capabilities.push(capability(Capability::Editor, vec![lsp]));
170 }
171 if ctx.in_repo && want(Capability::BuildFromSource) {
172 let cargo = detect_plain(tb, "cargo", "install Rust via https://rustup.rs");
173 capabilities.push(capability(Capability::BuildFromSource, vec![cargo]));
174 }
175
176 Report {
177 driver_version: crate::DRIVER_VERSION.to_string(),
178 compiler: compiler.clone(),
179 capabilities,
180 }
181}
182
183fn compile_report(compiler: &Compiler) -> CapabilityReport {
190 let mut rows = vec![Row {
191 label: "compiler".into(),
192 level: Level::Ok,
193 detail: "in-process".into(),
194 remedy: None,
195 }];
196
197 if matches!(compiler.origin, Some(Origin::Override)) {
200 let ver = compiler
201 .version
202 .map(|v| v.to_string())
203 .unwrap_or_else(|| "unknown".into());
204 let row = match (&compiler.path, compiler.skew) {
205 (None, _) => Row {
206 label: "bynkc (override)".into(),
207 level: Level::Fail,
208 detail: "$BYNK_BYNKC set but not found".into(),
209 remedy: Some("fix BYNK_BYNKC, or unset it to use the in-process compiler".into()),
210 },
211 (Some(_), Some(Skew::Major)) => Row {
212 label: "bynkc (override)".into(),
213 level: Level::Fail,
214 detail: format!("{ver} — major skew vs driver"),
215 remedy: Some("align the override bynkc with bynk, or unset BYNK_BYNKC".into()),
216 },
217 (Some(_), Some(Skew::Minor)) => Row {
218 label: "bynkc (override)".into(),
219 level: Level::Warn,
220 detail: format!("{ver} — minor skew vs driver"),
221 remedy: Some("align the override bynkc with bynk, or unset BYNK_BYNKC".into()),
222 },
223 (Some(_), None) => Row {
226 label: "bynkc (override)".into(),
227 level: Level::Warn,
228 detail: "resolved, but its version could not be read".into(),
229 remedy: Some("check that BYNK_BYNKC points at a bynkc binary".into()),
230 },
231 (Some(_), _) => Row {
232 label: "bynkc (override)".into(),
233 level: Level::Ok,
234 detail: format!("{ver} (override)"),
235 remedy: None,
236 },
237 };
238 rows.push(row);
239 }
240
241 let level = rows.iter().map(|r| r.level).max().unwrap_or(Level::Ok);
242 CapabilityReport {
243 capability: Capability::Compile,
244 optional: false,
245 rows,
246 level,
247 }
248}
249
250fn capability(cap: Capability, rows: Vec<Row>) -> CapabilityReport {
252 let level = rows.iter().map(|r| r.level).max().unwrap_or(Level::Ok);
253 CapabilityReport {
254 capability: cap,
255 optional: cap.is_optional(),
256 rows,
257 level,
258 }
259}
260
261fn detect_node(tb: &dyn Toolbox, root: Option<&std::path::Path>, floor: u32) -> Row {
262 let probe = probe::detect(
264 tb,
265 "node",
266 DetectOpts {
267 project_root: root,
268 allow_npx: false,
269 },
270 );
271 let remedy = format!("install Node.js ≥ {floor} from https://nodejs.org");
272 if probe.is_missing() {
273 return Row {
274 label: "node".into(),
275 level: Level::Fail,
276 detail: "missing".into(),
277 remedy: Some(remedy),
278 };
279 }
280 let below = probe.version.map(|v| v.major < floor).unwrap_or(false);
281 if below {
282 let v = probe.version.unwrap();
283 return Row {
284 label: "node".into(),
285 level: Level::Warn,
286 detail: format!("v{v} below floor (≥ {floor})"),
287 remedy: Some(remedy),
288 };
289 }
290 Row {
291 label: "node".into(),
292 level: Level::Ok,
293 detail: present_detail(&probe),
294 remedy: None,
295 }
296}
297
298fn detect_runner(tb: &dyn Toolbox, root: Option<&std::path::Path>) -> Row {
300 let tsc = probe::detect(
301 tb,
302 "tsc",
303 DetectOpts {
304 project_root: root,
305 allow_npx: true,
306 },
307 );
308 let tsx = probe::detect(
309 tb,
310 "tsx",
311 DetectOpts {
312 project_root: root,
313 allow_npx: true,
314 },
315 );
316 let best = pick_better(&tsc, &tsx);
317 let remedy = "npm install -g tsx (or: npm install -g typescript)".to_string();
318 match best {
319 Some(p) if p.is_present() => Row {
320 label: "tsc | tsx".into(),
321 level: Level::Ok,
322 detail: format!("{} {}", p.tool, present_detail(p)),
323 remedy: None,
324 },
325 Some(p) => Row {
326 label: "tsc | tsx".into(),
328 level: Level::Warn,
329 detail: format!("{} provisionable via npx (not installed)", p.tool),
330 remedy: Some(remedy),
331 },
332 None => Row {
333 label: "tsc | tsx".into(),
334 level: Level::Fail,
335 detail: "missing".into(),
336 remedy: Some(remedy),
337 },
338 }
339}
340
341fn detect_npm_tool(
342 tb: &dyn Toolbox,
343 root: Option<&std::path::Path>,
344 tool: &str,
345 remedy: &str,
346) -> Row {
347 let probe = probe::detect(
348 tb,
349 tool,
350 DetectOpts {
351 project_root: root,
352 allow_npx: true,
353 },
354 );
355 npm_row(tool, &probe, remedy)
356}
357
358fn detect_plain(tb: &dyn Toolbox, tool: &str, remedy: &str) -> Row {
359 let probe = probe::detect(
360 tb,
361 tool,
362 DetectOpts {
363 project_root: None,
364 allow_npx: false,
365 },
366 );
367 if probe.is_present() {
368 Row {
369 label: tool.into(),
370 level: Level::Ok,
371 detail: present_detail(&probe),
372 remedy: None,
373 }
374 } else {
375 Row {
376 label: tool.into(),
377 level: Level::Fail,
378 detail: "missing".into(),
379 remedy: Some(remedy.into()),
380 }
381 }
382}
383
384fn npm_row(tool: &str, probe: &Probe, remedy: &str) -> Row {
385 if probe.is_present() {
386 Row {
387 label: tool.into(),
388 level: Level::Ok,
389 detail: present_detail(probe),
390 remedy: None,
391 }
392 } else if probe.is_provisionable() {
393 Row {
394 label: tool.into(),
395 level: Level::Warn,
396 detail: "provisionable via npx (not installed)".into(),
397 remedy: Some(remedy.into()),
398 }
399 } else {
400 Row {
401 label: tool.into(),
402 level: Level::Fail,
403 detail: "missing".into(),
404 remedy: Some(remedy.into()),
405 }
406 }
407}
408
409fn pick_better<'a>(a: &'a Probe, b: &'a Probe) -> Option<&'a Probe> {
412 fn rank(p: &Probe) -> u8 {
413 if p.is_present() {
414 2
415 } else if p.is_provisionable() {
416 1
417 } else {
418 0
419 }
420 }
421 let (ra, rb) = (rank(a), rank(b));
422 if ra == 0 && rb == 0 {
423 None
424 } else if ra >= rb {
425 Some(a)
426 } else {
427 Some(b)
428 }
429}
430
431fn present_detail(probe: &Probe) -> String {
432 let ver = probe
433 .version
434 .map(|v| format!("v{v}"))
435 .unwrap_or_else(|| "installed".into());
436 format!("{ver} ({})", probe.provenance.token())
437}