use std::ffi::OsString;
#[derive(Debug, Clone, PartialEq)]
pub enum ResumeArg {
Id(String),
Picker,
}
#[derive(Debug, Default, PartialEq)]
pub struct Flags {
pub interactive: bool,
pub new: bool,
pub continue_: bool,
pub pick_account: bool,
pub no_pick: bool,
pub resume: Option<ResumeArg>,
pub permission_mode: Option<String>,
pub effort: Option<String>,
pub model: Option<String>,
pub session_id: Option<String>,
pub profile: Option<String>,
}
#[derive(Debug, Default)]
pub struct ParsedArgs {
pub flags: Flags,
pub passthru: Vec<OsString>,
}
pub fn parse(args: &[OsString]) -> ParsedArgs {
let mut flags = Flags::default();
let mut passthru: Vec<OsString> = Vec::new();
let mut iter = args.iter().peekable();
while let Some(arg) = iter.next() {
let s = arg.to_string_lossy();
if s == "--" {
passthru.extend(iter.cloned());
break;
}
if s == "-i" || s == "--interactive" {
flags.interactive = true;
continue;
}
if s == "-n" || s == "--new" {
flags.new = true;
continue;
}
if s == "-c" || s == "--continue" {
flags.continue_ = true;
continue;
}
if s == "-A" || s == "--pick-account" {
flags.pick_account = true;
continue;
}
if s == "--no-pick" {
flags.no_pick = true;
continue;
}
if s == "-r" || s == "--resume" {
flags.resume = Some(consume_value_or_picker(&mut iter));
continue;
}
if let Some(val) = strip_eq_prefix(&s, "--resume") {
flags.resume = Some(ResumeArg::Id(val.to_owned()));
continue;
}
if s == "--permission-mode" {
flags.permission_mode = consume_required_value(&mut iter);
continue;
}
if let Some(val) = strip_eq_prefix(&s, "--permission-mode") {
flags.permission_mode = Some(val.to_owned());
continue;
}
if s == "--effort" {
flags.effort = consume_required_value(&mut iter);
continue;
}
if let Some(val) = strip_eq_prefix(&s, "--effort") {
flags.effort = Some(val.to_owned());
continue;
}
if s == "--model" {
flags.model = consume_required_value(&mut iter);
continue;
}
if let Some(val) = strip_eq_prefix(&s, "--model") {
flags.model = Some(val.to_owned());
continue;
}
if s == "--session-id" {
flags.session_id = consume_required_value(&mut iter);
continue;
}
if let Some(val) = strip_eq_prefix(&s, "--session-id") {
flags.session_id = Some(val.to_owned());
continue;
}
if s == "--profile" {
flags.profile = consume_required_value(&mut iter);
continue;
}
if let Some(val) = strip_eq_prefix(&s, "--profile") {
flags.profile = Some(val.to_owned());
continue;
}
passthru.push(arg.clone());
}
ParsedArgs { flags, passthru }
}
fn strip_eq_prefix<'a>(s: &'a str, flag: &str) -> Option<&'a str> {
let prefix = format!("{flag}=");
s.strip_prefix(prefix.as_str())
}
fn consume_value_or_picker(
iter: &mut std::iter::Peekable<std::slice::Iter<'_, OsString>>,
) -> ResumeArg {
match iter.peek() {
Some(next) if !next.to_string_lossy().starts_with('-') => {
ResumeArg::Id(iter.next().unwrap().to_string_lossy().into_owned())
}
_ => ResumeArg::Picker,
}
}
fn consume_required_value(
iter: &mut std::iter::Peekable<std::slice::Iter<'_, OsString>>,
) -> Option<String> {
match iter.peek() {
Some(next) if !next.to_string_lossy().starts_with('-') => {
Some(iter.next().unwrap().to_string_lossy().into_owned())
}
_ => None,
}
}
#[cfg(test)]
fn os_args(ss: &[&str]) -> Vec<OsString> {
ss.iter().map(OsString::from).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_interactive_short() {
let r = parse(&os_args(&["-i"]));
assert!(r.flags.interactive);
assert!(!r.flags.new);
assert!(!r.flags.continue_);
assert!(!r.flags.pick_account);
assert!(!r.flags.no_pick);
}
#[test]
fn parse_new_short() {
let r = parse(&os_args(&["-n"]));
assert!(r.flags.new);
assert!(!r.flags.interactive);
}
#[test]
fn parse_continue_short() {
let r = parse(&os_args(&["-c"]));
assert!(r.flags.continue_);
assert!(!r.flags.new);
}
#[test]
fn parse_pick_account_short() {
let r = parse(&os_args(&["-A"]));
assert!(r.flags.pick_account);
}
#[test]
fn parse_no_pick() {
let r = parse(&os_args(&["--no-pick"]));
assert!(r.flags.no_pick);
}
#[test]
fn parse_interactive_long() {
let r = parse(&os_args(&["--interactive"]));
assert!(r.flags.interactive);
assert!(r.passthru.is_empty());
}
#[test]
fn parse_new_long() {
let r = parse(&os_args(&["--new"]));
assert!(r.flags.new);
assert!(r.passthru.is_empty());
}
#[test]
fn parse_continue_long() {
let r = parse(&os_args(&["--continue"]));
assert!(r.flags.continue_);
assert!(r.passthru.is_empty());
}
#[test]
fn parse_pick_account_long() {
let r = parse(&os_args(&["--pick-account"]));
assert!(r.flags.pick_account);
assert!(r.passthru.is_empty());
}
#[test]
fn parse_resume_short_with_uuid() {
let r = parse(&os_args(&["-r", "01234567-89ab-cdef-0123-456789abcdef"]));
assert_eq!(
r.flags.resume,
Some(ResumeArg::Id(
"01234567-89ab-cdef-0123-456789abcdef".to_owned()
))
);
assert!(r.passthru.is_empty());
}
#[test]
fn parse_resume_long_with_uuid() {
let r = parse(&os_args(&[
"--resume",
"01234567-89ab-cdef-0123-456789abcdef",
]));
assert_eq!(
r.flags.resume,
Some(ResumeArg::Id(
"01234567-89ab-cdef-0123-456789abcdef".to_owned()
))
);
assert!(r.passthru.is_empty());
}
#[test]
fn parse_resume_long_with_alias() {
let r = parse(&os_args(&["--resume", "my-session-alias"]));
assert_eq!(
r.flags.resume,
Some(ResumeArg::Id("my-session-alias".to_owned()))
);
}
#[test]
fn parse_resume_equals_form_uuid() {
let r = parse(&os_args(&["--resume=01234567-89ab-cdef-0123-456789abcdef"]));
assert_eq!(
r.flags.resume,
Some(ResumeArg::Id(
"01234567-89ab-cdef-0123-456789abcdef".to_owned()
))
);
}
#[test]
fn parse_resume_equals_form_alias() {
let r = parse(&os_args(&["--resume=abc-def"]));
assert_eq!(r.flags.resume, Some(ResumeArg::Id("abc-def".to_owned())));
}
#[test]
fn parse_resume_short_missing_value_promotes_to_picker() {
let r = parse(&os_args(&["-r"]));
assert_eq!(r.flags.resume, Some(ResumeArg::Picker));
assert!(r.passthru.is_empty());
}
#[test]
fn parse_resume_long_missing_value_promotes_to_picker() {
let r = parse(&os_args(&["--resume"]));
assert_eq!(r.flags.resume, Some(ResumeArg::Picker));
assert!(r.passthru.is_empty());
}
#[test]
fn parse_resume_dash_prefixed_next_promotes_to_picker() {
let r = parse(&os_args(&["-r", "--model", "opus"]));
assert_eq!(r.flags.resume, Some(ResumeArg::Picker));
assert_eq!(r.flags.model.as_deref(), Some("opus"));
assert!(r.passthru.is_empty());
}
#[test]
fn parse_resume_long_dash_prefixed_next_promotes_to_picker() {
let r = parse(&os_args(&["--resume", "--effort", "high"]));
assert_eq!(r.flags.resume, Some(ResumeArg::Picker));
assert_eq!(r.flags.effort.as_deref(), Some("high"));
assert!(r.passthru.is_empty());
}
#[test]
fn parse_resume_absent_means_none() {
let r = parse(&os_args(&["-n"]));
assert_eq!(r.flags.resume, None);
}
#[test]
fn parse_permission_mode_space() {
let r = parse(&os_args(&["--permission-mode", "bypassPermissions"]));
assert_eq!(
r.flags.permission_mode.as_deref(),
Some("bypassPermissions")
);
assert!(r.passthru.is_empty());
}
#[test]
fn parse_effort_space() {
let r = parse(&os_args(&["--effort", "high"]));
assert_eq!(r.flags.effort.as_deref(), Some("high"));
assert!(r.passthru.is_empty());
}
#[test]
fn parse_model_space() {
let r = parse(&os_args(&["--model", "claude-opus-4-5"]));
assert_eq!(r.flags.model.as_deref(), Some("claude-opus-4-5"));
assert!(r.passthru.is_empty());
}
#[test]
fn parse_session_id_space() {
let r = parse(&os_args(&[
"--session-id",
"aaaabbbb-cccc-dddd-eeee-ffffaaaabbbb",
]));
assert_eq!(
r.flags.session_id.as_deref(),
Some("aaaabbbb-cccc-dddd-eeee-ffffaaaabbbb")
);
assert!(r.passthru.is_empty());
}
#[test]
fn parse_profile_space() {
let r = parse(&os_args(&["--profile", "home"]));
assert_eq!(r.flags.profile.as_deref(), Some("home"));
assert!(r.passthru.is_empty());
}
#[test]
fn parse_permission_mode_equals() {
let r = parse(&os_args(&["--permission-mode=bypassPermissions"]));
assert_eq!(
r.flags.permission_mode.as_deref(),
Some("bypassPermissions")
);
}
#[test]
fn parse_effort_equals() {
let r = parse(&os_args(&["--effort=high"]));
assert_eq!(r.flags.effort.as_deref(), Some("high"));
}
#[test]
fn parse_model_equals() {
let r = parse(&os_args(&["--model=claude-opus-4-5"]));
assert_eq!(r.flags.model.as_deref(), Some("claude-opus-4-5"));
}
#[test]
fn parse_session_id_equals() {
let r = parse(&os_args(&[
"--session-id=aaaabbbb-cccc-dddd-eeee-ffffaaaabbbb",
]));
assert_eq!(
r.flags.session_id.as_deref(),
Some("aaaabbbb-cccc-dddd-eeee-ffffaaaabbbb")
);
}
#[test]
fn parse_profile_equals() {
let r = parse(&os_args(&["--profile=home"]));
assert_eq!(r.flags.profile.as_deref(), Some("home"));
}
#[test]
fn permission_mode_space_and_equals_equivalent() {
let r_space = parse(&os_args(&["--permission-mode", "plan"]));
let r_eq = parse(&os_args(&["--permission-mode=plan"]));
assert_eq!(r_space.flags.permission_mode, r_eq.flags.permission_mode);
}
#[test]
fn effort_space_and_equals_equivalent() {
let r_space = parse(&os_args(&["--effort", "xhigh"]));
let r_eq = parse(&os_args(&["--effort=xhigh"]));
assert_eq!(r_space.flags.effort, r_eq.flags.effort);
}
#[test]
fn model_space_and_equals_equivalent() {
let r_space = parse(&os_args(&["--model", "claude-sonnet-4-5"]));
let r_eq = parse(&os_args(&["--model=claude-sonnet-4-5"]));
assert_eq!(r_space.flags.model, r_eq.flags.model);
}
#[test]
fn session_id_space_and_equals_equivalent() {
let id = "aaaabbbb-cccc-dddd-eeee-ffffaaaabbbb";
let r_space = parse(&os_args(&["--session-id", id]));
let r_eq = parse(&os_args(&[&format!("--session-id={id}")]));
assert_eq!(r_space.flags.session_id, r_eq.flags.session_id);
}
#[test]
fn profile_space_and_equals_equivalent() {
let r_space = parse(&os_args(&["--profile", "work"]));
let r_eq = parse(&os_args(&["--profile=work"]));
assert_eq!(r_space.flags.profile, r_eq.flags.profile);
}
#[test]
fn double_dash_stops_parsing_passes_rest() {
let r = parse(&os_args(&["-n", "--", "--model", "raw-arg"]));
assert!(r.flags.new);
assert!(r.flags.model.is_none());
assert_eq!(r.passthru, os_args(&["--model", "raw-arg"]));
}
#[test]
fn double_dash_with_nothing_after() {
let r = parse(&os_args(&["-i", "--"]));
assert!(r.flags.interactive);
assert!(r.passthru.is_empty());
}
#[test]
fn double_dash_at_start() {
let r = parse(&os_args(&["--", "-n", "--model", "x"]));
assert!(!r.flags.new);
assert_eq!(r.passthru, os_args(&["-n", "--model", "x"]));
}
#[test]
fn double_dash_preserves_multiple_args() {
let r = parse(&os_args(&["--", "foo", "bar", "baz"]));
assert_eq!(r.passthru, os_args(&["foo", "bar", "baz"]));
}
#[test]
fn passthru_unknown_long_flag() {
let r = parse(&os_args(&["--dangerously-skip-permissions"]));
assert_eq!(r.passthru, os_args(&["--dangerously-skip-permissions"]));
}
#[test]
fn passthru_unknown_short_flag() {
let r = parse(&os_args(&["-v"]));
assert_eq!(r.passthru, os_args(&["-v"]));
}
#[test]
fn passthru_positional_prompt() {
let r = parse(&os_args(&["implement the feature"]));
assert_eq!(r.passthru, os_args(&["implement the feature"]));
}
#[test]
fn passthru_multiple_positional_args() {
let r = parse(&os_args(&["foo", "bar", "baz"]));
assert_eq!(r.passthru, os_args(&["foo", "bar", "baz"]));
}
#[test]
fn passthru_claude_print_flag() {
let r = parse(&os_args(&["--print"]));
assert_eq!(r.passthru, os_args(&["--print"]));
}
#[test]
fn passthru_claude_output_format_flag() {
let r = parse(&os_args(&["--output-format=json"]));
assert_eq!(r.passthru, os_args(&["--output-format=json"]));
}
#[test]
fn passthru_preserves_interleaving_with_csm_flags() {
let r = parse(&os_args(&[
"--dangerously-skip-permissions",
"-n",
"my prompt",
]));
assert!(r.flags.new);
assert_eq!(
r.passthru,
os_args(&["--dangerously-skip-permissions", "my prompt"])
);
}
#[test]
fn empty_args() {
let r = parse(&[]);
assert_eq!(r.flags, Flags::default());
assert!(r.passthru.is_empty());
}
#[test]
fn all_boolean_flags_together() {
let r = parse(&os_args(&["-i", "-n", "-c", "-A", "--no-pick"]));
assert!(r.flags.interactive);
assert!(r.flags.new);
assert!(r.flags.continue_);
assert!(r.flags.pick_account);
assert!(r.flags.no_pick);
assert!(r.passthru.is_empty());
}
#[test]
fn all_boolean_flags_long_forms() {
let r = parse(&os_args(&[
"--interactive",
"--new",
"--continue",
"--pick-account",
"--no-pick",
]));
assert!(r.flags.interactive);
assert!(r.flags.new);
assert!(r.flags.continue_);
assert!(r.flags.pick_account);
assert!(r.flags.no_pick);
}
#[test]
fn interactive_plus_pick_account() {
let r = parse(&os_args(&["-i", "-A"]));
assert!(r.flags.interactive);
assert!(r.flags.pick_account);
assert!(!r.flags.no_pick);
assert!(r.passthru.is_empty());
}
#[test]
fn permission_mode_and_effort_fresh_session() {
let r = parse(&os_args(&["--permission-mode", "plan", "--effort", "high"]));
assert_eq!(r.flags.permission_mode.as_deref(), Some("plan"));
assert_eq!(r.flags.effort.as_deref(), Some("high"));
assert!(r.passthru.is_empty());
}
#[test]
fn combined_flags_passthru_and_double_dash() {
let r = parse(&os_args(&[
"-n",
"--profile=work",
"--effort",
"low",
"--",
"extra",
]));
assert!(r.flags.new);
assert_eq!(r.flags.profile.as_deref(), Some("work"));
assert_eq!(r.flags.effort.as_deref(), Some("low"));
assert_eq!(r.passthru, os_args(&["extra"]));
}
#[test]
fn value_flag_followed_by_dash_flag_does_not_consume_it() {
let r = parse(&os_args(&["--model", "--effort", "high"]));
assert!(r.flags.model.is_none());
assert_eq!(r.flags.effort.as_deref(), Some("high"));
}
#[test]
fn resume_followed_by_passthru_prompt() {
let r = parse(&os_args(&[
"--resume",
"aaaabbbb-cccc-dddd-eeee-ffffaaaabbbb",
"my follow-up",
]));
assert_eq!(
r.flags.resume,
Some(ResumeArg::Id(
"aaaabbbb-cccc-dddd-eeee-ffffaaaabbbb".to_owned()
))
);
assert_eq!(r.passthru, os_args(&["my follow-up"]));
}
#[test]
fn picker_promotion_then_other_flags_and_passthru() {
let r = parse(&os_args(&["-r", "--permission-mode=plan", "do the thing"]));
assert_eq!(r.flags.resume, Some(ResumeArg::Picker));
assert_eq!(r.flags.permission_mode.as_deref(), Some("plan"));
assert_eq!(r.passthru, os_args(&["do the thing"]));
}
#[test]
fn resume_equals_form_does_not_need_next_token() {
let r = parse(&os_args(&[
"--resume=aaaabbbb-cccc-dddd-eeee-ffffaaaabbbb",
"-n",
]));
assert_eq!(
r.flags.resume,
Some(ResumeArg::Id(
"aaaabbbb-cccc-dddd-eeee-ffffaaaabbbb".to_owned()
))
);
assert!(r.flags.new);
}
#[test]
fn profile_work_equals_form() {
let r = parse(&os_args(&["--profile=work"]));
assert_eq!(r.flags.profile.as_deref(), Some("work"));
}
#[test]
fn profile_space_form_work() {
let r = parse(&os_args(&["--profile", "work"]));
assert_eq!(r.flags.profile.as_deref(), Some("work"));
}
#[test]
fn unknown_flag_before_and_after_csm_flag() {
let r = parse(&os_args(&["--output-format=json", "--new", "--print"]));
assert!(r.flags.new);
assert_eq!(r.passthru, os_args(&["--output-format=json", "--print"]));
}
#[test]
fn auto_handoff_prompt_falls_to_passthru() {
let r = parse(&os_args(&[
"--resume",
"aaaabbbb-cccc-dddd-eeee-ffffaaaabbbb",
"resume",
]));
assert_eq!(
r.flags.resume,
Some(ResumeArg::Id(
"aaaabbbb-cccc-dddd-eeee-ffffaaaabbbb".to_owned()
))
);
assert_eq!(r.passthru, os_args(&["resume"]));
}
#[test]
fn resume_arg_id_carries_value() {
let r = parse(&os_args(&["-r", "some-alias"]));
match r.flags.resume.unwrap() {
ResumeArg::Id(s) => assert_eq!(s, "some-alias"),
ResumeArg::Picker => panic!("expected Id, got Picker"),
}
}
#[test]
fn resume_arg_picker_discriminant() {
let r = parse(&os_args(&["-r"]));
assert!(matches!(r.flags.resume, Some(ResumeArg::Picker)));
}
}