use clap::Args; 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::WindowsCommand; use codex_cli::login::read_api_key_from_stdin; 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_login_with_device_code; use codex_cli::login::run_logout; use codex_cloud_tasks::Cli as CloudTasksCli; use codex_common::CliConfigOverrides; use codex_exec::Cli as ExecCli; use codex_exec::Command as ExecCommand; use codex_exec::ReviewArgs; use codex_execpolicy::ExecPolicyCheckCommand; use codex_responses_api_proxy::Args as ResponsesApiProxyArgs; use codex_tui::AppExitInfo; use codex_tui::Cli as TuiCli; use codex_tui::ExitReason; use codex_tui::update_action::UpdateAction; use owo_colors::OwoColorize; use std::io::IsTerminal; use std::path::PathBuf; use supports_color::Stream; mod mcp_cmd; #[cfg(not(windows))] mod wsl_paths; use crate::mcp_cmd::McpCli; use codex_core::config::Config; use codex_core::config::ConfigOverrides; use codex_core::features::is_known_feature_key; use codex_core::terminal::TerminalName; /// 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", override_usage = "codex [OPTIONS] [PROMPT]\n codex [OPTIONS] [ARGS]" )] struct MultitoolCli { #[clap(flatten)] pub config_overrides: CliConfigOverrides, #[clap(flatten)] pub feature_toggles: FeatureToggles, #[clap(flatten)] interactive: TuiCli, #[clap(subcommand)] subcommand: Option, } #[derive(Debug, clap::Subcommand)] enum Subcommand { /// Run Codex non-interactively. #[clap(visible_alias = "e")] Exec(ExecCli), /// Run a code review non-interactively. Review(ReviewArgs), /// Manage login. Login(LoginCommand), /// Remove stored authentication credentials. Logout(LogoutCommand), /// [experimental] Run Codex as an MCP server and manage MCP servers. Mcp(McpCli), /// [experimental] Run the Codex MCP server (stdio transport). McpServer, /// [experimental] Run the app server or related tooling. AppServer(AppServerCommand), /// Generate shell completion scripts. Completion(CompletionCommand), /// Run commands within a Codex-provided sandbox. #[clap(visible_alias = "debug")] Sandbox(SandboxArgs), /// Execpolicy tooling. #[clap(hide = true)] Execpolicy(ExecpolicyCommand), /// 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), /// Fork a previous interactive session (picker by default; use --last to fork the most recent). Fork(ForkCommand), /// [EXPERIMENTAL] Browse tasks from Codex Cloud and apply changes locally. #[clap(name = "cloud", alias = "cloud-tasks")] Cloud(CloudTasksCli), /// Internal: run the responses API proxy. #[clap(hide = true)] ResponsesApiProxy(ResponsesApiProxyArgs), /// Internal: relay stdio to a Unix domain socket. #[clap(hide = true, name = "stdio-to-uds")] StdioToUds(StdioToUdsCommand), /// Inspect feature flags. Features(FeaturesCli), } #[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)] last: bool, /// Show all sessions (disables cwd filtering and shows CWD column). #[arg(long = "all", default_value_t = false)] all: bool, #[clap(flatten)] config_overrides: TuiCli, } #[derive(Debug, Parser)] struct ForkCommand { /// Conversation/session id (UUID). When provided, forks this session. /// If omitted, use --last to pick the most recent recorded session. #[arg(value_name = "SESSION_ID")] session_id: Option, /// Fork the most recent session without showing the picker. #[arg(long = "last", default_value_t = false, conflicts_with = "session_id")] last: bool, /// Show all sessions (disables cwd filtering and shows CWD column). #[arg(long = "all", default_value_t = false)] all: bool, #[clap(flatten)] config_overrides: TuiCli, } #[derive(Debug, Parser)] struct SandboxArgs { #[command(subcommand)] cmd: SandboxCommand, } #[derive(Debug, clap::Subcommand)] enum SandboxCommand { /// Run a command under Seatbelt (macOS only). #[clap(visible_alias = "seatbelt")] Macos(SeatbeltCommand), /// Run a command under Landlock+seccomp (Linux only). #[clap(visible_alias = "landlock")] Linux(LandlockCommand), /// Run a command under Windows restricted token (Windows only). Windows(WindowsCommand), } #[derive(Debug, Parser)] struct ExecpolicyCommand { #[command(subcommand)] sub: ExecpolicySubcommand, } #[derive(Debug, clap::Subcommand)] enum ExecpolicySubcommand { /// Check execpolicy files against a command. #[clap(name = "check")] Check(ExecPolicyCheckCommand), } #[derive(Debug, Parser)] struct LoginCommand { #[clap(skip)] config_overrides: CliConfigOverrides, #[arg( long = "with-api-key", help = "Read the API key from stdin (e.g. `printenv OPENAI_API_KEY | codex login --with-api-key`)" )] with_api_key: bool, #[arg( long = "api-key", value_name = "API_KEY", help = "(deprecated) Previously accepted the API key directly; now exits with guidance to use --with-api-key", hide = true )] api_key: Option, #[arg(long = "device-auth")] use_device_code: bool, /// EXPERIMENTAL: Use custom OAuth issuer base URL (advanced) /// Override the OAuth issuer base URL (advanced) #[arg(long = "experimental_issuer", value_name = "URL", hide = true)] issuer_base_url: Option, /// EXPERIMENTAL: Use custom OAuth client ID (advanced) #[arg(long = "experimental_client-id", value_name = "CLIENT_ID", hide = true)] client_id: 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 AppServerCommand { /// Omit to run the app server; specify a subcommand for tooling. #[command(subcommand)] subcommand: Option, /// Controls whether analytics are enabled by default. /// /// Analytics are disabled by default for app-server. Users have to explicitly opt in /// via the `analytics` section in the config.toml file. /// /// However, for first-party use cases like the VSCode IDE extension, we default analytics /// to be enabled by default by setting this flag. Users can still opt out by setting this /// in their config.toml: /// /// ```toml /// [analytics] /// enabled = false /// ``` /// /// See https://developers.openai.com/codex/config-advanced/#metrics for more details. #[arg(long = "analytics-default-enabled")] analytics_default_enabled: bool, } #[derive(Debug, clap::Subcommand)] enum AppServerSubcommand { /// [experimental] Generate TypeScript bindings for the app server protocol. GenerateTs(GenerateTsCommand), /// [experimental] Generate JSON Schema for the app server protocol. GenerateJsonSchema(GenerateJsonSchemaCommand), } #[derive(Debug, Args)] 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, } #[derive(Debug, Args)] struct GenerateJsonSchemaCommand { /// Output directory where the schema bundle will be written #[arg(short = 'o', long = "out", value_name = "DIR")] out_dir: PathBuf, } #[derive(Debug, Parser)] struct StdioToUdsCommand { /// Path to the Unix domain socket to connect to. #[arg(value_name = "SOCKET_PATH")] socket_path: PathBuf, } fn format_exit_messages(exit_info: AppExitInfo, color_enabled: bool) -> Vec { let AppExitInfo { token_usage, thread_id: 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 } /// Handle the app exit and print the results. Optionally run the update action. fn handle_app_exit(exit_info: AppExitInfo) -> anyhow::Result<()> { match exit_info.exit_reason { ExitReason::Fatal(message) => { eprintln!("ERROR: {message}"); std::process::exit(1); } ExitReason::UserRequested => { /* normal exit */ } } let update_action = exit_info.update_action; let color_enabled = supports_color::on(Stream::Stdout).is_some(); for line in format_exit_messages(exit_info, color_enabled) { println!("{line}"); } if let Some(action) = update_action { run_update_action(action)?; } Ok(()) } /// Run the update action and print the result. fn run_update_action(action: UpdateAction) -> anyhow::Result<()> { println!(); let cmd_str = action.command_str(); println!("Updating Codex via `{cmd_str}`..."); let status = { #[cfg(windows)] { // On Windows, run via cmd.exe so .CMD/.BAT are correctly resolved (PATHEXT semantics). std::process::Command::new("cmd") .args(["/C", &cmd_str]) .status()? } #[cfg(not(windows))] { let (cmd, args) = action.command_args(); let command_path = crate::wsl_paths::normalize_for_wsl(cmd); let normalized_args: Vec = args .iter() .map(crate::wsl_paths::normalize_for_wsl) .collect(); std::process::Command::new(&command_path) .args(&normalized_args) .status()? } }; if !status.success() { anyhow::bail!("`{cmd_str}` failed with status {status}"); } println!("\n🎉 Update ran successfully! Please restart Codex."); Ok(()) } fn run_execpolicycheck(cmd: ExecPolicyCheckCommand) -> anyhow::Result<()> { cmd.run() } #[derive(Debug, Default, Parser, Clone)] struct FeatureToggles { /// Enable a feature (repeatable). Equivalent to `-c features.=true`. #[arg(long = "enable", value_name = "FEATURE", action = clap::ArgAction::Append, global = true)] enable: Vec, /// Disable a feature (repeatable). Equivalent to `-c features.=false`. #[arg(long = "disable", value_name = "FEATURE", action = clap::ArgAction::Append, global = true)] disable: Vec, } impl FeatureToggles { fn to_overrides(&self) -> anyhow::Result> { let mut v = Vec::new(); for feature in &self.enable { Self::validate_feature(feature)?; v.push(format!("features.{feature}=true")); } for feature in &self.disable { Self::validate_feature(feature)?; v.push(format!("features.{feature}=false")); } Ok(v) } fn validate_feature(feature: &str) -> anyhow::Result<()> { if is_known_feature_key(feature) { Ok(()) } else { anyhow::bail!("Unknown feature flag: {feature}") } } } #[derive(Debug, Parser)] struct FeaturesCli { #[command(subcommand)] sub: FeaturesSubcommand, } #[derive(Debug, Parser)] enum FeaturesSubcommand { /// List known features with their stage and effective state. List, } fn stage_str(stage: codex_core::features::Stage) -> &'static str { use codex_core::features::Stage; match stage { Stage::UnderDevelopment => "under development", Stage::Experimental { .. } => "experimental", Stage::Stable => "stable", Stage::Deprecated => "deprecated", Stage::Removed => "removed", } } 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: mut root_config_overrides, feature_toggles, mut interactive, subcommand, } = MultitoolCli::parse(); // Fold --enable/--disable into config overrides so they flow to all subcommands. let toggle_overrides = feature_toggles.to_overrides()?; root_config_overrides.raw_overrides.extend(toggle_overrides); match subcommand { None => { prepend_config_flags( &mut interactive.config_overrides, root_config_overrides.clone(), ); let exit_info = run_interactive_tui(interactive, codex_linux_sandbox_exe).await?; handle_app_exit(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::Review(review_args)) => { let mut exec_cli = ExecCli::try_parse_from(["codex", "exec"])?; exec_cli.command = Some(ExecCommand::Review(review_args)); 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::McpServer) => { codex_mcp_server::run_main(codex_linux_sandbox_exe, root_config_overrides).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().await?; } Some(Subcommand::AppServer(app_server_cli)) => match app_server_cli.subcommand { None => { codex_app_server::run_main( codex_linux_sandbox_exe, root_config_overrides, codex_core::config_loader::LoaderOverrides::default(), app_server_cli.analytics_default_enabled, ) .await?; } Some(AppServerSubcommand::GenerateTs(gen_cli)) => { codex_app_server_protocol::generate_ts( &gen_cli.out_dir, gen_cli.prettier.as_deref(), )?; } Some(AppServerSubcommand::GenerateJsonSchema(gen_cli)) => { codex_app_server_protocol::generate_json(&gen_cli.out_dir)?; } }, Some(Subcommand::Resume(ResumeCommand { session_id, last, all, config_overrides, })) => { interactive = finalize_resume_interactive( interactive, root_config_overrides.clone(), session_id, last, all, config_overrides, ); let exit_info = run_interactive_tui(interactive, codex_linux_sandbox_exe).await?; handle_app_exit(exit_info)?; } Some(Subcommand::Fork(ForkCommand { session_id, last, all, config_overrides, })) => { interactive = finalize_fork_interactive( interactive, root_config_overrides.clone(), session_id, last, all, config_overrides, ); let exit_info = run_interactive_tui(interactive, codex_linux_sandbox_exe).await?; handle_app_exit(exit_info)?; } 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 login_cli.use_device_code { run_login_with_device_code( login_cli.config_overrides, login_cli.issuer_base_url, login_cli.client_id, ) .await; } else if login_cli.api_key.is_some() { eprintln!( "The --api-key flag is no longer supported. Pipe the key instead, e.g. `printenv OPENAI_API_KEY | codex login --with-api-key`." ); std::process::exit(1); } else if login_cli.with_api_key { let api_key = read_api_key_from_stdin(); 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::Completion(completion_cli)) => { print_completion(completion_cli); } Some(Subcommand::Cloud(mut cloud_cli)) => { prepend_config_flags( &mut cloud_cli.config_overrides, root_config_overrides.clone(), ); codex_cloud_tasks::run_main(cloud_cli, codex_linux_sandbox_exe).await?; } Some(Subcommand::Sandbox(sandbox_args)) => match sandbox_args.cmd { SandboxCommand::Macos(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?; } SandboxCommand::Linux(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?; } SandboxCommand::Windows(mut windows_cli) => { prepend_config_flags( &mut windows_cli.config_overrides, root_config_overrides.clone(), ); codex_cli::debug_sandbox::run_command_under_windows( windows_cli, codex_linux_sandbox_exe, ) .await?; } }, Some(Subcommand::Execpolicy(ExecpolicyCommand { sub })) => match sub { ExecpolicySubcommand::Check(cmd) => run_execpolicycheck(cmd)?, }, 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::ResponsesApiProxy(args)) => { tokio::task::spawn_blocking(move || codex_responses_api_proxy::run_main(args)) .await??; } Some(Subcommand::StdioToUds(cmd)) => { let socket_path = cmd.socket_path; tokio::task::spawn_blocking(move || codex_stdio_to_uds::run(socket_path.as_path())) .await??; } Some(Subcommand::Features(FeaturesCli { sub })) => match sub { FeaturesSubcommand::List => { // Respect root-level `-c` overrides plus top-level flags like `--profile`. let mut cli_kv_overrides = root_config_overrides .parse_overrides() .map_err(anyhow::Error::msg)?; // Honor `--search` via the canonical web_search mode. if interactive.web_search { cli_kv_overrides.push(( "web_search".to_string(), toml::Value::String("live".to_string()), )); } // Thread through relevant top-level flags (at minimum, `--profile`). let overrides = ConfigOverrides { config_profile: interactive.config_profile.clone(), ..Default::default() }; let config = Config::load_with_cli_overrides_and_harness_overrides( cli_kv_overrides, overrides, ) .await?; let mut rows = Vec::with_capacity(codex_core::features::FEATURES.len()); let mut name_width = 0; let mut stage_width = 0; for def in codex_core::features::FEATURES.iter() { let name = def.key; let stage = stage_str(def.stage); let enabled = config.features.enabled(def.id); name_width = name_width.max(name.len()); stage_width = stage_width.max(stage.len()); rows.push((name, stage, enabled)); } for (name, stage, enabled) in rows { println!("{name:, ) -> std::io::Result { if let Some(prompt) = interactive.prompt.take() { // Normalize CRLF/CR to LF so CLI-provided text can't leak `\r` into TUI state. interactive.prompt = Some(prompt.replace("\r\n", "\n").replace('\r', "\n")); } let terminal_info = codex_core::terminal::terminal_info(); if terminal_info.name == TerminalName::Dumb { if !(std::io::stdin().is_terminal() && std::io::stderr().is_terminal()) { return Ok(AppExitInfo::fatal( "TERM is set to \"dumb\". Refusing to start the interactive TUI because no terminal is available for a confirmation prompt (stdin/stderr is not a TTY). Run in a supported terminal or unset TERM.", )); } eprintln!( "WARNING: TERM is set to \"dumb\". Codex's interactive TUI may not work in this terminal." ); if !confirm("Continue anyway? [y/N]: ")? { return Ok(AppExitInfo::fatal( "Refusing to start the interactive TUI because TERM is set to \"dumb\". Run in a supported terminal or unset TERM.", )); } } codex_tui::run_main(interactive, codex_linux_sandbox_exe).await } fn confirm(prompt: &str) -> std::io::Result { eprintln!("{prompt}"); let mut input = String::new(); std::io::stdin().read_line(&mut input)?; let answer = input.trim(); Ok(answer.eq_ignore_ascii_case("y") || answer.eq_ignore_ascii_case("yes")) } /// 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, show_all: 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; interactive.resume_show_all = show_all; // Merge resume-scoped flags and overrides with highest precedence. merge_interactive_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 } /// Build the final `TuiCli` for a `codex fork` invocation. fn finalize_fork_interactive( mut interactive: TuiCli, root_config_overrides: CliConfigOverrides, session_id: Option, last: bool, show_all: bool, fork_cli: TuiCli, ) -> TuiCli { // Start with the parsed interactive CLI so fork shares the same // configuration surface area as `codex` without additional flags. let fork_session_id = session_id; interactive.fork_picker = fork_session_id.is_none() && !last; interactive.fork_last = last; interactive.fork_session_id = fork_session_id; interactive.fork_show_all = show_all; // Merge fork-scoped flags and overrides with highest precedence. merge_interactive_cli_flags(&mut interactive, fork_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`/`codex fork` so they take precedence over any /// root-level flags. Only overrides fields explicitly set on the subcommand-scoped /// CLI. Also appends `-c key=value` overrides with highest precedence. fn merge_interactive_cli_flags(interactive: &mut TuiCli, subcommand_cli: TuiCli) { if let Some(model) = subcommand_cli.model { interactive.model = Some(model); } if subcommand_cli.oss { interactive.oss = true; } if let Some(profile) = subcommand_cli.config_profile { interactive.config_profile = Some(profile); } if let Some(sandbox) = subcommand_cli.sandbox_mode { interactive.sandbox_mode = Some(sandbox); } if let Some(approval) = subcommand_cli.approval_policy { interactive.approval_policy = Some(approval); } if subcommand_cli.full_auto { interactive.full_auto = true; } if subcommand_cli.dangerously_bypass_approvals_and_sandbox { interactive.dangerously_bypass_approvals_and_sandbox = true; } if let Some(cwd) = subcommand_cli.cwd { interactive.cwd = Some(cwd); } if subcommand_cli.web_search { interactive.web_search = true; } if !subcommand_cli.images.is_empty() { interactive.images = subcommand_cli.images; } if !subcommand_cli.add_dir.is_empty() { interactive.add_dir.extend(subcommand_cli.add_dir); } if let Some(prompt) = subcommand_cli.prompt { // Normalize CRLF/CR to LF so CLI-provided text can't leak `\r` into TUI state. interactive.prompt = Some(prompt.replace("\r\n", "\n").replace('\r', "\n")); } interactive .config_overrides .raw_overrides .extend(subcommand_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 assert_matches::assert_matches; use codex_core::protocol::TokenUsage; use codex_protocol::ThreadId; use pretty_assertions::assert_eq; fn finalize_resume_from_args(args: &[&str]) -> TuiCli { let cli = MultitoolCli::try_parse_from(args).expect("parse"); let MultitoolCli { interactive, config_overrides: root_overrides, subcommand, feature_toggles: _, } = cli; let Subcommand::Resume(ResumeCommand { session_id, last, all, config_overrides: resume_cli, }) = subcommand.expect("resume present") else { unreachable!() }; finalize_resume_interactive( interactive, root_overrides, session_id, last, all, resume_cli, ) } fn finalize_fork_from_args(args: &[&str]) -> TuiCli { let cli = MultitoolCli::try_parse_from(args).expect("parse"); let MultitoolCli { interactive, config_overrides: root_overrides, subcommand, feature_toggles: _, } = cli; let Subcommand::Fork(ForkCommand { session_id, last, all, config_overrides: fork_cli, }) = subcommand.expect("fork present") else { unreachable!() }; finalize_fork_interactive(interactive, root_overrides, session_id, last, all, fork_cli) } #[test] fn exec_resume_last_accepts_prompt_positional() { let cli = MultitoolCli::try_parse_from(["codex", "exec", "--json", "resume", "--last", "2+2"]) .expect("parse should succeed"); let Some(Subcommand::Exec(exec)) = cli.subcommand else { panic!("expected exec subcommand"); }; let Some(codex_exec::Command::Resume(args)) = exec.command else { panic!("expected exec resume"); }; assert!(args.last); assert_eq!(args.session_id, None); assert_eq!(args.prompt.as_deref(), Some("2+2")); } fn app_server_from_args(args: &[&str]) -> AppServerCommand { let cli = MultitoolCli::try_parse_from(args).expect("parse"); let Subcommand::AppServer(app_server) = cli.subcommand.expect("app-server present") else { unreachable!() }; app_server } fn sample_exit_info(conversation: Option<&str>) -> AppExitInfo { let token_usage = TokenUsage { output_tokens: 2, total_tokens: 2, ..Default::default() }; AppExitInfo { token_usage, thread_id: conversation.map(ThreadId::from_string).map(Result::unwrap), update_action: None, exit_reason: ExitReason::UserRequested, } } #[test] fn format_exit_messages_skips_zero_usage() { let exit_info = AppExitInfo { token_usage: TokenUsage::default(), thread_id: None, update_action: None, exit_reason: ExitReason::UserRequested, }; 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_resume_from_args(["codex", "resume", "-m", "gpt-5.1-test"].as_ref()); assert_eq!(interactive.model.as_deref(), Some("gpt-5.1-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_resume_from_args(["codex", "resume"].as_ref()); assert!(interactive.resume_picker); assert!(!interactive.resume_last); assert_eq!(interactive.resume_session_id, None); assert!(!interactive.resume_show_all); } #[test] fn resume_picker_logic_last() { let interactive = finalize_resume_from_args(["codex", "resume", "--last"].as_ref()); assert!(!interactive.resume_picker); assert!(interactive.resume_last); assert_eq!(interactive.resume_session_id, None); assert!(!interactive.resume_show_all); } #[test] fn resume_picker_logic_with_session_id() { let interactive = finalize_resume_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")); assert!(!interactive.resume_show_all); } #[test] fn resume_all_flag_sets_show_all() { let interactive = finalize_resume_from_args(["codex", "resume", "--all"].as_ref()); assert!(interactive.resume_picker); assert!(interactive.resume_show_all); } #[test] fn resume_merges_option_flags_and_full_auto() { let interactive = finalize_resume_from_args( [ "codex", "resume", "sid", "--oss", "--full-auto", "--search", "--sandbox", "workspace-write", "--ask-for-approval", "on-request", "-m", "gpt-5.1-test", "-p", "my-profile", "-C", "/tmp", "-i", "/tmp/a.png,/tmp/b.png", ] .as_ref(), ); assert_eq!(interactive.model.as_deref(), Some("gpt-5.1-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_resume_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); } #[test] fn fork_picker_logic_none_and_not_last() { let interactive = finalize_fork_from_args(["codex", "fork"].as_ref()); assert!(interactive.fork_picker); assert!(!interactive.fork_last); assert_eq!(interactive.fork_session_id, None); assert!(!interactive.fork_show_all); } #[test] fn fork_picker_logic_last() { let interactive = finalize_fork_from_args(["codex", "fork", "--last"].as_ref()); assert!(!interactive.fork_picker); assert!(interactive.fork_last); assert_eq!(interactive.fork_session_id, None); assert!(!interactive.fork_show_all); } #[test] fn fork_picker_logic_with_session_id() { let interactive = finalize_fork_from_args(["codex", "fork", "1234"].as_ref()); assert!(!interactive.fork_picker); assert!(!interactive.fork_last); assert_eq!(interactive.fork_session_id.as_deref(), Some("1234")); assert!(!interactive.fork_show_all); } #[test] fn fork_all_flag_sets_show_all() { let interactive = finalize_fork_from_args(["codex", "fork", "--all"].as_ref()); assert!(interactive.fork_picker); assert!(interactive.fork_show_all); } #[test] fn app_server_analytics_default_disabled_without_flag() { let app_server = app_server_from_args(["codex", "app-server"].as_ref()); assert!(!app_server.analytics_default_enabled); } #[test] fn app_server_analytics_default_enabled_with_flag() { let app_server = app_server_from_args(["codex", "app-server", "--analytics-default-enabled"].as_ref()); assert!(app_server.analytics_default_enabled); } #[test] fn feature_toggles_known_features_generate_overrides() { let toggles = FeatureToggles { enable: vec!["web_search_request".to_string()], disable: vec!["unified_exec".to_string()], }; let overrides = toggles.to_overrides().expect("valid features"); assert_eq!( overrides, vec![ "features.web_search_request=true".to_string(), "features.unified_exec=false".to_string(), ] ); } #[test] fn feature_toggles_unknown_feature_errors() { let toggles = FeatureToggles { enable: vec!["does_not_exist".to_string()], disable: Vec::new(), }; let err = toggles .to_overrides() .expect_err("feature should be rejected"); assert_eq!(err.to_string(), "Unknown feature flag: does_not_exist"); } }