1use std::ffi::OsString;
2
3use rootcause::report;
4use ytil_sys::pico_args::Arguments;
5
6pub mod codex;
7pub mod sessions;
8
9#[derive(Debug, Eq, PartialEq)]
10pub enum Cmd {
11 Help,
12 SessionsList,
13 SessionsListJson(Vec<String>),
14 CodexCompact,
15}
16
17impl Cmd {
18 pub fn from_env() -> rootcause::Result<Self> {
19 Self::try_from(Arguments::from_env())
20 }
21
22 fn parse_sessions(mut args: Arguments) -> rootcause::Result<Self> {
23 match args.subcommand()?.as_deref() {
24 Some("list") if args.contains("--json") => Ok(Self::SessionsListJson(strings(args.finish()))),
25 Some("list") if args.finish().is_empty() => Ok(Self::SessionsList),
26 Some("list") => Err(report!("unsupported agg sessions list command")),
27 Some(_) => Err(report!("unsupported agg sessions command")),
28 None => Err(report!("missing agg sessions command")),
29 }
30 }
31
32 fn parse_codex(mut args: Arguments) -> rootcause::Result<Self> {
33 if args.contains("--compact") && args.finish().is_empty() {
34 Ok(Self::CodexCompact)
35 } else {
36 Err(report!("unsupported agg codex command"))
37 }
38 }
39}
40
41impl TryFrom<Arguments> for Cmd {
42 type Error = rootcause::Report;
43
44 fn try_from(mut args: Arguments) -> Result<Self, Self::Error> {
45 if args.contains("--help") {
46 return Ok(Self::Help);
47 }
48
49 let Some(command) = args.subcommand()? else {
50 return if args.finish().is_empty() {
51 Ok(Self::SessionsList)
52 } else {
53 Err(report!("unsupported agg command"))
54 };
55 };
56
57 match command.as_str() {
58 "sessions" => Self::parse_sessions(args),
59 "codex" => Self::parse_codex(args),
60 _ => Err(report!("unsupported agg command")),
61 }
62 }
63}
64
65fn strings(args: Vec<OsString>) -> Vec<String> {
66 args.into_iter().map(|arg| arg.to_string_lossy().into_owned()).collect()
67}
68
69#[cfg(test)]
70mod tests {
71 use rstest::rstest;
72 use test_that::prelude::*;
73
74 use super::*;
75
76 #[rstest]
77 #[case::bare(&[], Cmd::SessionsList)]
78 #[case::sessions_list(&["sessions", "list"], Cmd::SessionsList)]
79 #[case::codex_compact(&["codex", "--compact"], Cmd::CodexCompact)]
80 #[case::help(&["sessions", "--help"], Cmd::Help)]
81 fn test_parse_known_commands(#[case] args: &[&str], #[case] expected: Cmd) {
82 assert_that!(parse(args), ok(eq(expected)));
83 }
84
85 #[test]
86 fn test_parse_sessions_list_json_keeps_session_args() {
87 assert_that!(
88 parse(&["sessions", "list", "--json", "--session", "codex:session-id"]),
89 ok(eq(Cmd::SessionsListJson(vec![
90 "--session".to_owned(),
91 "codex:session-id".to_owned()
92 ])))
93 );
94 }
95
96 #[rstest]
97 #[case::unknown(&["unknown"])]
98 #[case::missing_sessions_subcommand(&["sessions"])]
99 #[case::unexpected_sessions_list_arg(&["sessions", "list", "unexpected"])]
100 #[case::missing_codex_flag(&["codex"])]
101 fn test_parse_rejects_invalid_commands(#[case] args: &[&str]) {
102 assert_that!(parse(args), err(anything()));
103 }
104
105 fn parse(args: &[&str]) -> rootcause::Result<Cmd> {
106 Cmd::try_from(Arguments::from_vec(args.iter().map(OsString::from).collect()))
107 }
108}