1use clap::Parser;
7
8use crate::model::ListDetail;
9use crate::paths::DEFAULT_CACHE_TTL;
10use crate::spool::DEFAULT_AGENT_MAX_BYTES;
11
12pub const DEFAULT_AGENT_SEARCH_TOP: usize = 20;
14
15#[derive(Debug, Clone, Parser)]
16#[command(
17 name = "skiff",
18 about = "Turn any MCP server, OpenAPI spec, or GraphQL endpoint into a CLI",
19 version,
20 disable_help_subcommand = true,
21 allow_hyphen_values = true
22)]
23pub struct GlobalArgs {
24 #[arg(long)]
26 pub spec: Option<String>,
27
28 #[arg(long)]
30 pub mcp: Option<String>,
31
32 #[arg(long)]
34 pub mcp_stdio: Option<String>,
35
36 #[arg(long)]
38 pub graphql: Option<String>,
39
40 #[arg(long = "auth-header", value_name = "Name:Value")]
42 pub auth_header: Vec<String>,
43
44 #[arg(long)]
46 pub base_url: Option<String>,
47
48 #[arg(long)]
50 pub cache_key: Option<String>,
51
52 #[arg(long, default_value_t = DEFAULT_CACHE_TTL)]
54 pub cache_ttl: u64,
55
56 #[arg(long)]
58 pub refresh: bool,
59
60 #[arg(long = "list", visible_alias = "list-commands")]
62 pub list_commands: bool,
63
64 #[arg(long = "search", value_name = "PATTERN")]
66 pub search_pattern: Option<String>,
67
68 #[arg(long, value_name = "LEVEL", value_parser = ["names", "brief", "full"])]
70 pub detail: Option<String>,
71
72 #[arg(long = "describe", value_name = "TOOL")]
74 pub describe: Option<String>,
75
76 #[arg(long)]
78 pub verbose: bool,
79
80 #[arg(long, value_parser = ["usage", "recent", "alpha", "default"])]
82 pub sort: Option<String>,
83
84 #[arg(long, value_name = "N")]
86 pub top: Option<usize>,
87
88 #[arg(long)]
90 pub compact: bool,
91
92 #[arg(long)]
94 pub pretty: bool,
95
96 #[arg(long)]
98 pub raw: bool,
99
100 #[arg(long = "json")]
102 pub json_output: bool,
103
104 #[arg(long = "envelope", visible_alias = "full")]
106 pub envelope: bool,
107
108 #[arg(long)]
110 pub toon: bool,
111
112 #[arg(long, value_name = "N")]
114 pub head: Option<usize>,
115
116 #[arg(long = "max-bytes", value_name = "N")]
118 pub max_bytes: Option<usize>,
119
120 #[arg(long)]
122 pub inline: bool,
123
124 #[arg(long)]
126 pub agent: bool,
127
128 #[arg(long = "spool-clean")]
130 pub spool_clean: bool,
131
132 #[arg(long)]
134 pub fields: Option<String>,
135
136 #[arg(long, default_value = "auto", value_parser = ["auto", "sse", "streamable"])]
138 pub transport: String,
139
140 #[arg(long = "env", value_name = "KEY=VALUE")]
142 pub env: Vec<String>,
143
144 #[arg(long)]
146 pub oauth: bool,
147
148 #[arg(long)]
150 pub oauth_client_id: Option<String>,
151
152 #[arg(long)]
154 pub oauth_client_secret: Option<String>,
155
156 #[arg(long, default_value = "skiff")]
158 pub oauth_client_name: String,
159
160 #[arg(long)]
162 pub oauth_scope: Option<String>,
163
164 #[arg(long)]
166 pub oauth_redirect_uri: Option<String>,
167
168 #[arg(long, default_value = "auto", value_parser = ["auto", "authorization_code", "client_credentials"])]
170 pub oauth_flow: String,
171
172 #[arg(long = "oauth-clear")]
174 pub oauth_clear: bool,
175
176 #[arg(long = "session-start", value_name = "NAME")]
178 pub session_start: Option<String>,
179
180 #[arg(long = "session", value_name = "NAME")]
182 pub session: Option<String>,
183
184 #[arg(long = "session-stop", value_name = "NAME")]
186 pub session_stop: Option<String>,
187
188 #[arg(long = "session-list")]
190 pub session_list: bool,
191
192 #[arg(long = "session-idle-secs", value_name = "SECS")]
194 pub session_idle_secs: Option<u64>,
195
196 #[arg(long = "session-clean-env")]
198 pub session_clean_env: bool,
199
200 #[arg(long = "list-resources")]
202 pub list_resources: bool,
203
204 #[arg(long = "list-resource-templates")]
206 pub list_resource_templates: bool,
207
208 #[arg(long = "read-resource", value_name = "URI")]
210 pub read_resource: Option<String>,
211
212 #[arg(long = "list-prompts")]
214 pub list_prompts: bool,
215
216 #[arg(long = "get-prompt", value_name = "NAME")]
218 pub get_prompt: Option<String>,
219
220 #[arg(long = "prompt-arg", value_name = "KEY=VALUE")]
222 pub prompt_arg: Vec<String>,
223}
224
225impl GlobalArgs {
226 pub fn apply_agent_defaults(&mut self) {
228 let env_agent = std::env::var("SKIFF_AGENT")
229 .map(|v| matches!(v.as_str(), "1" | "true" | "yes" | "on"))
230 .unwrap_or(false);
231 if !self.agent && !env_agent {
232 return;
233 }
234 self.agent = true;
235 if !self.raw && !self.toon {
236 self.json_output = true;
237 }
238 if self.detail.is_none() && !self.compact {
239 if self.search_pattern.is_some() {
241 self.detail = Some("names".into());
242 } else {
243 self.detail = Some("brief".into());
244 }
245 }
246 if self.search_pattern.is_some() && self.top.is_none() {
247 self.top = Some(DEFAULT_AGENT_SEARCH_TOP);
248 }
249 if self.max_bytes.is_none() {
250 self.max_bytes = Some(DEFAULT_AGENT_MAX_BYTES);
251 }
252 }
253
254 pub fn list_detail(&self) -> ListDetail {
255 if self.compact {
256 return ListDetail::Names;
257 }
258 if let Some(d) = self.detail.as_deref().and_then(ListDetail::parse) {
259 return d;
260 }
261 if self.agent {
263 ListDetail::Brief
264 } else if self.json_output || self.toon {
265 ListDetail::Full
266 } else {
267 ListDetail::Brief
268 }
269 }
270
271 pub fn quiet_list(&self) -> bool {
273 self.agent || self.json_output || self.compact || self.toon
274 }
275
276 pub fn output_options(&self) -> crate::output::OutputOptions {
277 crate::output::OutputOptions {
280 pretty: self.pretty,
281 raw: self.raw,
282 toon: self.toon,
283 head: self.head,
284 json_output: self.json_output,
285 max_bytes: self.max_bytes,
286 inline: self.inline,
287 }
288 }
289
290 pub fn full_envelope(&self) -> bool {
292 self.envelope
293 }
294
295 pub fn parse_auth_headers(&self) -> crate::error::Result<Vec<(String, String)>> {
296 let mut out = Vec::new();
297 for item in &self.auth_header {
298 let Some((k, v)) = item.split_once(':') else {
299 return Err(crate::error::Error::usage(format!(
300 "invalid auth header format: {item:?}"
301 )));
302 };
303 let v = crate::coerce::resolve_secret(v.trim())?;
304 out.push((k.trim().to_string(), v));
305 }
306 Ok(out)
307 }
308
309 pub fn parse_env_vars(
310 &self,
311 ) -> crate::error::Result<std::collections::BTreeMap<String, String>> {
312 let mut out = std::collections::BTreeMap::new();
313 for item in &self.env {
314 let Some((k, v)) = item.split_once('=') else {
315 return Err(crate::error::Error::usage(format!(
316 "invalid env format: {item:?}"
317 )));
318 };
319 out.insert(k.trim().to_string(), v.to_string());
320 }
321 Ok(out)
322 }
323}