Skip to main content

skiff_cli/cli/
mod.rs

1//! CLI: two-pass argv split, clap globals, and mode dispatch.
2//!
3//! [`split_at_subcommand`] keeps tool flags (e.g. a tool named `--env`) out of
4//! the global parser (upstream GH #15). Session start/stop/list and `--session`
5//! are handled before source-mode mutual exclusion.
6
7pub mod args;
8pub mod bake;
9pub mod dispatch;
10pub mod dynamic;
11pub mod list;
12
13pub use dispatch::dispatch;
14
15use std::collections::HashSet;
16use std::ffi::OsString;
17
18/// Split argv into `(global_args, tool_args)` at the first positional subcommand.
19///
20/// Mirrors Python `_split_at_subcommand` so tool flags like `--env` are not
21/// swallowed by the global parser (upstream GH #15).
22pub fn split_at_subcommand(
23    argv: &[OsString],
24    value_options: &HashSet<String>,
25    bool_options: &HashSet<String>,
26) -> (Vec<OsString>, Vec<OsString>) {
27    let _ = bool_options; // retained for API parity / future stricter splitting
28    let mut i = 0;
29    while i < argv.len() {
30        let arg = argv[i].to_string_lossy();
31        if arg == "--" {
32            return (argv[..i].to_vec(), argv[i + 1..].to_vec());
33        }
34        if arg.starts_with('-') {
35            if arg.starts_with("--") && arg.contains('=') {
36                i += 1;
37            } else if value_options.contains(arg.as_ref()) {
38                i += 2;
39            } else {
40                // Bool flags and unknown dashed tokens stay in the global slice.
41                i += 1;
42            }
43        } else {
44            return (argv[..i].to_vec(), argv[i..].to_vec());
45        }
46    }
47    (argv.to_vec(), Vec::new())
48}
49
50/// Global flag sets for the pre-parser (subset used for splitting).
51pub fn global_option_sets() -> (HashSet<String>, HashSet<String>) {
52    let value = [
53        "--spec",
54        "--mcp",
55        "--mcp-stdio",
56        "--graphql",
57        "--auth-header",
58        "--base-url",
59        "--cache-key",
60        "--cache-ttl",
61        "--search",
62        "--sort",
63        "--top",
64        "--head",
65        "--detail",
66        "--describe",
67        "--max-bytes",
68        "--fields",
69        "--transport",
70        "--env",
71        "--oauth-client-id",
72        "--oauth-client-secret",
73        "--oauth-client-name",
74        "--oauth-scope",
75        "--oauth-redirect-uri",
76        "--oauth-flow",
77        "--read-resource",
78        "--get-prompt",
79        "--prompt-arg",
80        "--session-start",
81        "--session-stop",
82        "--session",
83        "--session-idle-secs",
84    ]
85    .into_iter()
86    .map(str::to_string)
87    .collect();
88
89    let bools = [
90        "--refresh",
91        "--list",
92        "--verbose",
93        "--compact",
94        "--pretty",
95        "--raw",
96        "--json",
97        "--toon",
98        "--envelope",
99        "--full",
100        "--inline",
101        "--agent",
102        "--spool-clean",
103        "--oauth",
104        "--oauth-clear",
105        "--list-resources",
106        "--list-resource-templates",
107        "--list-prompts",
108        "--session-list",
109        "--session-clean-env",
110        "--version",
111        "-V",
112        "-h",
113        "--help",
114    ]
115    .into_iter()
116    .map(str::to_string)
117    .collect();
118
119    (value, bools)
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    fn os(s: &str) -> OsString {
127        OsString::from(s)
128    }
129
130    #[test]
131    fn splits_before_subcommand() {
132        let (value, bools) = global_option_sets();
133        let argv = vec![
134            os("--mcp"),
135            os("http://example.com"),
136            os("--list"),
137            os("search"),
138            os("--query"),
139            os("hi"),
140        ];
141        let (global, tool) = split_at_subcommand(&argv, &value, &bools);
142        assert_eq!(
143            global,
144            vec![os("--mcp"), os("http://example.com"), os("--list")]
145        );
146        assert_eq!(tool, vec![os("search"), os("--query"), os("hi")]);
147    }
148
149    #[test]
150    fn double_dash_separator() {
151        let (value, bools) = global_option_sets();
152        let argv = vec![os("--spec"), os("a.json"), os("--"), os("--env"), os("x=1")];
153        let (global, tool) = split_at_subcommand(&argv, &value, &bools);
154        assert_eq!(global, vec![os("--spec"), os("a.json")]);
155        assert_eq!(tool, vec![os("--env"), os("x=1")]);
156    }
157}