tftio-clanker 0.2.0

Launch AI harnesses with runtime-configured context, domains, models, and prompts
Documentation
//! Runtime-configuration doctor integration.

use std::path::PathBuf;

use tftio_cli_common::{DoctorCheck, DoctorChecks, RepoInfo};

use crate::config::load_config;

/// Doctor checks bound to one runtime configuration path.
pub struct Doctor {
    config_path: Option<PathBuf>,
    current_dir: PathBuf,
    home: Option<PathBuf>,
}

impl Doctor {
    /// Create doctor checks for an optional resolved config path.
    #[must_use]
    pub const fn new(
        config_path: Option<PathBuf>,
        current_dir: PathBuf,
        home: Option<PathBuf>,
    ) -> Self {
        Self {
            config_path,
            current_dir,
            home,
        }
    }
}

impl DoctorChecks for Doctor {
    fn repo_info() -> RepoInfo {
        RepoInfo::new("tftio-stuff", "clanker")
    }

    fn current_version() -> &'static str {
        env!("CARGO_PKG_VERSION")
    }

    fn tool_checks(&self) -> Vec<DoctorCheck> {
        let mut checks = self.config_path.as_ref().map_or_else(
            || {
                vec![DoctorCheck::fail(
                    "Clanker config",
                    "$HOME is not set and CLANKER_CONFIG was not provided",
                )]
            },
            |path| match load_config(path) {
                Ok(loaded) => {
                    let mut checks = vec![DoctorCheck::pass(format!(
                        "Config file: {}",
                        loaded.base_path.display()
                    ))];
                    if let Some(overlay) = loaded.overlay_path {
                        checks.push(DoctorCheck::pass(format!(
                            "Local overlay: {}",
                            overlay.display()
                        )));
                    }
                    checks.push(DoctorCheck::pass("Runtime config schema and invariants"));
                    if let Some(home) = &self.home {
                        let filesystem_issues = loaded.config.filesystem_issues(home);
                        if filesystem_issues.is_empty() {
                            checks.push(DoctorCheck::pass("Skill bundle and configured skills"));
                        } else {
                            checks.extend(
                                filesystem_issues.into_iter().map(|issue| {
                                    DoctorCheck::fail("Skill bundle", issue.to_string())
                                }),
                            );
                        }
                    } else {
                        checks.push(DoctorCheck::fail(
                            "Skill bundle",
                            "$HOME is not set; cannot resolve defaults.skills_bundle",
                        ));
                    }
                    checks
                }
                Err(error) => vec![DoctorCheck::fail("Clanker config", error.to_string())],
            },
        );

        let legacy_context = self.current_dir.join(".context");
        if legacy_context.exists() {
            checks.push(DoctorCheck::fail(
                "Legacy context file",
                format!(
                    "{} exists; replace it with .clanker TOML",
                    legacy_context.display()
                ),
            ));
        } else {
            checks.push(DoctorCheck::pass("No legacy .context file in cwd"));
        }
        checks
    }
}