codexctl 0.9.0

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

#[allow(clippy::too_many_lines)]
pub async fn execute(
    config: Config,
    profile1: String,
    profile2: String,
    changes_only: bool,
    quiet: bool,
) -> Result<()> {
    let name1 = ProfileName::try_from(profile1.as_str())
        .with_context(|| format!("Invalid profile name '{profile1}'"))?;
    let name2 = ProfileName::try_from(profile2.as_str())
        .with_context(|| format!("Invalid profile name '{profile2}'"))?;
    let dir1 = config.profile_path_validated(&name1)?;
    let dir2 = config.profile_path_validated(&name2)?;

    if !dir1.exists() {
        anyhow::bail!("Profile '{profile1}' not found");
    }
    if !dir2.exists() {
        anyhow::bail!("Profile '{profile2}' not found");
    }

    if !quiet {
        println!(
            "\n{} Comparing profiles: {} vs {}",
            "🔍".cyan(),
            profile1.green(),
            profile2.yellow()
        );
    }

    // Get file listings for both profiles
    let files1 = get_profile_files(&dir1).await?;
    let files2 = get_profile_files(&dir2).await?;

    // Compare files
    let mut table = Table::new();
    table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);

    // Header
    table.add_row(Row::new(vec![
        Cell::new("File").style_spec("Fb"),
        Cell::new(&profile1.clone()).style_spec("Fg"),
        Cell::new(&profile2.clone()).style_spec("Fy"),
        Cell::new("Status").style_spec("Fb"),
    ]));

    // Get all unique files
    let mut all_files: Vec<String> = files1.keys().cloned().collect();
    for file in files2.keys() {
        if !all_files.contains(file) {
            all_files.push(file.clone());
        }
    }
    all_files.sort();

    let mut differences = 0;

    for file in &all_files {
        let (content1, content2) = (
            files1.get(file).map(Vec::as_slice),
            files2.get(file).map(Vec::as_slice),
        );

        let status = match (content1, content2) {
            (Some(c1), Some(c2)) => {
                if c1 == c2 {
                    if changes_only {
                        continue;
                    }
                    "✓ Same".to_string()
                } else {
                    differences += 1;
                    format!("{} Different", "".yellow())
                }
            }
            (Some(_), None) => {
                differences += 1;
                format!("{} Only in {}", "".green(), profile1)
            }
            (None, Some(_)) => {
                differences += 1;
                format!("{} Only in {}", "".yellow(), profile2)
            }
            (None, None) => unreachable!(),
        };

        let size1 = content1.map_or_else(|| "-".to_string(), |c| format!("{} bytes", c.len()));
        let size2 = content2.map_or_else(|| "-".to_string(), |c| format!("{} bytes", c.len()));

        table.add_row(Row::new(vec![
            Cell::new(file),
            Cell::new(&size1),
            Cell::new(&size2),
            Cell::new(&status),
        ]));
    }

    if !all_files.is_empty() {
        table.printstd();
    }

    if !quiet {
        println!();
        if differences == 0 {
            println!("{} Profiles are identical!", "".green());
        } else {
            println!("{} {} difference(s) found", "!".yellow(), differences);
        }
    }

    Ok(())
}

async fn get_profile_files(dir: &std::path::Path) -> Result<HashMap<String, Vec<u8>>> {
    let mut files = HashMap::new();

    let mut entries = tokio::fs::read_dir(dir).await?;
    while let Some(entry) = entries.next_entry().await? {
        let path = entry.path();
        if path.is_file() {
            let name = path
                .file_name()
                .map(|s| s.to_string_lossy().to_string())
                .unwrap_or_default();
            let content = tokio::fs::read(&path).await?;
            files.insert(name, content);
        }
    }

    Ok(files)
}