use crate::utils::auth::read_email_from_codex_dir;
use crate::utils::config::Config;
use crate::utils::files::{
copy_profile_files, get_critical_files, write_bytes_preserve_permissions,
};
use crate::utils::profile::ProfileMeta;
use crate::utils::validation::ProfileName;
use anyhow::{Context as _, Result};
use colored::Colorize as _;
use indicatif::{ProgressBar, ProgressStyle};
use std::time::Duration;
pub async fn execute(
config: Config,
name: String,
description: Option<String>,
force: bool,
quiet: bool,
passphrase: Option<String>,
) -> Result<()> {
let profile_name = ProfileName::try_from(name.as_str())
.with_context(|| format!("Invalid profile name '{name}'"))?;
let codex_dir = config.codex_dir();
let profile_dir = config.profile_path_validated(&profile_name)?;
if !codex_dir.exists() {
anyhow::bail!(
"Codex directory not found at {}. Is Codex CLI installed?",
codex_dir.display()
);
}
if profile_dir.exists() && !force {
let confirm = dialoguer::Confirm::new()
.with_prompt(format!(
"Profile '{}' already exists. Overwrite?",
name.yellow()
))
.default(false)
.interact()?;
if !confirm {
if !quiet {
println!("Cancelled");
}
return Ok(());
}
}
if profile_dir.exists() {
tokio::fs::remove_dir_all(&profile_dir)
.await
.with_context(|| format!("Failed to remove existing profile '{}'", name))?;
}
let pb = if quiet {
None
} else {
let bar = ProgressBar::new_spinner();
bar.set_style(
ProgressStyle::default_spinner()
.template("{spinner:.green} {msg}")
.expect("Valid template"),
);
bar.set_message("Saving profile...");
bar.enable_steady_tick(Duration::from_millis(100));
Some(bar)
};
let email = read_email_from_codex_dir(codex_dir).await;
let files_to_copy = get_critical_files();
let copied = copy_profile_files(codex_dir, &profile_dir, files_to_copy)
.with_context(|| "Failed to copy profile files")?;
let secret_passphrase = passphrase.filter(|p| !p.is_empty());
let is_encrypted = secret_passphrase.is_some();
if let Some(pass) = secret_passphrase.as_ref() {
let auth_path = profile_dir.join("auth.json");
if auth_path.exists() && auth_path.is_file() {
let auth_content = tokio::fs::read(&auth_path)
.await
.context("Failed to read auth.json for encryption")?;
let encrypted = crate::utils::crypto::encrypt(&auth_content, Some(pass))
.context("Failed to encrypt auth.json")?;
write_bytes_preserve_permissions(&auth_path, &encrypted)
.context("Failed to write encrypted auth.json")?;
}
}
let mut meta = ProfileMeta::new(name.clone(), email.clone(), description.clone());
meta.encrypted = is_encrypted;
meta.update();
let mut meta_json = serde_json::to_vec_pretty(&meta).context("Failed to serialize metadata")?;
meta_json.push(b'\n');
let meta_path = profile_dir.join("profile.json");
write_bytes_preserve_permissions(&meta_path, &meta_json)
.context("Failed to write profile metadata")?;
if let Some(bar) = pb {
bar.finish_and_clear();
}
if !quiet {
let encryption_status = if is_encrypted {
format!(" {}", "[encrypted]".cyan())
} else {
String::new()
};
println!(
"{} Profile {} saved successfully{}",
"✓".green().bold(),
name.cyan(),
encryption_status
);
if let Some(e) = email {
println!(" {}: {}", "Email".dimmed(), e);
}
println!(" {}: {}", "Location".dimmed(), profile_dir.display());
println!(" {}: {} files", "Files".dimmed(), copied.len());
}
Ok(())
}