codexctl 0.8.0

Codex Controller - Full control plane for Codex CLI
use crate::utils::config::Config;
use crate::utils::files::write_bytes_preserve_permissions;
use crate::utils::validation::ProfileName;
use anyhow::{Context as _, Result};
use colored::Colorize as _;
use std::io::ErrorKind;
use std::path::Path;
use std::process::Stdio;
use tokio::process::Command;

pub async fn execute(
    config: Config,
    profile: String,
    command: Vec<String>,
    quiet: bool,
) -> Result<()> {
    let profile_name = ProfileName::try_from(profile.as_str())
        .with_context(|| format!("Invalid profile name '{profile}'"))?;
    let profile_dir = config.profile_path_validated(&profile_name)?;
    let codex_dir = config.codex_dir();

    if !profile_dir.exists() {
        anyhow::bail!("Profile '{profile}' not found");
    }

    if command.is_empty() {
        anyhow::bail!("No command specified to run");
    }

    let profile_auth = load_profile_auth(&profile_dir, &profile).await?;
    let original_auth = apply_profile_auth(codex_dir, &profile_auth).await?;

    // Execute command
    let cmd = &command[0];
    let args = &command[1..];

    if !quiet {
        println!(
            "{} Running with profile {}: {}",
            "".cyan(),
            profile.green(),
            command.join(" ").dimmed()
        );
    }

    // Log to history
    let _ = crate::commands::history::log_command(&config, &profile, &command.join(" ")).await;

    let status_result = Command::new(cmd)
        .args(args)
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit())
        .stdin(Stdio::inherit())
        .status()
        .await
        .with_context(|| format!("Failed to execute command: {cmd}"));

    // Always restore original auth after command execution.
    if let Err(e) = restore_original_auth(codex_dir, original_auth).await
        && !quiet
    {
        eprintln!(
            "{} Warning: Could not fully restore original auth: {}",
            "".yellow(),
            e
        );
    }
    let status = status_result?;

    if !quiet {
        if status.success() {
            println!(
                "\n{} Command completed, restored original auth",
                "".green()
            );
        } else {
            println!(
                "\n{} Command exited with code {:?}",
                "!".yellow(),
                status.code()
            );
        }
    }

    Ok(())
}

async fn load_profile_auth(profile_dir: &Path, profile_name: &str) -> Result<Vec<u8>> {
    let profile_auth_path = profile_dir.join("auth.json");
    if !profile_auth_path.exists() {
        anyhow::bail!("Profile '{profile_name}' does not contain auth.json");
    }

    let profile_auth = tokio::fs::read(&profile_auth_path)
        .await
        .with_context(|| format!("Failed to read {}", profile_auth_path.display()))?;

    if crate::utils::crypto::is_encrypted(&profile_auth) {
        anyhow::bail!(
            "Profile '{profile_name}' is encrypted. Use 'codexctl load {profile_name} --passphrase ...' instead."
        );
    }

    Ok(profile_auth)
}

async fn apply_profile_auth(codex_dir: &Path, profile_auth: &[u8]) -> Result<Option<Vec<u8>>> {
    tokio::fs::create_dir_all(codex_dir)
        .await
        .with_context(|| format!("Failed to create codex directory: {}", codex_dir.display()))?;

    let auth_path = codex_dir.join("auth.json");
    let original_auth = match tokio::fs::read(&auth_path).await {
        Ok(content) => Some(content),
        Err(e) if e.kind() == ErrorKind::NotFound => None,
        Err(e) => {
            return Err(e).with_context(|| {
                format!("Failed to read existing auth file: {}", auth_path.display())
            });
        }
    };

    write_bytes_preserve_permissions(&auth_path, profile_auth)
        .with_context(|| format!("Failed to apply profile auth to {}", auth_path.display()))?;

    Ok(original_auth)
}

async fn restore_original_auth(codex_dir: &Path, original_auth: Option<Vec<u8>>) -> Result<()> {
    let auth_path = codex_dir.join("auth.json");
    match original_auth {
        Some(content) => write_bytes_preserve_permissions(&auth_path, &content)
            .with_context(|| format!("Failed to restore {}", auth_path.display()))?,
        None => match tokio::fs::remove_file(&auth_path).await {
            Ok(()) => {}
            Err(e) if e.kind() == ErrorKind::NotFound => {}
            Err(e) => {
                return Err(e).with_context(|| format!("Failed to remove {}", auth_path.display()));
            }
        },
    }

    Ok(())
}