use clap::Args; use clap::FromArgMatches; use clap::Parser; use clap::ValueEnum; use codex_utils_cli::CliConfigOverrides; use std::path::PathBuf; #[derive(Parser, Debug)] #[command(version)] pub struct Cli { /// Action to perform. If omitted, runs a new non-interactive session. #[command(subcommand)] pub command: Option, /// Optional image(s) to attach to the initial prompt. #[arg( long = "image", short = 'i', value_name = "FILE", value_delimiter = ',', num_args = 1.. )] pub images: Vec, /// Model the agent should use. #[arg(long, short = 'm', global = true)] pub model: Option, /// Use open-source provider. #[arg(long = "oss", default_value_t = false)] pub oss: bool, /// Specify which local provider to use (lmstudio or ollama). /// If not specified with --oss, will use config default or show selection. #[arg(long = "local-provider")] pub oss_provider: Option, /// Select the sandbox policy to use when executing model-generated shell /// commands. #[arg(long = "sandbox", short = 's', value_enum)] pub sandbox_mode: Option, /// Configuration profile from config.toml to specify default options. #[arg(long = "profile", short = 'p')] pub config_profile: Option, /// Convenience alias for low-friction sandboxed automatic execution (-a on-request, --sandbox workspace-write). #[arg(long = "full-auto", default_value_t = false, global = true)] pub full_auto: bool, /// Skip all confirmation prompts and execute commands without sandboxing. /// EXTREMELY DANGEROUS. Intended solely for running in environments that are externally sandboxed. #[arg( long = "dangerously-bypass-approvals-and-sandbox", alias = "yolo", default_value_t = false, global = true, conflicts_with = "full_auto" )] pub dangerously_bypass_approvals_and_sandbox: bool, /// Tell the agent to use the specified directory as its working root. #[clap(long = "cd", short = 'C', value_name = "DIR")] pub cwd: Option, /// Allow running Codex outside a Git repository. #[arg(long = "skip-git-repo-check", global = true, default_value_t = false)] pub skip_git_repo_check: bool, /// Additional directories that should be writable alongside the primary workspace. #[arg(long = "add-dir", value_name = "DIR", value_hint = clap::ValueHint::DirPath)] pub add_dir: Vec, /// Run without persisting session files to disk. #[arg(long = "ephemeral", global = true, default_value_t = false)] pub ephemeral: bool, /// Path to a JSON Schema file describing the model's final response shape. #[arg(long = "output-schema", value_name = "FILE")] pub output_schema: Option, #[clap(skip)] pub config_overrides: CliConfigOverrides, /// Specifies color settings for use in the output. #[arg(long = "color", value_enum, default_value_t = Color::Auto)] pub color: Color, /// Force cursor-based progress updates in exec mode. #[arg(long = "progress-cursor", default_value_t = false)] pub progress_cursor: bool, /// Print events to stdout as JSONL. #[arg( long = "json", alias = "experimental-json", default_value_t = false, global = true )] pub json: bool, /// Specifies file where the last message from the agent should be written. #[arg( long = "output-last-message", short = 'o', value_name = "FILE", global = true )] pub last_message_file: Option, /// Initial instructions for the agent. If not provided as an argument (or /// if `-` is used), instructions are read from stdin. #[arg(value_name = "PROMPT", value_hint = clap::ValueHint::Other)] pub prompt: Option, } #[derive(Debug, clap::Subcommand)] pub enum Command { /// Resume a previous session by id or pick the most recent with --last. Resume(ResumeArgs), /// Run a code review against the current repository. Review(ReviewArgs), } #[derive(Args, Debug)] struct ResumeArgsRaw { // Note: This is the direct clap shape. We reinterpret the positional when --last is set // so "codex resume --last " treats the positional as a prompt, not a session id. /// Conversation/session id (UUID) or thread name. UUIDs take precedence if it parses. /// If omitted, use --last to pick the most recent recorded session. #[arg(value_name = "SESSION_ID")] session_id: Option, /// Resume the most recent recorded session (newest) without specifying an id. #[arg(long = "last", default_value_t = false)] last: bool, /// Show all sessions (disables cwd filtering). #[arg(long = "all", default_value_t = false)] all: bool, /// Optional image(s) to attach to the prompt sent after resuming. #[arg( long = "image", short = 'i', value_name = "FILE", value_delimiter = ',', num_args = 1 )] images: Vec, /// Prompt to send after resuming the session. If `-` is used, read from stdin. #[arg(value_name = "PROMPT", value_hint = clap::ValueHint::Other)] prompt: Option, } #[derive(Debug)] pub struct ResumeArgs { /// Conversation/session id (UUID) or thread name. UUIDs take precedence if it parses. /// If omitted, use --last to pick the most recent recorded session. pub session_id: Option, /// Resume the most recent recorded session (newest) without specifying an id. pub last: bool, /// Show all sessions (disables cwd filtering). pub all: bool, /// Optional image(s) to attach to the prompt sent after resuming. pub images: Vec, /// Prompt to send after resuming the session. If `-` is used, read from stdin. pub prompt: Option, } impl From for ResumeArgs { fn from(raw: ResumeArgsRaw) -> Self { // When --last is used without an explicit prompt, treat the positional as the prompt // (clap can’t express this conditional positional meaning cleanly). let (session_id, prompt) = if raw.last && raw.prompt.is_none() { (None, raw.session_id) } else { (raw.session_id, raw.prompt) }; Self { session_id, last: raw.last, all: raw.all, images: raw.images, prompt, } } } impl Args for ResumeArgs { fn augment_args(cmd: clap::Command) -> clap::Command { ResumeArgsRaw::augment_args(cmd) } fn augment_args_for_update(cmd: clap::Command) -> clap::Command { ResumeArgsRaw::augment_args_for_update(cmd) } } impl FromArgMatches for ResumeArgs { fn from_arg_matches(matches: &clap::ArgMatches) -> Result { ResumeArgsRaw::from_arg_matches(matches).map(Self::from) } fn update_from_arg_matches(&mut self, matches: &clap::ArgMatches) -> Result<(), clap::Error> { *self = ResumeArgsRaw::from_arg_matches(matches).map(Self::from)?; Ok(()) } } #[derive(Parser, Debug)] pub struct ReviewArgs { /// Review staged, unstaged, and untracked changes. #[arg( long = "uncommitted", default_value_t = false, conflicts_with_all = ["base", "commit", "prompt"] )] pub uncommitted: bool, /// Review changes against the given base branch. #[arg( long = "base", value_name = "BRANCH", conflicts_with_all = ["uncommitted", "commit", "prompt"] )] pub base: Option, /// Review the changes introduced by a commit. #[arg( long = "commit", value_name = "SHA", conflicts_with_all = ["uncommitted", "base", "prompt"] )] pub commit: Option, /// Optional commit title to display in the review summary. #[arg(long = "title", value_name = "TITLE", requires = "commit")] pub commit_title: Option, /// Custom review instructions. If `-` is used, read from stdin. #[arg(value_name = "PROMPT", value_hint = clap::ValueHint::Other)] pub prompt: Option, } #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)] #[value(rename_all = "kebab-case")] pub enum Color { Always, Never, #[default] Auto, } #[cfg(test)] mod tests { use super::*; use pretty_assertions::assert_eq; #[test] fn resume_parses_prompt_after_global_flags() { const PROMPT: &str = "echo resume-with-global-flags-after-subcommand"; let cli = Cli::parse_from([ "codex-exec", "resume", "--last", "--json", "--model", "gpt-5.2-codex", "--dangerously-bypass-approvals-and-sandbox", "--skip-git-repo-check", "--ephemeral", PROMPT, ]); assert!(cli.ephemeral); let Some(Command::Resume(args)) = cli.command else { panic!("expected resume command"); }; let effective_prompt = args.prompt.clone().or_else(|| { if args.last { args.session_id.clone() } else { None } }); assert_eq!(effective_prompt.as_deref(), Some(PROMPT)); } #[test] fn resume_accepts_output_last_message_flag_after_subcommand() { const PROMPT: &str = "echo resume-with-output-file"; let cli = Cli::parse_from([ "codex-exec", "resume", "session-123", "-o", "/tmp/resume-output.md", PROMPT, ]); assert_eq!( cli.last_message_file, Some(PathBuf::from("/tmp/resume-output.md")) ); let Some(Command::Resume(args)) = cli.command else { panic!("expected resume command"); }; assert_eq!(args.session_id.as_deref(), Some("session-123")); assert_eq!(args.prompt.as_deref(), Some(PROMPT)); } }