Skip to main content

agg/
cmds.rs

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}