use crate::{Config, PrompterError, load_config_bundle};
use serde_json::json;
use std::path::PathBuf;
use tftio_cli_common::{DoctorCheck, DoctorChecks, DoctorReport, JsonOutput, RepoInfo};
pub struct PrompterDoctor;
impl DoctorChecks for PrompterDoctor {
fn repo_info() -> RepoInfo {
RepoInfo::new("tftio-stuff", "tools")
}
fn current_version() -> &'static str {
env!("CARGO_PKG_VERSION")
}
fn tool_checks(&self) -> Vec<DoctorCheck> {
let state = collect_doctor_state();
let mut checks = Vec::new();
if state.config_file_exists {
checks.push(DoctorCheck::pass(format!(
"Config file: {}",
state.config_path.display()
)));
} else {
checks.push(DoctorCheck::fail(
"Config file",
format!("Config file not found: {}", state.config_path.display()),
));
}
match &state.bundle_result {
Some(Ok(_)) => {
checks.push(DoctorCheck::pass("Config bundle loads (TOML + imports)"));
}
Some(Err(e)) => {
checks.push(DoctorCheck::fail(
"Config bundle loads (TOML + imports)",
format!("Bundle load failed: {e}"),
));
}
None => {}
}
if state.library_roots.is_empty() {
checks.push(DoctorCheck::fail(
"Library directory",
format!(
"Library directory not found: {}",
state.default_library_path.display()
),
));
} else {
for root in &state.library_roots {
if root.exists() {
checks.push(DoctorCheck::pass(format!(
"Library directory: {}",
root.display()
)));
} else {
checks.push(DoctorCheck::fail(
"Library directory",
format!("Library directory not found: {}", root.display()),
));
}
}
}
checks
}
}
struct DoctorState {
config_path: PathBuf,
default_library_path: PathBuf,
config_file_exists: bool,
bundle_result: Option<Result<Config, PrompterError>>,
library_roots: Vec<PathBuf>,
errors: Vec<String>,
warnings: Vec<String>,
}
fn collect_doctor_state() -> DoctorState {
let home = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("~"));
let config_path = home.join(".config/prompter/config.toml");
let default_library_path = home.join(".local/prompter/library");
let config_file_exists = config_path.exists();
let mut errors = Vec::new();
let warnings = Vec::new();
let (bundle_result, library_roots) = if config_file_exists {
match load_config_bundle(&config_path, Some(&default_library_path)) {
Ok(cfg) => {
let roots = cfg.library_roots();
for root in &roots {
if !root.exists() {
errors.push(format!("Library directory not found: {}", root.display()));
}
}
(Some(Ok(cfg)), roots)
}
Err(e) => {
errors.push(format!("Bundle load failed: {e}"));
(Some(Err(e)), Vec::new())
}
}
} else {
errors.push(format!("Config file not found: {}", config_path.display()));
(None, Vec::new())
};
DoctorState {
config_path,
default_library_path,
config_file_exists,
bundle_result,
library_roots,
errors,
warnings,
}
}
fn build_doctor_report() -> DoctorReport {
let state = collect_doctor_state();
let bundle_loads = matches!(state.bundle_result, Some(Ok(_)));
let library_directory_exists =
!state.library_roots.is_empty() && state.library_roots.iter().all(|p| p.exists());
let mut report = DoctorReport::for_tool(&PrompterDoctor)
.with_detail("config_file_exists", json!(state.config_file_exists))
.with_detail("bundle_loads", json!(bundle_loads))
.with_detail("library_directory_exists", json!(library_directory_exists))
.with_detail(
"library_roots",
json!(
state
.library_roots
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
),
);
for error in state.errors {
report = report.with_error(error);
}
for warning in state.warnings {
report = report.with_warning(warning);
}
report
.with_info(format!("Current version: v{}", env!("CARGO_PKG_VERSION")))
.with_info("Check https://github.com/tftio/prompter/releases for updates")
}
pub fn run_doctor(output: JsonOutput) -> i32 {
let report = build_doctor_report();
report.emit_output(output)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_run_doctor_returns_valid_exit_code() {
let exit_code = run_doctor(JsonOutput::Text);
assert!(exit_code == 0 || exit_code == 1);
}
#[test]
fn test_run_doctor_json_returns_valid_exit_code() {
let exit_code = run_doctor(JsonOutput::Json);
assert!(exit_code == 0 || exit_code == 1);
}
}