use clap::Parser;
use crate::model::ListDetail;
use crate::paths::DEFAULT_CACHE_TTL;
use crate::spool::DEFAULT_AGENT_MAX_BYTES;
pub const DEFAULT_AGENT_SEARCH_TOP: usize = 20;
#[derive(Debug, Clone, Parser)]
#[command(
name = "skiff",
about = "Turn any MCP server, OpenAPI spec, or GraphQL endpoint into a CLI",
version,
disable_help_subcommand = true,
allow_hyphen_values = true
)]
pub struct GlobalArgs {
#[arg(long)]
pub spec: Option<String>,
#[arg(long)]
pub mcp: Option<String>,
#[arg(long)]
pub mcp_stdio: Option<String>,
#[arg(long)]
pub graphql: Option<String>,
#[arg(long = "auth-header", value_name = "Name:Value")]
pub auth_header: Vec<String>,
#[arg(long)]
pub base_url: Option<String>,
#[arg(long)]
pub cache_key: Option<String>,
#[arg(long, default_value_t = DEFAULT_CACHE_TTL)]
pub cache_ttl: u64,
#[arg(long)]
pub refresh: bool,
#[arg(long = "list", visible_alias = "list-commands")]
pub list_commands: bool,
#[arg(long = "search", value_name = "PATTERN")]
pub search_pattern: Option<String>,
#[arg(long, value_name = "LEVEL", value_parser = ["names", "brief", "full"])]
pub detail: Option<String>,
#[arg(long = "describe", value_name = "TOOL")]
pub describe: Option<String>,
#[arg(long)]
pub verbose: bool,
#[arg(long, value_parser = ["usage", "recent", "alpha", "default"])]
pub sort: Option<String>,
#[arg(long, value_name = "N")]
pub top: Option<usize>,
#[arg(long)]
pub compact: bool,
#[arg(long)]
pub pretty: bool,
#[arg(long)]
pub raw: bool,
#[arg(long = "json")]
pub json_output: bool,
#[arg(long = "envelope", visible_alias = "full")]
pub envelope: bool,
#[arg(long)]
pub toon: bool,
#[arg(long, value_name = "N")]
pub head: Option<usize>,
#[arg(long = "max-bytes", value_name = "N")]
pub max_bytes: Option<usize>,
#[arg(long)]
pub inline: bool,
#[arg(long)]
pub agent: bool,
#[arg(long = "spool-clean")]
pub spool_clean: bool,
#[arg(long)]
pub fields: Option<String>,
#[arg(long, default_value = "auto", value_parser = ["auto", "sse", "streamable"])]
pub transport: String,
#[arg(long = "env", value_name = "KEY=VALUE")]
pub env: Vec<String>,
#[arg(long)]
pub oauth: bool,
#[arg(long)]
pub oauth_client_id: Option<String>,
#[arg(long)]
pub oauth_client_secret: Option<String>,
#[arg(long, default_value = "skiff")]
pub oauth_client_name: String,
#[arg(long)]
pub oauth_scope: Option<String>,
#[arg(long)]
pub oauth_redirect_uri: Option<String>,
#[arg(long, default_value = "auto", value_parser = ["auto", "authorization_code", "client_credentials"])]
pub oauth_flow: String,
#[arg(long = "oauth-clear")]
pub oauth_clear: bool,
#[arg(long = "session-start", value_name = "NAME")]
pub session_start: Option<String>,
#[arg(long = "session", value_name = "NAME")]
pub session: Option<String>,
#[arg(long = "session-stop", value_name = "NAME")]
pub session_stop: Option<String>,
#[arg(long = "session-list")]
pub session_list: bool,
#[arg(long = "session-idle-secs", value_name = "SECS")]
pub session_idle_secs: Option<u64>,
#[arg(long = "session-clean-env")]
pub session_clean_env: bool,
#[arg(long = "list-resources")]
pub list_resources: bool,
#[arg(long = "list-resource-templates")]
pub list_resource_templates: bool,
#[arg(long = "read-resource", value_name = "URI")]
pub read_resource: Option<String>,
#[arg(long = "list-prompts")]
pub list_prompts: bool,
#[arg(long = "get-prompt", value_name = "NAME")]
pub get_prompt: Option<String>,
#[arg(long = "prompt-arg", value_name = "KEY=VALUE")]
pub prompt_arg: Vec<String>,
}
impl GlobalArgs {
pub fn apply_agent_defaults(&mut self) {
let env_agent = std::env::var("SKIFF_AGENT")
.map(|v| matches!(v.as_str(), "1" | "true" | "yes" | "on"))
.unwrap_or(false);
if !self.agent && !env_agent {
return;
}
self.agent = true;
if !self.raw && !self.toon {
self.json_output = true;
}
if self.detail.is_none() && !self.compact {
if self.search_pattern.is_some() {
self.detail = Some("names".into());
} else {
self.detail = Some("brief".into());
}
}
if self.search_pattern.is_some() && self.top.is_none() {
self.top = Some(DEFAULT_AGENT_SEARCH_TOP);
}
if self.max_bytes.is_none() {
self.max_bytes = Some(DEFAULT_AGENT_MAX_BYTES);
}
}
pub fn list_detail(&self) -> ListDetail {
if self.compact {
return ListDetail::Names;
}
if let Some(d) = self.detail.as_deref().and_then(ListDetail::parse) {
return d;
}
if self.agent {
ListDetail::Brief
} else if self.json_output || self.toon {
ListDetail::Full
} else {
ListDetail::Brief
}
}
pub fn quiet_list(&self) -> bool {
self.agent || self.json_output || self.compact || self.toon
}
pub fn output_options(&self) -> crate::output::OutputOptions {
crate::output::OutputOptions {
pretty: self.pretty,
raw: self.raw,
toon: self.toon,
head: self.head,
json_output: self.json_output,
max_bytes: self.max_bytes,
inline: self.inline,
}
}
pub fn full_envelope(&self) -> bool {
self.envelope
}
pub fn parse_auth_headers(&self) -> crate::error::Result<Vec<(String, String)>> {
let mut out = Vec::new();
for item in &self.auth_header {
let Some((k, v)) = item.split_once(':') else {
return Err(crate::error::Error::usage(format!(
"invalid auth header format: {item:?}"
)));
};
let v = crate::coerce::resolve_secret(v.trim())?;
out.push((k.trim().to_string(), v));
}
Ok(out)
}
pub fn parse_env_vars(
&self,
) -> crate::error::Result<std::collections::BTreeMap<String, String>> {
let mut out = std::collections::BTreeMap::new();
for item in &self.env {
let Some((k, v)) = item.split_once('=') else {
return Err(crate::error::Error::usage(format!(
"invalid env format: {item:?}"
)));
};
out.insert(k.trim().to_string(), v.to_string());
}
Ok(out)
}
}