codexctl 0.8.0

Codex Controller - Full control plane for Codex CLI
use crate::utils::config::Config;
use anyhow::Result;
use colored::Colorize as _;
use prettytable::{Cell, Row, Table, format};

async fn run_health_checks(config: &Config) -> Vec<(String, String, bool, String)> {
    let mut checks: Vec<(String, String, bool, String)> = Vec::new();
    let codex_installed = which::which("codex").is_ok();
    checks.push((
        "`Codex` CLI".to_string(),
        if codex_installed {
            "✓ Installed".to_string()
        } else {
            "✗ Not found".to_string()
        },
        codex_installed,
        if codex_installed {
            "none".to_string()
        } else {
            "Install: npm install -g @openai/codex then npm install -g codexctl".to_string()
        },
    ));
    let codex_dir = config.codex_dir();
    let codex_dir_exists = codex_dir.exists();
    checks.push((
        "`Codex` Directory".to_string(),
        if codex_dir_exists {
            "✓ Exists".to_string()
        } else {
            "✗ Missing".to_string()
        },
        codex_dir_exists,
        if codex_dir_exists {
            "none".to_string()
        } else {
            format!("Directory: {}", codex_dir.display())
        },
    ));
    let auth_file = codex_dir.join("auth.json");
    let auth_exists = auth_file.exists();
    checks.push((
        "Auth File".to_string(),
        if auth_exists {
            "✓ Exists".to_string()
        } else {
            "✗ Missing".to_string()
        },
        auth_exists,
        if auth_exists {
            "none".to_string()
        } else {
            "Run: codex login".to_string()
        },
    ));
    let mut auth_valid = false;
    let mut auth_error = String::new();
    if auth_exists {
        match tokio::fs::read_to_string(&auth_file).await {
            Ok(content) => match serde_json::from_str::<serde_json::Value>(&content) {
                Ok(_) => auth_valid = true,
                Err(e) => auth_error = format!("Invalid JSON: {e}"),
            },
            Err(e) => auth_error = format!("Cannot read: {e}"),
        }
    }
    checks.push((
        "Auth Valid".to_string(),
        if auth_valid {
            "✓ Valid".to_string()
        } else {
            "✗ Invalid".to_string()
        },
        auth_valid,
        if auth_valid {
            "none".to_string()
        } else {
            auth_error
        },
    ));
    let profiles_dir = config.profiles_dir();
    let profiles_dir_exists = profiles_dir.exists();
    checks.push((
        "Profiles Directory".to_string(),
        if profiles_dir_exists {
            "✓ Exists".to_string()
        } else {
            "✗ Missing".to_string()
        },
        profiles_dir_exists,
        if profiles_dir_exists {
            "none".to_string()
        } else {
            format!("Will be created at: {}", profiles_dir.display())
        },
    ));
    let mut profile_count = 0;
    if profiles_dir_exists && let Ok(mut entries) = tokio::fs::read_dir(&profiles_dir).await {
        while let Ok(Some(entry)) = entries.next_entry().await {
            if entry.path().is_dir() {
                let name = entry.file_name();
                let name = name.to_string_lossy();
                if name == "backups" || name.starts_with('.') {
                    continue;
                }
                profile_count += 1;
            }
        }
    }
    let has_profiles = profile_count > 0;
    checks.push((
        "Saved Profiles".to_string(),
        format!("{profile_count} profile(s)"),
        has_profiles,
        if has_profiles {
            "none".to_string()
        } else {
            "Create one with: codexctl save <name>".to_string()
        },
    ));
    checks
}

pub async fn execute(config: Config, quiet: bool) -> Result<()> {
    if !quiet {
        println!("\n{} Running health check...\n", "🏥".cyan());
    }
    let checks = run_health_checks(&config).await;
    let mut issues = 0;
    for (_, _, ok, _) in &checks {
        if !ok {
            issues += 1;
        }
    }
    if !quiet {
        let mut table = Table::new();
        table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
        table.add_row(Row::new(vec![
            Cell::new("Check").style_spec("Fb"),
            Cell::new("Status").style_spec("Fb"),
            Cell::new("Fix").style_spec("Fb"),
        ]));
        for (check, status, _, fix) in &checks {
            let status_cell = if status.starts_with('') {
                Cell::new(status).style_spec("Fg")
            } else {
                Cell::new(status).style_spec("Fr")
            };
            let fix_str = if fix == "none" {
                "-".to_string()
            } else {
                fix.clone()
            };
            table.add_row(Row::new(vec![
                Cell::new(check),
                status_cell,
                Cell::new(&fix_str),
            ]));
        }
        table.printstd();
        println!();
        if issues == 0 {
            println!(
                "{} All checks passed! System is healthy.",
                "".green().bold()
            );
        } else {
            println!(
                "{} {issues} issue(s) found. See 'Fix' column above.",
                "!".yellow().bold()
            );
        }
    }
    if issues > 0 {
        anyhow::bail!("{issues} issue(s) found. See 'Fix' column above.");
    }
    Ok(())
}