use std::path::PathBuf;
use anyhow::Result;
use clap::{Parser, Subcommand, ValueHint};
use tracing::{debug, info};
mod commands;
mod utils;
use commands::{backup, delete, list, load, run, save, status};
use utils::config::Config;
#[derive(Parser, Debug, Clone)]
#[command(
name = "codexctl",
bin_name = "codexctl",
version,
about = "Codex Controller - Full control plane for Codex CLI",
long_about = None
)]
#[command(arg_required_else_help = true)]
pub struct Cli {
#[command(subcommand)]
command: Commands,
#[arg(short, long, global = true)]
verbose: bool,
#[arg(long, global = true, env = "CODEXCTL_DIR", value_hint = ValueHint::DirPath)]
config_dir: Option<PathBuf>,
#[arg(short, long, global = true, env = "CODEXCTL_QUIET")]
quiet: bool,
}
#[derive(Subcommand, Debug, Clone)]
#[allow(clippy::large_enum_variant)]
pub enum Commands {
#[command(alias = "s")]
Save {
name: String,
#[arg(short, long)]
description: Option<String>,
#[arg(short, long)]
force: bool,
#[arg(short = 'p', long, env = "CODEXCTL_PASSPHRASE")]
passphrase: Option<String>,
},
#[command(alias = "l")]
Load {
name: String,
#[arg(short, long)]
force: bool,
#[arg(long)]
dry_run: bool,
#[arg(short = 'p', long, env = "CODEXCTL_PASSPHRASE")]
passphrase: Option<String>,
},
#[command(alias = "ls")]
List {
#[arg(short, long)]
detailed: bool,
},
#[command(alias = "rm", alias = "remove")]
Delete {
name: String,
#[arg(short, long)]
force: bool,
},
#[command(alias = "st", alias = "current")]
Status {
#[arg(long)]
json: bool,
},
#[command(alias = "u")]
Usage {
#[arg(short, long)]
all: bool,
#[arg(short, long)]
realtime: bool,
#[arg(long)]
json: bool,
},
#[command(alias = "v")]
Verify {
#[arg(long)]
json: bool,
},
#[command(alias = "b")]
Backup {
#[arg(short, long)]
name: Option<String>,
},
#[command(alias = "r")]
Run {
#[arg(short, long)]
profile: String,
#[arg(short = 'P', long, env = "CODEXCTL_PASSPHRASE")]
passphrase: Option<String>,
#[arg(required = true)]
command: Vec<String>,
},
#[command(alias = "e")]
Env {
profile: String,
#[arg(short, long, default_value = "bash")]
shell: String,
#[arg(long)]
unset: bool,
},
#[command(alias = "d")]
Diff {
profile1: String,
profile2: String,
#[arg(short, long)]
changes_only: bool,
},
#[command(alias = "sw")]
Switch,
#[command(alias = "hist")]
History {
#[arg(short, long, default_value = "20")]
limit: usize,
#[arg(short, long)]
profile: Option<String>,
},
#[command(alias = "doc")]
Doctor {
#[arg(long)]
json: bool,
},
#[command(alias = "comp")]
Completions {
#[arg(value_enum)]
shell: ShellType,
#[arg(short, long)]
print: bool,
},
Import {
name: String,
data: String,
},
Export {
name: String,
},
#[command(alias = "init")]
Setup,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
pub enum ShellType {
Bash,
Zsh,
Fish,
PowerShell,
Elvish,
}
#[tokio::main]
#[allow(clippy::too_many_lines)]
async fn main() -> Result<()> {
let cli = Cli::parse();
tracing_subscriber::fmt()
.with_env_filter(
std::env::var("RUST_LOG")
.unwrap_or_else(|_| if cli.verbose { "debug" } else { "warn" }.into()),
)
.with_target(false)
.with_level(true)
.with_writer(std::io::stderr)
.init();
debug!("Starting codexctl");
info!("Config directory: {:?}", cli.config_dir);
let config = Config::new(cli.config_dir.clone())?;
if let Err(e) = crate::utils::migrate::auto_migrate(&config).await {
tracing::warn!("Auto-migration warning: {}", e);
}
match cli.command {
Commands::Save {
name,
description,
force,
passphrase,
} => {
save::execute(config, name, description, force, cli.quiet, passphrase).await?;
}
Commands::Load {
name,
force,
dry_run,
passphrase,
} => {
load::execute(config, name, force, dry_run, cli.quiet, passphrase).await?;
}
Commands::List { detailed } => {
list::execute(config, detailed, cli.quiet).await?;
}
Commands::Delete { name, force } => {
delete::execute(config, name, force, cli.quiet).await?;
}
Commands::Status { json } => {
status::execute(config, json, cli.quiet).await?;
}
Commands::Usage {
all,
realtime,
json,
} => {
commands::usage::execute(config, all, realtime, json, cli.quiet).await?;
}
Commands::Verify { json } => {
commands::verify::execute(config, json, cli.quiet).await?;
}
Commands::Backup { name } => {
backup::execute(config, name, cli.quiet)?;
}
Commands::Run {
profile,
passphrase,
command,
} => {
run::execute(config, profile, passphrase, command, cli.quiet).await?;
}
Commands::Env {
profile,
shell,
unset,
} => {
commands::env::execute(config, profile, shell, unset, cli.quiet)?;
}
Commands::Diff {
profile1,
profile2,
changes_only,
} => {
commands::diff::execute(config, profile1, profile2, changes_only, cli.quiet).await?;
}
Commands::Switch => {
commands::switch::execute(config, cli.quiet).await?;
}
Commands::History { limit, profile } => {
commands::history::execute(config, limit, profile, cli.quiet).await?;
}
Commands::Doctor { json } => {
commands::doctor::execute(config, json, cli.quiet).await?;
}
Commands::Completions { shell, print } => {
let shell_str = match shell {
ShellType::Bash => "bash",
ShellType::Zsh => "zsh",
ShellType::Fish => "fish",
ShellType::PowerShell => "powershell",
ShellType::Elvish => "elvish",
};
if print {
let output = commands::completions::generate_completions(shell_str)?;
println!("{output}");
} else {
commands::completions::install_completions(shell_str)?;
}
}
Commands::Import { name, data } => {
commands::import::execute(config, name, data, cli.quiet).await?;
}
Commands::Export { name } => {
commands::export::execute(config, name, cli.quiet).await?;
}
Commands::Setup => {
commands::setup::execute(config, cli.quiet).await?;
}
}
Ok(())
}