tftio-prompter 4.0.0

A CLI tool for composing reusable prompt snippets from a library using TOML profiles
Documentation
//! Health check and diagnostics module.

use crate::{Config, PrompterError, load_config_bundle};
use serde_json::json;
use std::path::PathBuf;
use tftio_cli_common::{DoctorCheck, DoctorChecks, DoctorReport, JsonOutput, RepoInfo};

/// Doctor checks provider for the prompter tool.
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")
}

/// Run doctor command to check health and configuration.
///
/// Returns exit code: 0 if healthy, 1 if issues found.
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);
    }
}