use clap::CommandFactory; use clap::Parser; use clap_complete::Shell; use clap_complete::generate; use codex_arg0::arg0_dispatch_or_else; use codex_chatgpt::apply_command::ApplyCommand; use codex_chatgpt::apply_command::run_apply_command; use codex_cli::LandlockCommand; use codex_cli::SeatbeltCommand; use codex_cli::login::run_login_status; use codex_cli::login::run_login_with_api_key; use codex_cli::login::run_login_with_chatgpt; use codex_cli::login::run_logout; use codex_cli::proto; use codex_common::CliConfigOverrides; use codex_exec::Cli as ExecCli; use codex_tui::AppExitInfo; use codex_tui::Cli as TuiCli; use owo_colors::OwoColorize; use std::path::PathBuf; use supports_color::Stream; mod mcp_cmd; use crate::mcp_cmd::McpCli; use crate::proto::ProtoCli; /// Codex CLI /// /// If no subcommand is specified, options will be forwarded to the interactive CLI. #[derive(Debug, Parser)] #[clap( author, version, // If a sub‑command is given, ignore requirements of the default args. subcommand_negates_reqs = true, // The executable is sometimes invoked via a platform‑specific name like // `codex-x86_64-unknown-linux-musl`, but the help output should always use // the generic `codex` command name that users run. bin_name = "codex" )] struct MultitoolCli { #[clap(flatten)] pub config_overrides: CliConfigOverrides, #[clap(flatten)] interactive: TuiCli, #[clap(subcommand)] subcommand: Option, } #[derive(Debug, clap::Subcommand)] enum Subcommand { /// Run Codex non-interactively. #[clap(visible_alias = "e")] Exec(ExecCli), /// Manage login. Login(LoginCommand), /// Remove stored authentication credentials. Logout(LogoutCommand), /// [experimental] Run Codex as an MCP server and manage MCP servers. Mcp(McpCli), /// Run the Protocol stream via stdin/stdout #[clap(visible_alias = "p")] Proto(ProtoCli), /// Generate shell completion scripts. Completion(CompletionCommand), /// Internal debugging commands. Debug(DebugArgs), /// Apply the latest diff produced by Codex agent as a `git apply` to your local working tree. #[clap(visible_alias = "a")] Apply(ApplyCommand), /// Resume a previous interactive session (picker by default; use --last to continue the most recent). Resume(ResumeCommand), /// Internal: generate TypeScript protocol bindings. #[clap(hide = true)] GenerateTs(GenerateTsCommand), } #[derive(Debug, Parser)] struct CompletionCommand { /// Shell to generate completions for #[clap(value_enum, default_value_t = Shell::Bash)] shell: Shell, } #[derive(Debug, Parser)] struct ResumeCommand { /// Conversation/session id (UUID). When provided, resumes this session. /// If omitted, use --last to pick the most recent recorded session. #[arg(value_name = "SESSION_ID")] session_id: Option, /// Continue the most recent session without showing the picker. #[arg(long = "last", default_value_t = false, conflicts_with = "session_id")] last: bool, #[clap(flatten)] config_overrides: TuiCli, } #[derive(Debug, Parser)] struct DebugArgs { #[command(subcommand)] cmd: DebugCommand, } #[derive(Debug, clap::Subcommand)] enum DebugCommand { /// Run a command under Seatbelt (macOS only). Seatbelt(SeatbeltCommand), /// Run a command under Landlock+seccomp (Linux only). Landlock(LandlockCommand), } #[derive(Debug, Parser)] struct LoginCommand { #[clap(skip)] config_overrides: CliConfigOverrides, #[arg(long = "api-key", value_name = "API_KEY")] api_key: Option, #[command(subcommand)] action: Option, } #[derive(Debug, clap::Subcommand)] enum LoginSubcommand { /// Show login status. Status, } #[derive(Debug, Parser)] struct LogoutCommand { #[clap(skip)] config_overrides: CliConfigOverrides, } #[derive(Debug, Parser)] struct GenerateTsCommand { /// Output directory where .ts files will be written #[arg(short = 'o', long = "out", value_name = "DIR")] out_dir: PathBuf, /// Optional path to the Prettier executable to format generated files #[arg(short = 'p', long = "prettier", value_name = "PRETTIER_BIN")] prettier: Option, } fn format_exit_messages(exit_info: AppExitInfo, color_enabled: bool) -> Vec { let AppExitInfo { token_usage, conversation_id, } = exit_info; if token_usage.is_zero() { return Vec::new(); } let mut lines = vec![format!( "{}", codex_core::protocol::FinalOutput::from(token_usage) )]; if let Some(session_id) = conversation_id { let resume_cmd = format!("codex resume {session_id}"); let command = if color_enabled { resume_cmd.cyan().to_string() } else { resume_cmd }; lines.push(format!("To continue this session, run {command}.")); } lines } fn print_exit_messages(exit_info: AppExitInfo) { let color_enabled = supports_color::on(Stream::Stdout).is_some(); for line in format_exit_messages(exit_info, color_enabled) { println!("{line}"); } } fn main() -> anyhow::Result<()> { arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move { cli_main(codex_linux_sandbox_exe).await?; Ok(()) }) } async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<()> { let MultitoolCli { config_overrides: root_config_overrides, mut interactive, subcommand, } = MultitoolCli::parse(); match subcommand { None => { prepend_config_flags( &mut interactive.config_overrides, root_config_overrides.clone(), ); let exit_info = codex_tui::run_main(interactive, codex_linux_sandbox_exe).await?; print_exit_messages(exit_info); } Some(Subcommand::Exec(mut exec_cli)) => { prepend_config_flags( &mut exec_cli.config_overrides, root_config_overrides.clone(), ); codex_exec::run_main(exec_cli, codex_linux_sandbox_exe).await?; } Some(Subcommand::Mcp(mut mcp_cli)) => { // Propagate any root-level config overrides (e.g. `-c key=value`). prepend_config_flags(&mut mcp_cli.config_overrides, root_config_overrides.clone()); mcp_cli.run(codex_linux_sandbox_exe).await?; } Some(Subcommand::Resume(ResumeCommand { session_id, last, config_overrides, })) => { interactive = finalize_resume_interactive( interactive, root_config_overrides.clone(), session_id, last, config_overrides, ); codex_tui::run_main(interactive, codex_linux_sandbox_exe).await?; } Some(Subcommand::Login(mut login_cli)) => { prepend_config_flags( &mut login_cli.config_overrides, root_config_overrides.clone(), ); match login_cli.action { Some(LoginSubcommand::Status) => { run_login_status(login_cli.config_overrides).await; } None => { if let Some(api_key) = login_cli.api_key { run_login_with_api_key(login_cli.config_overrides, api_key).await; } else { run_login_with_chatgpt(login_cli.config_overrides).await; } } } } Some(Subcommand::Logout(mut logout_cli)) => { prepend_config_flags( &mut logout_cli.config_overrides, root_config_overrides.clone(), ); run_logout(logout_cli.config_overrides).await; } Some(Subcommand::Proto(mut proto_cli)) => { prepend_config_flags( &mut proto_cli.config_overrides, root_config_overrides.clone(), ); proto::run_main(proto_cli).await?; } Some(Subcommand::Completion(completion_cli)) => { print_completion(completion_cli); } Some(Subcommand::Debug(debug_args)) => match debug_args.cmd { DebugCommand::Seatbelt(mut seatbelt_cli) => { prepend_config_flags( &mut seatbelt_cli.config_overrides, root_config_overrides.clone(), ); codex_cli::debug_sandbox::run_command_under_seatbelt( seatbelt_cli, codex_linux_sandbox_exe, ) .await?; } DebugCommand::Landlock(mut landlock_cli) => { prepend_config_flags( &mut landlock_cli.config_overrides, root_config_overrides.clone(), ); codex_cli::debug_sandbox::run_command_under_landlock( landlock_cli, codex_linux_sandbox_exe, ) .await?; } }, Some(Subcommand::Apply(mut apply_cli)) => { prepend_config_flags( &mut apply_cli.config_overrides, root_config_overrides.clone(), ); run_apply_command(apply_cli, None).await?; } Some(Subcommand::GenerateTs(gen_cli)) => { codex_protocol_ts::generate_ts(&gen_cli.out_dir, gen_cli.prettier.as_deref())?; } } Ok(()) } /// Prepend root-level overrides so they have lower precedence than /// CLI-specific ones specified after the subcommand (if any). fn prepend_config_flags( subcommand_config_overrides: &mut CliConfigOverrides, cli_config_overrides: CliConfigOverrides, ) { subcommand_config_overrides .raw_overrides .splice(0..0, cli_config_overrides.raw_overrides); } /// Build the final `TuiCli` for a `codex resume` invocation. fn finalize_resume_interactive( mut interactive: TuiCli, root_config_overrides: CliConfigOverrides, session_id: Option, last: bool, resume_cli: TuiCli, ) -> TuiCli { // Start with the parsed interactive CLI so resume shares the same // configuration surface area as `codex` without additional flags. let resume_session_id = session_id; interactive.resume_picker = resume_session_id.is_none() && !last; interactive.resume_last = last; interactive.resume_session_id = resume_session_id; // Merge resume-scoped flags and overrides with highest precedence. merge_resume_cli_flags(&mut interactive, resume_cli); // Propagate any root-level config overrides (e.g. `-c key=value`). prepend_config_flags(&mut interactive.config_overrides, root_config_overrides); interactive } /// Merge flags provided to `codex resume` so they take precedence over any /// root-level flags. Only overrides fields explicitly set on the resume-scoped /// CLI. Also appends `-c key=value` overrides with highest precedence. fn merge_resume_cli_flags(interactive: &mut TuiCli, resume_cli: TuiCli) { if let Some(model) = resume_cli.model { interactive.model = Some(model); } if resume_cli.oss { interactive.oss = true; } if let Some(profile) = resume_cli.config_profile { interactive.config_profile = Some(profile); } if let Some(sandbox) = resume_cli.sandbox_mode { interactive.sandbox_mode = Some(sandbox); } if let Some(approval) = resume_cli.approval_policy { interactive.approval_policy = Some(approval); } if resume_cli.full_auto { interactive.full_auto = true; } if resume_cli.dangerously_bypass_approvals_and_sandbox { interactive.dangerously_bypass_approvals_and_sandbox = true; } if let Some(cwd) = resume_cli.cwd { interactive.cwd = Some(cwd); } if resume_cli.web_search { interactive.web_search = true; } if !resume_cli.images.is_empty() { interactive.images = resume_cli.images; } if let Some(prompt) = resume_cli.prompt { interactive.prompt = Some(prompt); } interactive .config_overrides .raw_overrides .extend(resume_cli.config_overrides.raw_overrides); } fn print_completion(cmd: CompletionCommand) { let mut app = MultitoolCli::command(); let name = "codex"; generate(cmd.shell, &mut app, name, &mut std::io::stdout()); } #[cfg(test)] mod tests { use super::*; use codex_core::protocol::TokenUsage; use codex_protocol::mcp_protocol::ConversationId; fn finalize_from_args(args: &[&str]) -> TuiCli { let cli = MultitoolCli::try_parse_from(args).expect("parse"); let MultitoolCli { interactive, config_overrides: root_overrides, subcommand, } = cli; let Subcommand::Resume(ResumeCommand { session_id, last, config_overrides: resume_cli, }) = subcommand.expect("resume present") else { unreachable!() }; finalize_resume_interactive(interactive, root_overrides, session_id, last, resume_cli) } fn sample_exit_info(conversation: Option<&str>) -> AppExitInfo { let token_usage = TokenUsage { output_tokens: 2, total_tokens: 2, ..Default::default() }; AppExitInfo { token_usage, conversation_id: conversation .map(ConversationId::from_string) .map(Result::unwrap), } } #[test] fn format_exit_messages_skips_zero_usage() { let exit_info = AppExitInfo { token_usage: TokenUsage::default(), conversation_id: None, }; let lines = format_exit_messages(exit_info, false); assert!(lines.is_empty()); } #[test] fn format_exit_messages_includes_resume_hint_without_color() { let exit_info = sample_exit_info(Some("123e4567-e89b-12d3-a456-426614174000")); let lines = format_exit_messages(exit_info, false); assert_eq!( lines, vec![ "Token usage: total=2 input=0 output=2".to_string(), "To continue this session, run codex resume 123e4567-e89b-12d3-a456-426614174000." .to_string(), ] ); } #[test] fn format_exit_messages_applies_color_when_enabled() { let exit_info = sample_exit_info(Some("123e4567-e89b-12d3-a456-426614174000")); let lines = format_exit_messages(exit_info, true); assert_eq!(lines.len(), 2); assert!(lines[1].contains("\u{1b}[36m")); } #[test] fn resume_model_flag_applies_when_no_root_flags() { let interactive = finalize_from_args(["codex", "resume", "-m", "gpt-5-test"].as_ref()); assert_eq!(interactive.model.as_deref(), Some("gpt-5-test")); assert!(interactive.resume_picker); assert!(!interactive.resume_last); assert_eq!(interactive.resume_session_id, None); } #[test] fn resume_picker_logic_none_and_not_last() { let interactive = finalize_from_args(["codex", "resume"].as_ref()); assert!(interactive.resume_picker); assert!(!interactive.resume_last); assert_eq!(interactive.resume_session_id, None); } #[test] fn resume_picker_logic_last() { let interactive = finalize_from_args(["codex", "resume", "--last"].as_ref()); assert!(!interactive.resume_picker); assert!(interactive.resume_last); assert_eq!(interactive.resume_session_id, None); } #[test] fn resume_picker_logic_with_session_id() { let interactive = finalize_from_args(["codex", "resume", "1234"].as_ref()); assert!(!interactive.resume_picker); assert!(!interactive.resume_last); assert_eq!(interactive.resume_session_id.as_deref(), Some("1234")); } #[test] fn resume_merges_option_flags_and_full_auto() { let interactive = finalize_from_args( [ "codex", "resume", "sid", "--oss", "--full-auto", "--search", "--sandbox", "workspace-write", "--ask-for-approval", "on-request", "-m", "gpt-5-test", "-p", "my-profile", "-C", "/tmp", "-i", "/tmp/a.png,/tmp/b.png", ] .as_ref(), ); assert_eq!(interactive.model.as_deref(), Some("gpt-5-test")); assert!(interactive.oss); assert_eq!(interactive.config_profile.as_deref(), Some("my-profile")); assert!(matches!( interactive.sandbox_mode, Some(codex_common::SandboxModeCliArg::WorkspaceWrite) )); assert!(matches!( interactive.approval_policy, Some(codex_common::ApprovalModeCliArg::OnRequest) )); assert!(interactive.full_auto); assert_eq!( interactive.cwd.as_deref(), Some(std::path::Path::new("/tmp")) ); assert!(interactive.web_search); let has_a = interactive .images .iter() .any(|p| p == std::path::Path::new("/tmp/a.png")); let has_b = interactive .images .iter() .any(|p| p == std::path::Path::new("/tmp/b.png")); assert!(has_a && has_b); assert!(!interactive.resume_picker); assert!(!interactive.resume_last); assert_eq!(interactive.resume_session_id.as_deref(), Some("sid")); } #[test] fn resume_merges_dangerously_bypass_flag() { let interactive = finalize_from_args( [ "codex", "resume", "--dangerously-bypass-approvals-and-sandbox", ] .as_ref(), ); assert!(interactive.dangerously_bypass_approvals_and_sandbox); assert!(interactive.resume_picker); assert!(!interactive.resume_last); assert_eq!(interactive.resume_session_id, None); } }