use crate::product::common::CliConfigOverrides;
use crate::product::protocol::config_types::IdentityKind;
use clap::Args;
use clap::FromArgMatches;
use clap::Parser;
use clap::ValueEnum;
use std::path::PathBuf;
#[derive(Parser, Debug)]
#[command(version)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Command>,
#[arg(
long = "image",
short = 'i',
value_name = "FILE",
value_delimiter = ',',
num_args = 1..
)]
pub images: Vec<PathBuf>,
#[arg(long, short = 'm', global = true)]
pub model: Option<String>,
#[arg(long = "sandbox", short = 's', value_enum)]
pub sandbox_mode: Option<crate::product::common::SandboxModeCliArg>,
#[arg(long = "profile", short = 'p')]
pub config_profile: Option<String>,
#[arg(long = "identity", value_enum)]
pub identity: Option<ExecIdentityArg>,
#[arg(long = "full-auto", default_value_t = false, global = true)]
pub full_auto: bool,
#[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,
#[clap(long = "cd", short = 'C', value_name = "DIR")]
pub cwd: Option<PathBuf>,
#[arg(long = "skip-git-repo-check", global = true, default_value_t = false)]
pub skip_git_repo_check: bool,
#[arg(long = "add-dir", value_name = "DIR", value_hint = clap::ValueHint::DirPath)]
pub add_dir: Vec<PathBuf>,
#[arg(long = "output-schema", value_name = "FILE")]
pub output_schema: Option<PathBuf>,
#[clap(skip)]
pub config_overrides: CliConfigOverrides,
#[arg(long = "color", value_enum, default_value_t = Color::Auto)]
pub color: Color,
#[arg(
long = "json",
alias = "experimental-json",
default_value_t = false,
global = true
)]
pub json: bool,
#[arg(long = "internal-raw-events", hide = true, default_value_t = false)]
pub internal_raw_events: bool,
#[arg(long = "output-last-message", short = 'o', value_name = "FILE")]
pub last_message_file: Option<PathBuf>,
#[arg(value_name = "PROMPT", value_hint = clap::ValueHint::Other)]
pub prompt: Option<String>,
}
#[derive(Parser, Debug)]
pub(crate) struct CliWithConfigOverrides {
#[clap(flatten)]
config_overrides: CliConfigOverrides,
#[clap(flatten)]
inner: Cli,
}
impl CliWithConfigOverrides {
pub(crate) fn into_inner(mut self) -> Cli {
self.inner
.config_overrides
.raw_overrides
.splice(0..0, self.config_overrides.raw_overrides);
self.inner
}
}
pub(crate) fn parse_with_config_overrides() -> Cli {
CliWithConfigOverrides::parse().into_inner()
}
#[derive(Debug, clap::Subcommand)]
pub enum Command {
Resume(ResumeArgs),
Review(ReviewArgs),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
#[value(rename_all = "kebab-case")]
pub enum ExecIdentityArg {
Nobody,
Planner,
Programmer,
Explorer,
Reviewer,
}
impl From<ExecIdentityArg> for IdentityKind {
fn from(value: ExecIdentityArg) -> Self {
match value {
ExecIdentityArg::Nobody => IdentityKind::Nobody,
ExecIdentityArg::Planner => IdentityKind::Planner,
ExecIdentityArg::Programmer => IdentityKind::Programmer,
ExecIdentityArg::Explorer => IdentityKind::Explorer,
ExecIdentityArg::Reviewer => IdentityKind::Reviewer,
}
}
}
#[derive(Args, Debug)]
struct ResumeArgsRaw {
#[arg(value_name = "SESSION_ID")]
session_id: Option<String>,
#[arg(long = "last", default_value_t = false)]
last: bool,
#[arg(long = "all", default_value_t = false)]
all: bool,
#[arg(
long = "image",
short = 'i',
value_name = "FILE",
value_delimiter = ',',
num_args = 1
)]
images: Vec<PathBuf>,
#[arg(value_name = "PROMPT", value_hint = clap::ValueHint::Other)]
prompt: Option<String>,
}
#[derive(Debug)]
pub struct ResumeArgs {
pub session_id: Option<String>,
pub last: bool,
pub all: bool,
pub images: Vec<PathBuf>,
pub prompt: Option<String>,
}
impl From<ResumeArgsRaw> for ResumeArgs {
fn from(raw: ResumeArgsRaw) -> Self {
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 {
#[arg(
long = "uncommitted",
default_value_t = false,
conflicts_with_all = ["base", "commit", "prompt"]
)]
pub uncommitted: bool,
#[arg(
long = "base",
value_name = "BRANCH",
conflicts_with_all = ["uncommitted", "commit", "prompt"]
)]
pub base: Option<String>,
#[arg(
long = "commit",
value_name = "SHA",
conflicts_with_all = ["uncommitted", "base", "prompt"]
)]
pub commit: Option<String>,
#[arg(long = "title", value_name = "TITLE", requires = "commit")]
pub commit_title: Option<String>,
#[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([
"lha-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));
}
#[test]
fn root_config_overrides_are_merged_into_exec_cli() {
let cli = CliWithConfigOverrides::parse_from([
"lha-exec",
"resume",
"--last",
"--json",
"--model",
"gpt-5.2-codex",
"--config",
"reasoning_level=xhigh",
"--dangerously-bypass-approvals-and-sandbox",
"--skip-git-repo-check",
"echo resume",
])
.into_inner();
assert_eq!(
cli.config_overrides.raw_overrides,
vec!["reasoning_level=xhigh".to_string()]
);
let Some(Command::Resume(args)) = cli.command else {
panic!("expected resume command");
};
assert_eq!(args.prompt.as_deref(), Some("echo resume"));
}
#[test]
fn parses_identity_flag() {
let cli = Cli::parse_from(["lha-exec", "--identity", "explorer", "inspect"]);
assert_eq!(cli.identity, Some(ExecIdentityArg::Explorer));
}
#[test]
fn parses_internal_raw_events_flag() {
let cli = Cli::parse_from(["lha-exec", "--internal-raw-events", "inspect"]);
assert!(cli.internal_raw_events);
assert!(!cli.json);
}
}