use std::path::Path;
use std::process::Command;
use miette::Diagnostic;
use thiserror::Error;
use crate::config::FusibleConfig;
use crate::tscircuit::detect::find_binary_in_path;
use crate::tscircuit::Runner;
#[derive(Debug, Error, Diagnostic)]
pub enum DoctorError {
#[error("failed to run package manager: {command}")]
#[diagnostic(
code(fusible::doctor::install_failed),
help(
"verify your internet connection or install manually with `npm install -g tscircuit`"
)
)]
InstallFailed {
command: String,
#[source]
source: std::io::Error,
},
#[error("package manager command `{command}` exited with failure ({code:?})")]
#[diagnostic(
code(fusible::doctor::install_non_zero),
help("check stderr output for permission or network issues")
)]
InstallNonZero { command: String, code: Option<i32> },
#[error("no supported package manager found to install tscircuit")]
#[diagnostic(
code(fusible::doctor::no_package_manager),
help("install bun (https://bun.sh) or node/npm (https://nodejs.org) first")
)]
NoPackageManager,
}
fn report_prerequisites() -> (bool, bool) {
let path_var = std::env::var_os("PATH");
let has_bun = path_var
.as_deref()
.and_then(|p| find_binary_in_path("bun", p));
let has_node = path_var
.as_deref()
.and_then(|p| find_binary_in_path("node", p));
let has_npm = path_var
.as_deref()
.and_then(|p| find_binary_in_path("npm", p));
let has_tsci = path_var.as_deref().and_then(|p| {
find_binary_in_path("tsci", p).or_else(|| find_binary_in_path("tscircuit", p))
});
println!("Runtime prerequisites:");
print_status("tsci (tscircuit)", has_tsci.as_deref());
print_status("bun", has_bun.as_deref());
print_status("node", has_node.as_deref());
print_status("npm", has_npm.as_deref());
(has_bun.is_some(), has_npm.is_some())
}
fn report_manifest() {
println!("\nMonorepo configuration:");
match FusibleConfig::find_and_load(Path::new(".")) {
Ok(Some((path, cfg))) => {
let mcu = cfg
.mcu
.and_then(|m| m.chip)
.unwrap_or_else(|| "none".into());
let entry = cfg
.hardware
.and_then(|h| h.entry)
.map(|e| e.display().to_string())
.unwrap_or_else(|| "default".into());
println!(" ✅ Found manifest at \"{}\"", path.display());
println!(" • entry: {entry}");
println!(" • silicon target: {mcu}");
}
Ok(None) => {
println!(" ℹ️ No fusible.yml found in current directory (run `fusible init` to create one)");
}
Err(e) => {
println!(" ⚠️ Manifest error: {e}");
}
}
}
fn report_runner(install: bool, has_bun: bool, has_npm: bool) -> Result<(), DoctorError> {
println!("\ntscircuit runner status:");
match Runner::detect() {
Ok(runner) => {
println!(
" ✅ Ready — active backend: {}",
runner.kind().description()
);
}
Err(_) => {
println!(" ⚠️ No active runner detected.");
println!(
" Fusible can still lint pre-existing circuit.json files, but TSX compilation,"
);
println!(" dev webview, and exports require tscircuit.");
if install {
println!("\n🔧 Installing tscircuit CLI...");
install_tscircuit(has_bun, has_npm)?;
println!("✅ tscircuit installed successfully!");
} else {
println!(
"\nTip: Run `fusible doctor --install` to automatically install tscircuit,"
);
println!(" or run: `npm install -g tscircuit` / `bun add -g tscircuit`");
}
}
}
Ok(())
}
pub fn run(install: bool) -> Result<(), DoctorError> {
println!("🩺 Fusible Toolchain Doctor\n");
let (has_bun, has_npm) = report_prerequisites();
report_manifest();
report_runner(install, has_bun, has_npm)?;
println!();
Ok(())
}
fn print_status(name: &str, path: Option<&Path>) {
match path {
Some(p) => println!(" ✅ {name:<14} -> {}", p.display()),
None => println!(" ⚪ {name:<14} -> not found"),
}
}
fn install_tscircuit(has_bun: bool, has_npm: bool) -> Result<(), DoctorError> {
let (prog, args): (&str, &[&str]) = if has_bun {
("bun", &["add", "-g", "tscircuit"])
} else if has_npm {
("npm", &["install", "-g", "tscircuit"])
} else {
return Err(DoctorError::NoPackageManager);
};
println!(" Executing: {prog} {}", args.join(" "));
let status =
Command::new(prog)
.args(args)
.status()
.map_err(|source| DoctorError::InstallFailed {
command: format!("{prog} {}", args.join(" ")),
source,
})?;
if !status.success() {
return Err(DoctorError::InstallNonZero {
command: format!("{prog} {}", args.join(" ")),
code: status.code(),
});
}
Ok(())
}