280 lines
8.8 KiB
Rust
280 lines
8.8 KiB
Rust
use clap::Args;
|
||
use clap::FromArgMatches;
|
||
use clap::Parser;
|
||
use clap::ValueEnum;
|
||
use codex_common::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<Command>,
|
||
|
||
/// 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<PathBuf>,
|
||
|
||
/// Model the agent should use.
|
||
#[arg(long, short = 'm', global = true)]
|
||
pub model: Option<String>,
|
||
|
||
/// 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<String>,
|
||
|
||
/// Select the sandbox policy to use when executing model-generated shell
|
||
/// commands.
|
||
#[arg(long = "sandbox", short = 's', value_enum)]
|
||
pub sandbox_mode: Option<codex_common::SandboxModeCliArg>,
|
||
|
||
/// Configuration profile from config.toml to specify default options.
|
||
#[arg(long = "profile", short = 'p')]
|
||
pub config_profile: Option<String>,
|
||
|
||
/// 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<PathBuf>,
|
||
|
||
/// 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<PathBuf>,
|
||
|
||
/// Path to a JSON Schema file describing the model's final response shape.
|
||
#[arg(long = "output-schema", value_name = "FILE")]
|
||
pub output_schema: Option<PathBuf>,
|
||
|
||
#[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,
|
||
|
||
/// 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")]
|
||
pub last_message_file: Option<PathBuf>,
|
||
|
||
/// 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<String>,
|
||
}
|
||
|
||
#[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 <prompt>" 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<String>,
|
||
|
||
/// 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<PathBuf>,
|
||
|
||
/// Prompt to send after resuming the session. If `-` is used, read from stdin.
|
||
#[arg(value_name = "PROMPT", value_hint = clap::ValueHint::Other)]
|
||
prompt: Option<String>,
|
||
}
|
||
|
||
#[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<String>,
|
||
|
||
/// 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<PathBuf>,
|
||
|
||
/// Prompt to send after resuming the session. If `-` is used, read from stdin.
|
||
pub prompt: Option<String>,
|
||
}
|
||
|
||
impl From<ResumeArgsRaw> 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<Self, clap::Error> {
|
||
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<String>,
|
||
|
||
/// Review the changes introduced by a commit.
|
||
#[arg(
|
||
long = "commit",
|
||
value_name = "SHA",
|
||
conflicts_with_all = ["uncommitted", "base", "prompt"]
|
||
)]
|
||
pub commit: Option<String>,
|
||
|
||
/// Optional commit title to display in the review summary.
|
||
#[arg(long = "title", value_name = "TITLE", requires = "commit")]
|
||
pub commit_title: Option<String>,
|
||
|
||
/// Custom review instructions. If `-` is used, read from stdin.
|
||
#[arg(value_name = "PROMPT", value_hint = clap::ValueHint::Other)]
|
||
pub prompt: Option<String>,
|
||
}
|
||
|
||
#[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",
|
||
PROMPT,
|
||
]);
|
||
|
||
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));
|
||
}
|
||
}
|