aicx 0.6.6

Operator CLI + MCP server: canonical corpus first, optional semantic index second (Claude Code, Codex, Gemini)
Documentation
use std::process::Command;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SeverityLabel {
    Green,
    Warning,
    Critical,
    Unknown,
}

#[derive(Debug, Clone)]
pub struct DoctorCard {
    pub name: String,
    pub severity: SeverityLabel,
    pub detail: String,
    pub recommendation: Option<String>,
}

#[derive(Debug, Default)]
pub struct DoctorScreen {
    pub cards: Vec<DoctorCard>,
    pub selected: usize,
    pub loaded: bool,
    pub fixes_applied: Vec<String>,
    pub status: String,
}

impl DoctorScreen {
    pub fn refresh(&mut self, fix: bool) {
        self.run_command(if fix {
            &["doctor", "--fix"]
        } else {
            &["doctor"]
        });
    }

    pub fn fix_buckets(&mut self) {
        self.run_command(&["doctor", "--fix-buckets"]);
    }

    pub fn move_selection(&mut self, delta: isize) {
        if self.cards.is_empty() {
            return;
        }
        if delta < 0 {
            self.selected = self.selected.saturating_sub(delta.unsigned_abs());
        } else {
            self.selected = self
                .selected
                .saturating_add(delta as usize)
                .min(self.cards.len() - 1);
        }
    }

    fn run_command(&mut self, args: &[&str]) {
        let Ok(exe) = std::env::current_exe() else {
            self.status = "failed to resolve current aicx executable".to_string();
            self.loaded = true;
            return;
        };

        let output = Command::new(exe)
            .args(args)
            .arg("--format")
            .arg("json")
            .output();
        self.loaded = true;

        let output = match output {
            Ok(output) => output,
            Err(error) => {
                self.status = format!("doctor failed to spawn: {error}");
                return;
            }
        };

        if !output.status.success() && output.stdout.is_empty() {
            self.status = format!(
                "doctor failed: {}",
                String::from_utf8_lossy(&output.stderr).trim()
            );
            return;
        }

        let parsed = serde_json::from_slice::<serde_json::Value>(&output.stdout);
        let Ok(json) = parsed else {
            self.status = format!(
                "doctor output was not json: {}",
                String::from_utf8_lossy(&output.stderr).trim()
            );
            return;
        };

        self.cards = [
            "canonical_store",
            "steer_lance",
            "steer_bm25",
            "state",
            "sidecars",
            "corpus_buckets",
        ]
        .into_iter()
        .filter_map(|key| json.get(key).map(card_from_json))
        .collect();
        self.fixes_applied = json
            .get("fixes_applied")
            .and_then(|value| value.as_array())
            .map(|items| {
                items
                    .iter()
                    .filter_map(|item| item.as_str().map(ToOwned::to_owned))
                    .collect()
            })
            .unwrap_or_default();
        self.selected = self.selected.min(self.cards.len().saturating_sub(1));
        self.status = format!(
            "doctor overall: {}",
            json.get("overall")
                .and_then(|value| value.as_str())
                .unwrap_or("unknown")
        );
    }
}

fn card_from_json(value: &serde_json::Value) -> DoctorCard {
    DoctorCard {
        name: value
            .get("name")
            .and_then(|field| field.as_str())
            .unwrap_or("unknown")
            .to_string(),
        severity: match value
            .get("severity")
            .and_then(|field| field.as_str())
            .unwrap_or("unknown")
        {
            "green" => SeverityLabel::Green,
            "warning" => SeverityLabel::Warning,
            "critical" => SeverityLabel::Critical,
            _ => SeverityLabel::Unknown,
        },
        detail: value
            .get("detail")
            .and_then(|field| field.as_str())
            .unwrap_or("")
            .to_string(),
        recommendation: value
            .get("recommendation")
            .and_then(|field| field.as_str())
            .map(ToOwned::to_owned),
    }
}