use std::collections::{BTreeMap, HashMap};
use std::io::IsTerminal;
use std::sync::Arc;
use serde_json::Value;
use crate::command::{self, CommandDef, ExecuteOptions, InternalResult, ParseMode};
use crate::config;
use crate::fetch::{self, FetchGatewayOptions, FetchHandler};
use crate::filter;
use crate::help::{self, CommandSummary, FormatCommandOptions, FormatRootOptions};
use crate::middleware::MiddlewareFn;
use crate::output::*;
use crate::schema::FieldMeta;
use crate::skill;
pub enum CommandEntry {
Leaf(Arc<CommandDef>),
Group {
description: Option<String>,
commands: BTreeMap<String, CommandEntry>,
middleware: Vec<MiddlewareFn>,
output_policy: Option<OutputPolicy>,
},
FetchGateway {
description: Option<String>,
base_path: Option<String>,
output_policy: Option<OutputPolicy>,
handler: Arc<dyn FetchHandler>,
},
}
impl CommandEntry {
pub fn description(&self) -> Option<&str> {
match self {
CommandEntry::Leaf(def) => def.description.as_deref(),
CommandEntry::Group { description, .. } => description.as_deref(),
CommandEntry::FetchGateway { description, .. } => description.as_deref(),
}
}
}
pub struct ConfigOptions {
pub flag: String,
pub files: Vec<String>,
}
pub struct Cli {
pub name: String,
pub description: Option<String>,
pub version: Option<String>,
pub aliases: Vec<String>,
pub(crate) commands: BTreeMap<String, CommandEntry>,
pub(crate) middleware: Vec<MiddlewareFn>,
root_command: Option<Arc<CommandDef>>,
pub(crate) env_fields: Vec<FieldMeta>,
pub(crate) vars_fields: Vec<FieldMeta>,
config: Option<ConfigOptions>,
output_policy: Option<OutputPolicy>,
format: Option<Format>,
}
impl Cli {
pub fn create(name: impl Into<String>) -> Self {
Cli {
name: name.into(),
description: None,
version: None,
aliases: Vec::new(),
commands: BTreeMap::new(),
middleware: Vec::new(),
root_command: None,
env_fields: Vec::new(),
vars_fields: Vec::new(),
config: None,
output_policy: None,
format: None,
}
}
pub fn description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub fn version(mut self, v: impl Into<String>) -> Self {
self.version = Some(v.into());
self
}
pub fn aliases(mut self, aliases: Vec<String>) -> Self {
self.aliases = aliases;
self
}
pub fn format(mut self, format: Format) -> Self {
self.format = Some(format);
self
}
pub fn output_policy(mut self, policy: OutputPolicy) -> Self {
self.output_policy = Some(policy);
self
}
pub fn root(mut self, def: CommandDef) -> Self {
self.root_command = Some(Arc::new(def));
self
}
pub fn env_fields(mut self, fields: Vec<FieldMeta>) -> Self {
self.env_fields = fields;
self
}
pub fn vars_fields(mut self, fields: Vec<FieldMeta>) -> Self {
self.vars_fields = fields;
self
}
pub fn config(mut self, options: ConfigOptions) -> Self {
self.config = Some(options);
self
}
pub fn command(mut self, name: impl Into<String>, def: CommandDef) -> Self {
self.commands
.insert(name.into(), CommandEntry::Leaf(Arc::new(def)));
self
}
pub fn group(mut self, cli: Cli) -> Self {
if let Some(root_cmd) = cli.root_command {
if cli.commands.is_empty() {
self.commands.insert(cli.name, CommandEntry::Leaf(root_cmd));
return self;
}
}
let entry = CommandEntry::Group {
description: cli.description,
commands: cli.commands,
middleware: cli.middleware,
output_policy: cli.output_policy,
};
self.commands.insert(cli.name, entry);
self
}
pub fn fetch_gateway(
mut self,
name: impl Into<String>,
handler: impl FetchHandler + 'static,
options: FetchGatewayOptions,
) -> Self {
self.commands.insert(
name.into(),
CommandEntry::FetchGateway {
description: options.description,
base_path: options.base_path,
output_policy: options.output_policy,
handler: Arc::new(handler),
},
);
self
}
pub fn use_middleware(mut self, handler: MiddlewareFn) -> Self {
self.middleware.push(handler);
self
}
pub async fn serve(&self) -> Result<(), Box<dyn std::error::Error>> {
let argv: Vec<String> = std::env::args().skip(1).collect();
self.serve_with(argv).await
}
pub async fn serve_with(&self, argv: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
let human = std::io::stdout().is_terminal();
let config_flag = self.config.as_ref().map(|c| c.flag.as_str());
let builtin = match extract_builtin_flags(&argv, config_flag) {
Ok(b) => b,
Err(e) => {
let msg = e.to_string();
if human {
writeln_stdout(&format_human_error("UNKNOWN", &msg));
} else {
writeln_stdout(&format!(
"{{\"ok\":false,\"error\":{{\"code\":\"UNKNOWN\",\"message\":\"{}\"}}}}",
msg.replace('"', "\\\"")
));
}
std::process::exit(1);
}
};
if builtin.version
&& !builtin.help
&& let Some(v) = &self.version
{
writeln_stdout(v);
return Ok(());
}
if builtin.llms || builtin.llms_full {
let commands_info = collect_command_info(&self.commands, &[]);
if builtin.llms_full {
let groups = collect_group_descriptions(&self.commands, &[]);
let output = skill::generate(&self.name, &commands_info, &groups);
writeln_stdout(&output);
} else {
let output = skill::index(&self.name, &commands_info, self.description.as_deref());
writeln_stdout(&output);
}
return Ok(());
}
if builtin.mcp {
#[cfg(feature = "mcp")]
{
let version = self.version.as_deref().unwrap_or("0.0.0");
crate::mcp::serve(
&self.name,
version,
&self.commands,
&self.middleware,
&self.env_fields,
&Default::default(),
)
.await?;
return Ok(());
}
#[cfg(not(feature = "mcp"))]
{
writeln_stdout("MCP support requires the 'mcp' feature flag.");
std::process::exit(1);
}
}
if let Some(output) = completion_output(
&self.name,
&self.aliases,
&self.commands,
self.root_command.as_ref(),
&argv,
std::env::var("COMPLETE").ok().as_deref(),
std::env::var("_COMPLETE_INDEX").ok().as_deref(),
) {
writeln_stdout(&output);
return Ok(());
}
let builtins = builtin_commands(&self.name);
if let Some(index) = builtin_command_index(&builtin.rest, &self.name, "completions") {
let builtin_def = builtins
.iter()
.find(|item| item.name == "completions")
.expect("completions builtin must exist");
let shell = builtin.rest.get(index + 1).map(|token| token.as_str());
if builtin.help || shell.is_none() {
writeln_stdout(&help::format_command(
&format!("{} completions", self.name),
&FormatCommandOptions {
aliases: None,
args_fields: builtin_def.args_fields.clone(),
config_flag: None,
commands: Vec::new(),
description: Some(builtin_def.description.to_string()),
env_fields: Vec::new(),
examples: Vec::new(),
hint: builtin_def.hint.clone(),
hide_global_options: true,
options_fields: Vec::new(),
option_aliases: HashMap::new(),
root: false,
version: None,
},
));
return Ok(());
}
let shell = shell.expect("checked above");
if crate::completions::Shell::from_str(shell).is_none() {
writeln_stdout(&format_human_error(
"INVALID_SHELL",
&format!("Unknown shell '{shell}'. Supported: bash, fish, nushell, zsh"),
));
std::process::exit(1);
}
let output = std::iter::once(self.name.clone())
.chain(self.aliases.iter().cloned())
.map(|name| {
crate::completions::register(
crate::completions::Shell::from_str(shell).expect("checked above"),
&name,
)
})
.collect::<Vec<_>>()
.join("\n");
writeln_stdout(&output);
return Ok(());
}
if let Some(index) = builtin_command_index(&builtin.rest, &self.name, "skills") {
let builtin_def = builtins
.iter()
.find(|item| item.name == "skills")
.expect("skills builtin must exist");
if builtin.rest.get(index + 1).map(|token| token.as_str()) != Some("add") {
writeln_stdout(&format_builtin_help(&self.name, builtin_def));
return Ok(());
}
if builtin.help {
writeln_stdout(&format_builtin_subcommand_help(
&self.name,
builtin_def,
"add",
));
return Ok(());
}
let rest = builtin.rest[(index + 2)..].to_vec();
let depth = if let Some(depth_index) = rest.iter().position(|token| token == "--depth")
{
rest.get(depth_index + 1)
.and_then(|value| value.parse::<usize>().ok())
.unwrap_or(1)
} else if let Some(token) = rest.iter().find(|token| token.starts_with("--depth=")) {
token
.split_once('=')
.and_then(|(_, value)| value.parse::<usize>().ok())
.unwrap_or(1)
} else {
1
};
let result = crate::sync_skills::sync(
&self.name,
&collect_command_info(&self.commands, &[]),
&crate::sync_skills::SyncOptions {
cwd: None,
depth: Some(depth),
description: self.description.clone(),
global: !rest.iter().any(|token| token == "--no-global"),
include: None,
},
)
.await;
match result {
Ok(result) => {
let mut lines = vec![format!(
"Synced {} skill{}",
result.skills.len(),
if result.skills.len() == 1 { "" } else { "s" },
)];
for skill in &result.skills {
lines.push(format!(" {}", skill.name));
}
writeln_stdout(&lines.join("\n"));
if builtin.verbose || builtin.format_explicit {
let mut output = serde_json::Map::new();
output.insert(
"skills".to_string(),
Value::Array(
result
.paths
.iter()
.map(|path| Value::String(path.to_string_lossy().to_string()))
.collect(),
),
);
if builtin.verbose {
output.insert(
"agents".to_string(),
Value::Array(
result
.agents
.iter()
.map(|agent| {
serde_json::json!({
"agent": agent.agent,
"path": agent.path.to_string_lossy().to_string(),
"mode": match agent.mode {
crate::agents::InstallMode::Symlink => "symlink",
crate::agents::InstallMode::Copy => "copy",
},
})
})
.collect(),
),
);
}
writeln_stdout(&format_value(
&Value::Object(output),
if builtin.format_explicit {
builtin.format
} else {
Format::Toon
},
));
}
return Ok(());
}
Err(error) => {
writeln_stdout(&format_human_error(
"SYNC_SKILLS_FAILED",
&error.to_string(),
));
std::process::exit(1);
}
}
}
if let Some(index) = builtin_command_index(&builtin.rest, &self.name, "mcp") {
let builtin_def = builtins
.iter()
.find(|item| item.name == "mcp")
.expect("mcp builtin must exist");
if builtin.rest.get(index + 1).map(|token| token.as_str()) != Some("add") {
writeln_stdout(&format_builtin_help(&self.name, builtin_def));
return Ok(());
}
if builtin.help {
writeln_stdout(&format_builtin_subcommand_help(
&self.name,
builtin_def,
"add",
));
return Ok(());
}
let rest = builtin.rest[(index + 2)..].to_vec();
let mut command = None;
let mut agents = Vec::new();
let mut cursor = 0;
while cursor < rest.len() {
if (rest[cursor] == "--command" || rest[cursor] == "-c")
&& let Some(value) = rest.get(cursor + 1)
{
command = Some(value.clone());
cursor += 2;
continue;
}
if rest[cursor] == "--agent"
&& let Some(value) = rest.get(cursor + 1)
{
agents.push(value.clone());
cursor += 2;
continue;
}
cursor += 1;
}
let result = crate::sync_mcp::register(
&self.name,
&crate::sync_mcp::RegisterOptions {
agents: if agents.is_empty() {
None
} else {
Some(agents)
},
command,
global: !rest.iter().any(|token| token == "--no-global"),
},
)
.await;
match result {
Ok(result) => {
let mut lines = vec![format!("Registered {} as MCP server", self.name)];
if !result.agents.is_empty() {
lines.push(format!("Agents: {}", result.agents.join(", ")));
}
writeln_stdout(&lines.join("\n"));
if builtin.verbose || builtin.format_explicit {
writeln_stdout(&format_value(
&serde_json::json!({
"name": self.name,
"command": result.command,
"agents": result.agents,
}),
if builtin.format_explicit {
builtin.format
} else {
Format::Toon
},
));
}
return Ok(());
}
Err(error) => {
writeln_stdout(&format_human_error("MCP_ADD_FAILED", &error.to_string()));
std::process::exit(1);
}
}
}
if builtin.config_schema {
if self.config.is_none() {
writeln_stdout(&format_human_error(
"CONFIG_SCHEMA_UNAVAILABLE",
"--config-schema requires CLI config support.",
));
std::process::exit(1);
}
writeln_stdout(&format_config_schema(
self.root_command.as_ref(),
&self.commands,
)?);
return Ok(());
}
if builtin.rest.is_empty() {
if let Some(root_cmd) = &self.root_command {
if human && has_required_args(&root_cmd.args_fields) {
writeln_stdout(&format_command_help(
&self.name,
root_cmd,
&self.commands,
&self.aliases,
config_flag,
self.version.as_deref(),
true,
));
return Ok(());
}
} else if !builtin.help {
writeln_stdout(&help::format_root(
&self.name,
&FormatRootOptions {
aliases: if self.aliases.is_empty() {
None
} else {
Some(self.aliases.clone())
},
config_flag: config_flag.map(|s| s.to_string()),
commands: collect_help_commands(&self.commands),
description: self.description.clone(),
root: true,
version: self.version.clone(),
},
));
return Ok(());
}
}
let resolved = if builtin.rest.is_empty() {
if let Some(root_cmd) = &self.root_command {
ResolvedCommand::Leaf {
command: Arc::clone(root_cmd),
path: self.name.clone(),
rest: Vec::new(),
collected_middleware: Vec::new(),
output_policy: None,
}
} else {
ResolvedCommand::Help {
path: self.name.clone(),
description: self.description.clone(),
commands: &self.commands,
}
}
} else {
resolve_command(&self.commands, &builtin.rest)
};
if builtin.help {
match &resolved {
ResolvedCommand::Leaf { command, path, .. } => {
let is_root = path == &self.name;
let help_cmds = if is_root && !self.commands.is_empty() {
collect_help_commands(&self.commands)
} else {
Vec::new()
};
let command_name = if is_root {
self.name.clone()
} else {
format!("{} {path}", self.name)
};
writeln_stdout(&help::format_command(
&command_name,
&FormatCommandOptions {
aliases: if is_root && !self.aliases.is_empty() {
Some(self.aliases.clone())
} else {
None
},
args_fields: command.args_fields.clone(),
config_flag: config_flag.map(|s| s.to_string()),
commands: help_cmds,
description: command.description.clone(),
env_fields: command.env_fields.clone(),
examples: command.examples.clone(),
hint: command.hint.clone(),
hide_global_options: false,
options_fields: command.options_fields.clone(),
option_aliases: command.aliases.clone(),
root: is_root,
version: if is_root { self.version.clone() } else { None },
},
));
}
ResolvedCommand::Help {
path,
description,
commands,
} => {
let help_name = if path == &self.name {
self.name.clone()
} else {
format!("{} {path}", self.name)
};
let is_root = path == &self.name;
if is_root
&& let Some(root_cmd) = &self.root_command
&& !commands.is_empty()
{
writeln_stdout(&format_command_help(
&self.name,
root_cmd,
commands,
&self.aliases,
config_flag,
self.version.as_deref(),
true,
));
} else {
writeln_stdout(&help::format_root(
&help_name,
&FormatRootOptions {
aliases: if is_root && !self.aliases.is_empty() {
Some(self.aliases.clone())
} else {
None
},
config_flag: config_flag.map(|s| s.to_string()),
commands: collect_help_commands(commands),
description: description.clone(),
root: is_root,
version: if is_root { self.version.clone() } else { None },
},
));
}
}
ResolvedCommand::Gateway { path, .. } => {
let help_name = format!("{} {path}", self.name);
writeln_stdout(&format!(
"{help_name}: fetch gateway (use curl-style arguments)"
));
}
ResolvedCommand::Error { error: _, path } => {
let help_name = if path.is_empty() {
self.name.clone()
} else {
format!("{} {path}", self.name)
};
writeln_stdout(&help::format_root(
&help_name,
&FormatRootOptions {
aliases: None,
config_flag: config_flag.map(|s| s.to_string()),
commands: collect_help_commands(&self.commands),
description: self.description.clone(),
root: path.is_empty(),
version: None,
},
));
}
}
return Ok(());
}
if builtin.schema {
match &resolved {
ResolvedCommand::Leaf { command, .. } => {
if let Some(schema) = &command.output_schema {
writeln_stdout(&serde_json::to_string_pretty(schema)?);
} else {
writeln_stdout("{}");
}
}
_ => {
writeln_stdout("--schema requires a command.");
std::process::exit(1);
}
}
return Ok(());
}
if let ResolvedCommand::Gateway {
handler,
path,
rest,
base_path,
output_policy,
} = &resolved
{
let format = if builtin.format_explicit {
builtin.format
} else {
self.format.unwrap_or(Format::Json)
};
let policy = output_policy.or(self.output_policy);
let render_output =
!(human && !builtin.format_explicit && policy == Some(OutputPolicy::AgentOnly));
let mut fetch_input = fetch::parse_argv(rest);
if let Some(bp) = base_path {
let trimmed = bp.trim_end_matches('/');
fetch_input.path = format!("{trimmed}{}", fetch_input.path);
}
let output = handler.handle(fetch_input).await;
let data = format_fetch_output(&output);
if builtin.verbose {
let mut envelope = serde_json::Map::new();
envelope.insert("ok".to_string(), Value::Bool(output.ok));
envelope.insert("data".to_string(), data);
let mut meta = serde_json::Map::new();
meta.insert("command".to_string(), Value::String(path.clone()));
meta.insert("status".to_string(), Value::Number(output.status.into()));
envelope.insert("meta".to_string(), Value::Object(meta));
writeln_stdout(&format_value(&Value::Object(envelope), format));
} else if render_output {
writeln_stdout(&format_value(&data, format));
}
if !output.ok {
std::process::exit(1);
}
return Ok(());
}
let (command, command_path, rest, collected_mw, effective_output_policy) = match resolved {
ResolvedCommand::Leaf {
command,
path,
rest,
collected_middleware,
output_policy,
} => (command, path, rest, collected_middleware, output_policy),
ResolvedCommand::Gateway { .. } => unreachable!("Gateway handled before step 7"),
ResolvedCommand::Help {
path,
description,
commands,
} => {
let help_name = if path == self.name {
self.name.clone()
} else {
format!("{} {path}", self.name)
};
writeln_stdout(&help::format_root(
&help_name,
&FormatRootOptions {
aliases: None,
config_flag: config_flag.map(|s| s.to_string()),
commands: collect_help_commands(commands),
description: description.clone(),
root: path == self.name,
version: None,
},
));
return Ok(());
}
ResolvedCommand::Error { error, path } => {
if path.is_empty() {
if let Some(root_cmd) = &self.root_command {
(
Arc::clone(root_cmd),
self.name.clone(),
builtin.rest.clone(),
Vec::new(),
None,
)
} else {
let parent = if path.is_empty() { &self.name } else { &path };
let message = format!("'{error}' is not a command for '{parent}'.");
if human {
writeln_stdout(&format_human_error("COMMAND_NOT_FOUND", &message));
writeln_stdout(&format!(
"\nSuggested commands:\n {} --help",
self.name
));
} else {
writeln_stdout(&format!(
"{{\"ok\":false,\"error\":{{\"code\":\"COMMAND_NOT_FOUND\",\"message\":\"{}\"}}}}",
message.replace('"', "\\\"")
));
}
std::process::exit(1);
}
} else {
let parent = format!("{} {path}", self.name);
let message = format!("'{error}' is not a command for '{parent}'.");
if human {
writeln_stdout(&format_human_error("COMMAND_NOT_FOUND", &message));
} else {
writeln_stdout(&format!(
"{{\"ok\":false,\"error\":{{\"code\":\"COMMAND_NOT_FOUND\",\"message\":\"{}\"}}}}",
message.replace('"', "\\\"")
));
}
std::process::exit(1);
}
}
};
let start = std::time::Instant::now();
let format = if builtin.format_explicit {
builtin.format
} else {
command
.format
.unwrap_or_else(|| self.format.unwrap_or(Format::Toon))
};
let policy = effective_output_policy
.or(command.output_policy)
.or(self.output_policy);
let render_output =
!(human && !builtin.format_explicit && policy == Some(OutputPolicy::AgentOnly));
let defaults = if let Some(ref cfg) = self.config {
if builtin.config_disabled {
None
} else {
let config_path =
config::resolve_config_path(builtin.config_path.as_deref(), &cfg.files);
if let Some(path) = config_path {
match config::load_config(&path) {
Ok(tree) => {
match config::extract_command_section(&tree, &self.name, &command_path)
{
Ok(section) => section,
Err(e) => {
writeln_stdout(&format_human_error(
"CONFIG_ERROR",
&e.to_string(),
));
std::process::exit(1);
}
}
}
Err(e) => {
if builtin.config_path.is_some() {
writeln_stdout(&format_human_error("CONFIG_ERROR", &e.to_string()));
std::process::exit(1);
}
None
}
}
} else {
None
}
}
} else {
None
};
let all_middleware: Vec<MiddlewareFn> = self
.middleware
.iter()
.cloned()
.chain(collected_mw.into_iter())
.chain(command.middleware.iter().cloned())
.collect();
let env_source: std::collections::HashMap<String, String> = std::env::vars().collect();
let result = command::execute(
Arc::clone(&command),
ExecuteOptions {
agent: !human,
argv: rest,
defaults,
env_fields: self.env_fields.clone(),
env_source,
format,
format_explicit: builtin.format_explicit,
input_options: BTreeMap::new(),
middlewares: all_middleware,
name: self.name.clone(),
parse_mode: ParseMode::Argv,
path: command_path.clone(),
vars_fields: self.vars_fields.clone(),
version: self.version.clone(),
},
)
.await;
let duration = start.elapsed();
let duration_str = format!("{}ms", duration.as_millis());
match result {
InternalResult::Ok { data, cta } => {
let data = if let Some(ref expr) = builtin.filter_output {
let paths = filter::parse(expr);
filter::apply(&data, &paths)
} else {
data
};
let formatted_cta = format_cta_block(&self.name, cta.as_ref());
if builtin.verbose {
let mut envelope = serde_json::Map::new();
envelope.insert("ok".to_string(), Value::Bool(true));
envelope.insert("data".to_string(), data);
let mut meta = serde_json::Map::new();
meta.insert("command".to_string(), Value::String(command_path));
meta.insert("duration".to_string(), Value::String(duration_str));
if let Some(cta) = &formatted_cta {
meta.insert(
"cta".to_string(),
serde_json::to_value(cta).unwrap_or(Value::Null),
);
}
envelope.insert("meta".to_string(), Value::Object(meta));
let output = format_value(&Value::Object(envelope), format);
write_with_token_ops(&output, &builtin, writeln_stdout);
} else if human {
if render_output {
let output = format_value(&data, format);
write_with_token_ops(&output, &builtin, writeln_stdout);
}
if let Some(cta) = &formatted_cta {
writeln_stdout(&format_human_cta(cta));
}
} else {
if let Some(cta) = &formatted_cta {
if let Value::Object(ref map) = data {
let mut out = map.clone();
out.insert(
"cta".to_string(),
serde_json::to_value(cta).unwrap_or(Value::Null),
);
let output = format_value(&Value::Object(out), format);
write_with_token_ops(&output, &builtin, writeln_stdout);
} else {
let output = format_value(&data, format);
write_with_token_ops(&output, &builtin, writeln_stdout);
}
} else {
let output = format_value(&data, format);
write_with_token_ops(&output, &builtin, writeln_stdout);
}
}
}
InternalResult::Error {
code,
message,
retryable,
field_errors: _,
cta,
exit_code,
} => {
let formatted_cta = format_cta_block(&self.name, cta.as_ref());
if builtin.verbose {
let mut envelope = serde_json::Map::new();
envelope.insert("ok".to_string(), Value::Bool(false));
let mut error_obj = serde_json::Map::new();
error_obj.insert("code".to_string(), Value::String(code.clone()));
error_obj.insert("message".to_string(), Value::String(message.clone()));
if let Some(r) = retryable {
error_obj.insert("retryable".to_string(), Value::Bool(r));
}
envelope.insert("error".to_string(), Value::Object(error_obj));
let mut meta = serde_json::Map::new();
meta.insert("command".to_string(), Value::String(command_path));
meta.insert("duration".to_string(), Value::String(duration_str));
envelope.insert("meta".to_string(), Value::Object(meta));
writeln_stdout(&format_value(&Value::Object(envelope), format));
} else if human && !builtin.format_explicit {
writeln_stdout(&format_human_error(&code, &message));
if let Some(cta) = &formatted_cta {
writeln_stdout(&format_human_cta(cta));
}
} else {
let mut error_obj = serde_json::Map::new();
error_obj.insert("code".to_string(), Value::String(code.clone()));
error_obj.insert("message".to_string(), Value::String(message.clone()));
if let Some(cta) = &formatted_cta {
error_obj.insert(
"cta".to_string(),
serde_json::to_value(cta).unwrap_or(Value::Null),
);
}
writeln_stdout(&format_value(&Value::Object(error_obj), format));
}
std::process::exit(exit_code.unwrap_or(1));
}
InternalResult::Stream(stream) => {
handle_streaming(
stream,
StreamingOptions {
path: &command_path,
start,
format,
format_explicit: builtin.format_explicit,
human,
render_output,
verbose: builtin.verbose,
},
)
.await;
}
}
Ok(())
}
pub async fn serve_to(
&self,
argv: Vec<String>,
writer: &mut dyn std::io::Write,
human: bool,
) -> Result<Option<i32>, Box<dyn std::error::Error>> {
let config_flag = self.config.as_ref().map(|c| c.flag.as_str());
macro_rules! wln {
($s:expr) => {{
let s: &str = $s;
if s.ends_with('\n') {
write!(writer, "{s}").ok();
} else {
writeln!(writer, "{s}").ok();
}
}};
}
let builtin = match extract_builtin_flags(&argv, config_flag) {
Ok(b) => b,
Err(e) => {
let msg = e.to_string();
if human {
wln!(&format_human_error("UNKNOWN", &msg));
} else {
wln!(&format!(
"{{\"ok\":false,\"error\":{{\"code\":\"UNKNOWN\",\"message\":\"{}\"}}}}",
msg.replace('"', "\\\"")
));
}
return Ok(Some(1));
}
};
if builtin.version && !builtin.help {
if let Some(v) = &self.version {
wln!(v);
return Ok(None);
}
}
if builtin.llms || builtin.llms_full {
let commands_info = collect_command_info(&self.commands, &[]);
if builtin.llms_full {
let groups = collect_group_descriptions(&self.commands, &[]);
let output = skill::generate(&self.name, &commands_info, &groups);
wln!(&output);
} else {
let output = skill::index(&self.name, &commands_info, self.description.as_deref());
wln!(&output);
}
return Ok(None);
}
if builtin.mcp {
#[cfg(feature = "mcp")]
{
let version = self.version.as_deref().unwrap_or("0.0.0");
crate::mcp::serve(
&self.name,
version,
&self.commands,
&self.middleware,
&self.env_fields,
&Default::default(),
)
.await?;
return Ok(None);
}
#[cfg(not(feature = "mcp"))]
{
wln!("MCP support requires the 'mcp' feature flag.");
return Ok(Some(1));
}
}
if let Some(output) = completion_output(
&self.name,
&self.aliases,
&self.commands,
self.root_command.as_ref(),
&argv,
std::env::var("COMPLETE").ok().as_deref(),
std::env::var("_COMPLETE_INDEX").ok().as_deref(),
) {
wln!(&output);
return Ok(None);
}
let builtins = builtin_commands(&self.name);
if let Some(index) = builtin_command_index(&builtin.rest, &self.name, "completions") {
let builtin_def = builtins
.iter()
.find(|item| item.name == "completions")
.expect("completions builtin must exist");
let shell = builtin.rest.get(index + 1).map(|token| token.as_str());
if builtin.help || shell.is_none() {
wln!(&help::format_command(
&format!("{} completions", self.name),
&FormatCommandOptions {
aliases: None,
args_fields: builtin_def.args_fields.clone(),
config_flag: None,
commands: Vec::new(),
description: Some(builtin_def.description.to_string()),
env_fields: Vec::new(),
examples: Vec::new(),
hint: builtin_def.hint.clone(),
hide_global_options: true,
options_fields: Vec::new(),
option_aliases: HashMap::new(),
root: false,
version: None,
},
));
return Ok(None);
}
let shell = shell.expect("checked above");
if crate::completions::Shell::from_str(shell).is_none() {
wln!(&format_human_error(
"INVALID_SHELL",
&format!("Unknown shell '{shell}'. Supported: bash, fish, nushell, zsh"),
));
return Ok(Some(1));
}
let output = std::iter::once(self.name.clone())
.chain(self.aliases.iter().cloned())
.map(|name| {
crate::completions::register(
crate::completions::Shell::from_str(shell).expect("checked above"),
&name,
)
})
.collect::<Vec<_>>()
.join("\n");
wln!(&output);
return Ok(None);
}
if let Some(index) = builtin_command_index(&builtin.rest, &self.name, "skills") {
let builtin_def = builtins
.iter()
.find(|item| item.name == "skills")
.expect("skills builtin must exist");
if builtin.rest.get(index + 1).map(|token| token.as_str()) != Some("add") {
wln!(&format_builtin_help(&self.name, builtin_def));
return Ok(None);
}
if builtin.help {
wln!(&format_builtin_subcommand_help(
&self.name,
builtin_def,
"add"
));
return Ok(None);
}
let rest = builtin.rest[(index + 2)..].to_vec();
let depth = if let Some(depth_index) = rest.iter().position(|token| token == "--depth")
{
rest.get(depth_index + 1)
.and_then(|value| value.parse::<usize>().ok())
.unwrap_or(1)
} else if let Some(token) = rest.iter().find(|token| token.starts_with("--depth=")) {
token
.split_once('=')
.and_then(|(_, value)| value.parse::<usize>().ok())
.unwrap_or(1)
} else {
1
};
let result = crate::sync_skills::sync(
&self.name,
&collect_command_info(&self.commands, &[]),
&crate::sync_skills::SyncOptions {
cwd: None,
depth: Some(depth),
description: self.description.clone(),
global: !rest.iter().any(|token| token == "--no-global"),
include: None,
},
)
.await;
match result {
Ok(result) => {
let mut lines = vec![format!(
"Synced {} skill{}",
result.skills.len(),
if result.skills.len() == 1 { "" } else { "s" },
)];
for skill in &result.skills {
lines.push(format!(" {}", skill.name));
}
wln!(&lines.join("\n"));
if builtin.verbose || builtin.format_explicit {
let mut output = serde_json::Map::new();
output.insert(
"skills".to_string(),
Value::Array(
result
.paths
.iter()
.map(|path| Value::String(path.to_string_lossy().to_string()))
.collect(),
),
);
if builtin.verbose {
output.insert(
"agents".to_string(),
Value::Array(
result
.agents
.iter()
.map(|agent| {
serde_json::json!({
"agent": agent.agent,
"path": agent.path.to_string_lossy().to_string(),
"mode": match agent.mode {
crate::agents::InstallMode::Symlink => "symlink",
crate::agents::InstallMode::Copy => "copy",
},
})
})
.collect(),
),
);
}
wln!(&format_value(
&Value::Object(output),
if builtin.format_explicit {
builtin.format
} else {
Format::Toon
},
));
}
return Ok(None);
}
Err(error) => {
wln!(&format_human_error(
"SYNC_SKILLS_FAILED",
&error.to_string()
));
return Ok(Some(1));
}
}
}
if let Some(index) = builtin_command_index(&builtin.rest, &self.name, "mcp") {
let builtin_def = builtins
.iter()
.find(|item| item.name == "mcp")
.expect("mcp builtin must exist");
if builtin.rest.get(index + 1).map(|token| token.as_str()) != Some("add") {
wln!(&format_builtin_help(&self.name, builtin_def));
return Ok(None);
}
if builtin.help {
wln!(&format_builtin_subcommand_help(
&self.name,
builtin_def,
"add"
));
return Ok(None);
}
let rest = builtin.rest[(index + 2)..].to_vec();
let mut command = None;
let mut agents = Vec::new();
let mut cursor = 0;
while cursor < rest.len() {
if (rest[cursor] == "--command" || rest[cursor] == "-c")
&& let Some(value) = rest.get(cursor + 1)
{
command = Some(value.clone());
cursor += 2;
continue;
}
if rest[cursor] == "--agent"
&& let Some(value) = rest.get(cursor + 1)
{
agents.push(value.clone());
cursor += 2;
continue;
}
cursor += 1;
}
let result = crate::sync_mcp::register(
&self.name,
&crate::sync_mcp::RegisterOptions {
agents: if agents.is_empty() {
None
} else {
Some(agents)
},
command,
global: !rest.iter().any(|token| token == "--no-global"),
},
)
.await;
match result {
Ok(result) => {
let mut lines = vec![format!("Registered {} as MCP server", self.name)];
if !result.agents.is_empty() {
lines.push(format!("Agents: {}", result.agents.join(", ")));
}
wln!(&lines.join("\n"));
if builtin.verbose || builtin.format_explicit {
wln!(&format_value(
&serde_json::json!({
"name": self.name,
"command": result.command,
"agents": result.agents,
}),
if builtin.format_explicit {
builtin.format
} else {
Format::Toon
},
));
}
return Ok(None);
}
Err(error) => {
wln!(&format_human_error("MCP_ADD_FAILED", &error.to_string()));
return Ok(Some(1));
}
}
}
if builtin.config_schema {
if self.config.is_none() {
wln!(&format_human_error(
"CONFIG_SCHEMA_UNAVAILABLE",
"--config-schema requires CLI config support.",
));
return Ok(Some(1));
}
wln!(&format_config_schema(
self.root_command.as_ref(),
&self.commands
)?);
return Ok(None);
}
if builtin.rest.is_empty() {
if let Some(root_cmd) = &self.root_command {
if human && has_required_args(&root_cmd.args_fields) {
wln!(&format_command_help(
&self.name,
root_cmd,
&self.commands,
&self.aliases,
config_flag,
self.version.as_deref(),
true,
));
return Ok(None);
}
} else if !builtin.help {
wln!(&help::format_root(
&self.name,
&FormatRootOptions {
aliases: if self.aliases.is_empty() {
None
} else {
Some(self.aliases.clone())
},
config_flag: config_flag.map(|s| s.to_string()),
commands: collect_help_commands(&self.commands),
description: self.description.clone(),
root: true,
version: self.version.clone(),
},
));
return Ok(None);
}
}
let resolved = if builtin.rest.is_empty() {
if let Some(root_cmd) = &self.root_command {
ResolvedCommand::Leaf {
command: Arc::clone(root_cmd),
path: self.name.clone(),
rest: Vec::new(),
collected_middleware: Vec::new(),
output_policy: None,
}
} else {
ResolvedCommand::Help {
path: self.name.clone(),
description: self.description.clone(),
commands: &self.commands,
}
}
} else {
resolve_command(&self.commands, &builtin.rest)
};
if builtin.help {
match &resolved {
ResolvedCommand::Leaf { command, path, .. } => {
let is_root = path == &self.name;
let help_cmds = if is_root && !self.commands.is_empty() {
collect_help_commands(&self.commands)
} else {
Vec::new()
};
let command_name = if is_root {
self.name.clone()
} else {
format!("{} {path}", self.name)
};
wln!(&help::format_command(
&command_name,
&FormatCommandOptions {
aliases: if is_root && !self.aliases.is_empty() {
Some(self.aliases.clone())
} else {
None
},
args_fields: command.args_fields.clone(),
config_flag: config_flag.map(|s| s.to_string()),
commands: help_cmds,
description: command.description.clone(),
env_fields: command.env_fields.clone(),
examples: command.examples.clone(),
hint: command.hint.clone(),
hide_global_options: false,
options_fields: command.options_fields.clone(),
option_aliases: command.aliases.clone(),
root: is_root,
version: if is_root { self.version.clone() } else { None },
},
));
}
ResolvedCommand::Help {
path,
description,
commands,
} => {
let help_name = if path == &self.name {
self.name.clone()
} else {
format!("{} {path}", self.name)
};
let is_root = path == &self.name;
if is_root
&& let Some(root_cmd) = &self.root_command
&& !commands.is_empty()
{
wln!(&format_command_help(
&self.name,
root_cmd,
commands,
&self.aliases,
config_flag,
self.version.as_deref(),
true,
));
} else {
wln!(&help::format_root(
&help_name,
&FormatRootOptions {
aliases: if is_root && !self.aliases.is_empty() {
Some(self.aliases.clone())
} else {
None
},
config_flag: config_flag.map(|s| s.to_string()),
commands: collect_help_commands(commands),
description: description.clone(),
root: is_root,
version: if is_root { self.version.clone() } else { None },
},
));
}
}
ResolvedCommand::Gateway { path, .. } => {
let help_name = format!("{} {path}", self.name);
wln!(&format!(
"{help_name}: fetch gateway (use curl-style arguments)"
));
}
ResolvedCommand::Error { error: _, path } => {
let help_name = if path.is_empty() {
self.name.clone()
} else {
format!("{} {path}", self.name)
};
wln!(&help::format_root(
&help_name,
&FormatRootOptions {
aliases: None,
config_flag: config_flag.map(|s| s.to_string()),
commands: collect_help_commands(&self.commands),
description: self.description.clone(),
root: path.is_empty(),
version: None,
},
));
}
}
return Ok(None);
}
if builtin.schema {
match &resolved {
ResolvedCommand::Leaf { command, .. } => {
if let Some(schema) = &command.output_schema {
wln!(&serde_json::to_string_pretty(schema)?);
} else {
wln!("{}");
}
}
_ => {
wln!("--schema requires a command.");
return Ok(Some(1));
}
}
return Ok(None);
}
if let ResolvedCommand::Gateway {
handler,
path,
rest,
base_path,
output_policy,
} = &resolved
{
let format = if builtin.format_explicit {
builtin.format
} else {
self.format.unwrap_or(Format::Json)
};
let policy = output_policy.or(self.output_policy);
let render_output =
!(human && !builtin.format_explicit && policy == Some(OutputPolicy::AgentOnly));
let mut fetch_input = fetch::parse_argv(rest);
if let Some(bp) = base_path {
let trimmed = bp.trim_end_matches('/');
fetch_input.path = format!("{trimmed}{}", fetch_input.path);
}
let output = handler.handle(fetch_input).await;
let data = format_fetch_output(&output);
if builtin.verbose {
let mut envelope = serde_json::Map::new();
envelope.insert("ok".to_string(), Value::Bool(output.ok));
envelope.insert("data".to_string(), data);
let mut meta = serde_json::Map::new();
meta.insert("command".to_string(), Value::String(path.clone()));
meta.insert("status".to_string(), Value::Number(output.status.into()));
envelope.insert("meta".to_string(), Value::Object(meta));
wln!(&format_value(&Value::Object(envelope), format));
} else if render_output {
wln!(&format_value(&data, format));
}
if !output.ok {
return Ok(Some(1));
}
return Ok(None);
}
let (command, command_path, rest, collected_mw, effective_output_policy) = match resolved {
ResolvedCommand::Leaf {
command,
path,
rest,
collected_middleware,
output_policy,
} => (command, path, rest, collected_middleware, output_policy),
ResolvedCommand::Gateway { .. } => unreachable!("Gateway handled before step 7"),
ResolvedCommand::Help {
path,
description,
commands,
} => {
let help_name = if path == self.name {
self.name.clone()
} else {
format!("{} {path}", self.name)
};
wln!(&help::format_root(
&help_name,
&FormatRootOptions {
aliases: None,
config_flag: config_flag.map(|s| s.to_string()),
commands: collect_help_commands(commands),
description: description.clone(),
root: path == self.name,
version: None,
},
));
return Ok(None);
}
ResolvedCommand::Error { error, path } => {
if path.is_empty() {
if let Some(root_cmd) = &self.root_command {
(
Arc::clone(root_cmd),
self.name.clone(),
builtin.rest.clone(),
Vec::new(),
None,
)
} else {
let parent = if path.is_empty() { &self.name } else { &path };
let message = format!("'{error}' is not a command for '{parent}'.");
if human {
wln!(&format_human_error("COMMAND_NOT_FOUND", &message));
wln!(&format!("\nSuggested commands:\n {} --help", self.name));
} else {
let cta_json = serde_json::json!({
"code": "COMMAND_NOT_FOUND",
"message": message,
"cta": {
"description": "See available commands:",
"commands": [{ "command": format!("{} --help", self.name) }]
}
});
wln!(&format_value(&cta_json, builtin.format));
}
return Ok(Some(1));
}
} else {
let parent = format!("{} {path}", self.name);
let message = format!("'{error}' is not a command for '{parent}'.");
if human {
wln!(&format_human_error("COMMAND_NOT_FOUND", &message));
} else {
let cta_json = serde_json::json!({
"code": "COMMAND_NOT_FOUND",
"message": message,
"cta": {
"description": "See available commands:",
"commands": [{ "command": format!("{} --help", parent) }]
}
});
wln!(&format_value(&cta_json, builtin.format));
}
return Ok(Some(1));
}
}
};
let start = std::time::Instant::now();
let format = if builtin.format_explicit {
builtin.format
} else {
command
.format
.unwrap_or_else(|| self.format.unwrap_or(Format::Toon))
};
let policy = effective_output_policy
.or(command.output_policy)
.or(self.output_policy);
let render_output =
!(human && !builtin.format_explicit && policy == Some(OutputPolicy::AgentOnly));
let defaults = if let Some(ref cfg) = self.config {
if builtin.config_disabled {
None
} else {
let config_path =
config::resolve_config_path(builtin.config_path.as_deref(), &cfg.files);
if let Some(path) = config_path {
match config::load_config(&path) {
Ok(tree) => {
match config::extract_command_section(&tree, &self.name, &command_path)
{
Ok(section) => section,
Err(e) => {
wln!(&format_human_error("CONFIG_ERROR", &e.to_string()));
return Ok(Some(1));
}
}
}
Err(e) => {
if builtin.config_path.is_some() {
wln!(&format_human_error("CONFIG_ERROR", &e.to_string()));
return Ok(Some(1));
}
None
}
}
} else {
None
}
}
} else {
None
};
let all_middleware: Vec<MiddlewareFn> = self
.middleware
.iter()
.cloned()
.chain(collected_mw.into_iter())
.chain(command.middleware.iter().cloned())
.collect();
let env_source: std::collections::HashMap<String, String> = std::env::vars().collect();
let result = command::execute(
Arc::clone(&command),
ExecuteOptions {
agent: !human,
argv: rest,
defaults,
env_fields: self.env_fields.clone(),
env_source,
format,
format_explicit: builtin.format_explicit,
input_options: BTreeMap::new(),
middlewares: all_middleware,
name: self.name.clone(),
parse_mode: ParseMode::Argv,
path: command_path.clone(),
vars_fields: self.vars_fields.clone(),
version: self.version.clone(),
},
)
.await;
let duration = start.elapsed();
let duration_str = format!("{}ms", duration.as_millis());
match result {
InternalResult::Ok { data, cta } => {
let data = if let Some(ref expr) = builtin.filter_output {
let paths = filter::parse(expr);
filter::apply(&data, &paths)
} else {
data
};
let formatted_cta = format_cta_block(&self.name, cta.as_ref());
macro_rules! wln_tok {
($s:expr) => {{
let s: &str = $s;
if let Some(token_output) = apply_token_ops(s, &builtin) {
wln!(&token_output);
} else {
wln!(s);
}
}};
}
if builtin.verbose {
let mut envelope = serde_json::Map::new();
envelope.insert("ok".to_string(), Value::Bool(true));
envelope.insert("data".to_string(), data);
let mut meta = serde_json::Map::new();
meta.insert("command".to_string(), Value::String(command_path));
meta.insert("duration".to_string(), Value::String(duration_str));
if let Some(cta) = &formatted_cta {
meta.insert(
"cta".to_string(),
serde_json::to_value(cta).unwrap_or(Value::Null),
);
}
envelope.insert("meta".to_string(), Value::Object(meta));
let output = format_value(&Value::Object(envelope), format);
wln_tok!(&output);
} else if human {
if render_output {
let output = format_value(&data, format);
wln_tok!(&output);
}
if let Some(cta) = &formatted_cta {
wln!(&format_human_cta(cta));
}
} else {
let output = format_value(&data, format);
wln_tok!(&output);
}
Ok(None)
}
InternalResult::Error {
code,
message,
retryable,
field_errors: _,
cta,
exit_code,
} => {
let formatted_cta = format_cta_block(&self.name, cta.as_ref());
if builtin.verbose {
let mut envelope = serde_json::Map::new();
envelope.insert("ok".to_string(), Value::Bool(false));
let mut error_obj = serde_json::Map::new();
error_obj.insert("code".to_string(), Value::String(code.clone()));
error_obj.insert("message".to_string(), Value::String(message.clone()));
if let Some(r) = retryable {
error_obj.insert("retryable".to_string(), Value::Bool(r));
}
envelope.insert("error".to_string(), Value::Object(error_obj));
let mut meta = serde_json::Map::new();
meta.insert("command".to_string(), Value::String(command_path));
meta.insert("duration".to_string(), Value::String(duration_str));
if let Some(cta) = &formatted_cta {
meta.insert(
"cta".to_string(),
serde_json::to_value(cta).unwrap_or(Value::Null),
);
}
envelope.insert("meta".to_string(), Value::Object(meta));
wln!(&format_value(&Value::Object(envelope), format));
} else if human && !builtin.format_explicit {
wln!(&format_human_error(&code, &message));
if let Some(cta) = &formatted_cta {
wln!(&format_human_cta(cta));
}
} else {
let mut error_obj = serde_json::Map::new();
error_obj.insert("code".to_string(), Value::String(code.clone()));
error_obj.insert("message".to_string(), Value::String(message.clone()));
if let Some(r) = retryable {
error_obj.insert("retryable".to_string(), Value::Bool(r));
}
wln!(&format_value(&Value::Object(error_obj), format));
}
Ok(Some(exit_code.unwrap_or(1)))
}
InternalResult::Stream(mut stream) => {
use futures::StreamExt;
let use_jsonl = format == Format::Jsonl;
let incremental = use_jsonl || (!builtin.format_explicit && format == Format::Toon);
if incremental {
while let Some(value) = stream.next().await {
if use_jsonl {
let chunk = serde_json::json!({ "type": "chunk", "data": value });
wln!(&serde_json::to_string(&chunk).unwrap_or_default());
} else if render_output {
wln!(&format_value(&value, format));
}
}
if use_jsonl {
let done = serde_json::json!({
"type": "done",
"ok": true,
"meta": {
"command": command_path,
"duration": format!("{}ms", start.elapsed().as_millis()),
}
});
wln!(&serde_json::to_string(&done).unwrap_or_default());
}
} else {
let mut chunks: Vec<Value> = Vec::new();
while let Some(value) = stream.next().await {
chunks.push(value);
}
let data = Value::Array(chunks);
let dur = format!("{}ms", start.elapsed().as_millis());
if builtin.verbose {
let envelope = serde_json::json!({
"ok": true,
"data": data,
"meta": {
"command": command_path,
"duration": dur,
}
});
wln!(&format_value(&envelope, format));
} else if human {
if render_output {
wln!(&format_value(&data, format));
}
} else {
wln!(&format_value(&data, format));
}
}
Ok(None)
}
}
}
}
enum ResolvedCommand<'a> {
Leaf {
command: Arc<CommandDef>,
path: String,
rest: Vec<String>,
collected_middleware: Vec<MiddlewareFn>,
output_policy: Option<OutputPolicy>,
},
Gateway {
handler: &'a Arc<dyn FetchHandler>,
path: String,
rest: Vec<String>,
base_path: Option<String>,
output_policy: Option<OutputPolicy>,
},
Help {
path: String,
description: Option<String>,
commands: &'a BTreeMap<String, CommandEntry>,
},
Error { error: String, path: String },
}
fn resolve_command<'a>(
commands: &'a BTreeMap<String, CommandEntry>,
tokens: &[String],
) -> ResolvedCommand<'a> {
let (first, rest) = match tokens.split_first() {
Some((f, r)) => (f, r),
None => {
return ResolvedCommand::Error {
error: "(none)".to_string(),
path: String::new(),
};
}
};
let entry = match commands.get(first.as_str()) {
Some(e) => e,
None => {
return ResolvedCommand::Error {
error: first.clone(),
path: String::new(),
};
}
};
let mut path = vec![first.as_str()];
let mut remaining = rest;
let mut inherited_output_policy: Option<OutputPolicy> = None;
let mut collected_middleware: Vec<MiddlewareFn> = Vec::new();
let mut current = entry;
loop {
match current {
CommandEntry::Leaf(def) => {
let output_policy = def.output_policy.or(inherited_output_policy);
return ResolvedCommand::Leaf {
command: Arc::clone(def),
path: path.join(" "),
rest: remaining.to_vec(),
collected_middleware,
output_policy,
};
}
CommandEntry::Group {
description,
commands: sub_commands,
middleware,
output_policy,
} => {
if let Some(policy) = output_policy {
inherited_output_policy = Some(*policy);
}
collected_middleware.extend(middleware.iter().cloned());
let next = match remaining.first() {
Some(n) => n,
None => {
return ResolvedCommand::Help {
path: path.join(" "),
description: description.clone(),
commands: sub_commands,
};
}
};
match sub_commands.get(next.as_str()) {
Some(child) => {
path.push(next.as_str());
remaining = &remaining[1..];
current = child;
}
None => {
return ResolvedCommand::Error {
error: next.clone(),
path: path.join(" "),
};
}
}
}
CommandEntry::FetchGateway {
base_path,
output_policy,
handler,
..
} => {
let effective_policy = output_policy.or(inherited_output_policy);
return ResolvedCommand::Gateway {
handler,
path: path.join(" "),
rest: remaining.to_vec(),
base_path: base_path.clone(),
output_policy: effective_policy,
};
}
}
}
}
struct BuiltinFlags {
verbose: bool,
format: Format,
format_explicit: bool,
filter_output: Option<String>,
token_limit: Option<usize>,
token_offset: Option<usize>,
token_count: bool,
llms: bool,
llms_full: bool,
mcp: bool,
help: bool,
version: bool,
schema: bool,
config_schema: bool,
config_path: Option<String>,
config_disabled: bool,
rest: Vec<String>,
}
fn extract_builtin_flags(
argv: &[String],
config_flag: Option<&str>,
) -> Result<BuiltinFlags, Box<dyn std::error::Error>> {
let mut verbose = false;
let mut llms = false;
let mut llms_full = false;
let mut mcp = false;
let mut help = false;
let mut version = false;
let mut schema = false;
let mut config_schema = false;
let mut format = Format::Toon;
let mut format_explicit = false;
let mut config_path: Option<String> = None;
let mut config_disabled = false;
let mut filter_output: Option<String> = None;
let mut token_limit: Option<usize> = None;
let mut token_offset: Option<usize> = None;
let mut token_count = false;
let mut rest: Vec<String> = Vec::new();
let cfg_flag = config_flag.map(|f| format!("--{f}"));
let cfg_flag_eq = config_flag.map(|f| format!("--{f}="));
let no_cfg_flag = config_flag.map(|f| format!("--no-{f}"));
let mut i = 0;
while i < argv.len() {
let token = &argv[i];
if token == "--verbose" {
verbose = true;
} else if token == "--llms" {
llms = true;
} else if token == "--llms-full" {
llms_full = true;
} else if token == "--mcp" {
mcp = true;
} else if token == "--help" || token == "-h" {
help = true;
} else if token == "--version" {
version = true;
} else if token == "--schema" {
schema = true;
} else if token == "--config-schema" {
config_schema = true;
} else if token == "--json" {
format = Format::Json;
format_explicit = true;
} else if token == "--table" {
format = Format::Table;
format_explicit = true;
} else if token == "--csv" {
format = Format::Csv;
format_explicit = true;
} else if token == "--format" {
if let Some(next) = argv.get(i + 1) {
if let Some(f) = Format::from_str_opt(next) {
format = f;
} else {
format = Format::Toon; }
format_explicit = true;
i += 1;
}
} else if let Some(ref cfg) = cfg_flag {
if token == cfg {
if let Some(next) = argv.get(i + 1) {
config_path = Some(next.clone());
config_disabled = false;
i += 1;
} else {
return Err(format!("Missing value for flag: {cfg}").into());
}
} else if let Some(ref eq) = cfg_flag_eq {
if token.starts_with(eq.as_str()) {
let value = &token[eq.len()..];
if value.is_empty() {
return Err(format!("Missing value for flag: {cfg}").into());
}
config_path = Some(value.to_string());
config_disabled = false;
} else if let Some(ref no) = no_cfg_flag {
if token == no {
config_path = None;
config_disabled = true;
} else {
rest.push(token.clone());
}
} else {
rest.push(token.clone());
}
} else if let Some(ref no) = no_cfg_flag {
if token == no {
config_path = None;
config_disabled = true;
} else {
rest.push(token.clone());
}
} else {
rest.push(token.clone());
}
} else if token == "--filter-output" {
if let Some(next) = argv.get(i + 1) {
filter_output = Some(next.clone());
i += 1;
}
} else if token == "--token-limit" {
if let Some(next) = argv.get(i + 1) {
token_limit = next.parse().ok();
i += 1;
}
} else if token == "--token-offset" {
if let Some(next) = argv.get(i + 1) {
token_offset = next.parse().ok();
i += 1;
}
} else if token == "--token-count" {
token_count = true;
} else {
rest.push(token.clone());
}
i += 1;
}
Ok(BuiltinFlags {
verbose,
format,
format_explicit,
filter_output,
token_limit,
token_offset,
token_count,
llms,
llms_full,
mcp,
help,
version,
schema,
config_schema,
config_path,
config_disabled,
rest,
})
}
fn format_fetch_output(output: &fetch::FetchOutput) -> Value {
if output.ok {
output.data.clone()
} else {
serde_json::json!({
"ok": false,
"status": output.status,
"error": output.data,
})
}
}
fn writeln_stdout(s: &str) {
if s.ends_with('\n') {
print!("{s}");
} else {
println!("{s}");
}
}
fn format_value(value: &Value, fmt: Format) -> String {
crate::formatter::format(value, fmt)
}
fn format_human_error(code: &str, message: &str) -> String {
let prefix = if code == "UNKNOWN" || code == "COMMAND_NOT_FOUND" {
"Error"
} else {
&format!("Error ({code})")
};
format!("{prefix}: {message}")
}
#[derive(Debug, Clone, serde::Serialize)]
struct FormattedCtaBlock {
description: String,
commands: Vec<FormattedCta>,
}
#[derive(Debug, Clone, serde::Serialize)]
struct FormattedCta {
command: String,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
}
fn format_cta_block(name: &str, block: Option<&CtaBlock>) -> Option<FormattedCtaBlock> {
let block = block?;
if block.commands.is_empty() {
return None;
}
let commands = block
.commands
.iter()
.map(|entry| match entry {
CtaEntry::Simple(s) => FormattedCta {
command: format!("{name} {s}"),
description: None,
},
CtaEntry::Detailed {
command,
description,
} => {
let prefix = if command == name || command.starts_with(&format!("{name} ")) {
String::new()
} else {
format!("{name} ")
};
FormattedCta {
command: format!("{prefix}{command}"),
description: description.clone(),
}
}
})
.collect();
Some(FormattedCtaBlock {
description: block
.description
.clone()
.unwrap_or_else(|| "Suggested commands:".to_string()),
commands,
})
}
fn format_human_cta(cta: &FormattedCtaBlock) -> String {
let mut lines = vec![String::new(), cta.description.clone()];
let max_len = cta
.commands
.iter()
.map(|c| c.command.len())
.max()
.unwrap_or(0);
for c in &cta.commands {
let desc = match &c.description {
Some(d) => {
let padding = " ".repeat(max_len - c.command.len());
format!(" {padding}# {d}")
}
None => String::new(),
};
lines.push(format!(" {}{desc}", c.command));
}
lines.join("\n")
}
fn collect_help_commands(commands: &BTreeMap<String, CommandEntry>) -> Vec<CommandSummary> {
let mut result: Vec<CommandSummary> = commands
.iter()
.map(|(name, entry)| CommandSummary {
name: name.clone(),
description: entry.description().map(|s| s.to_string()),
})
.collect();
result.sort_by(|a, b| a.name.cmp(&b.name));
result
}
fn collect_command_info(
commands: &BTreeMap<String, CommandEntry>,
prefix: &[&str],
) -> Vec<skill::CommandInfo> {
let mut result = Vec::new();
for (name, entry) in commands {
let mut path_parts: Vec<&str> = prefix.to_vec();
path_parts.push(name);
match entry {
CommandEntry::Leaf(def) => {
result.push(skill::CommandInfo {
name: path_parts.join(" "),
description: def.description.clone(),
args_fields: def.args_fields.clone(),
options_fields: def.options_fields.clone(),
env_fields: def.env_fields.clone(),
hint: def.hint.clone(),
examples: def
.examples
.iter()
.map(|e| skill::Example {
command: e.command.clone(),
description: e.description.clone(),
})
.collect(),
output_schema: def.output_schema.clone(),
});
}
CommandEntry::Group { commands: sub, .. } => {
result.extend(collect_command_info(sub, &path_parts));
}
CommandEntry::FetchGateway { .. } => {}
}
}
result
}
fn collect_group_descriptions(
commands: &BTreeMap<String, CommandEntry>,
prefix: &[&str],
) -> BTreeMap<String, String> {
let mut result = BTreeMap::new();
for (name, entry) in commands {
if let CommandEntry::Group {
description,
commands: sub,
..
} = entry
{
let mut path_parts: Vec<&str> = prefix.to_vec();
path_parts.push(name);
let key = path_parts.join(" ");
if let Some(desc) = description {
result.insert(key.clone(), desc.clone());
}
result.extend(collect_group_descriptions(sub, &path_parts));
}
}
result
}
fn has_required_args(args_fields: &[FieldMeta]) -> bool {
args_fields.iter().any(|f| f.required)
}
fn format_command_help(
name: &str,
command: &CommandDef,
commands: &BTreeMap<String, CommandEntry>,
aliases: &[String],
config_flag: Option<&str>,
version: Option<&str>,
root: bool,
) -> String {
help::format_command(
name,
&FormatCommandOptions {
aliases: if aliases.is_empty() {
None
} else {
Some(aliases.to_vec())
},
args_fields: command.args_fields.clone(),
config_flag: config_flag.map(|s| s.to_string()),
commands: collect_help_commands(commands),
description: command.description.clone(),
env_fields: command.env_fields.clone(),
examples: command.examples.clone(),
hint: command.hint.clone(),
hide_global_options: false,
options_fields: command.options_fields.clone(),
option_aliases: command.aliases.clone(),
root,
version: version.map(|s| s.to_string()),
},
)
}
struct BuiltinCommand {
name: &'static str,
description: &'static str,
args_fields: Vec<FieldMeta>,
hint: Option<String>,
subcommands: Vec<BuiltinSubcommand>,
}
struct BuiltinSubcommand {
name: &'static str,
description: &'static str,
options_fields: Vec<FieldMeta>,
option_aliases: HashMap<String, char>,
}
fn builtin_commands(cli_name: &str) -> Vec<BuiltinCommand> {
let completions_rows = [
(
"bash",
format!("eval \"$({cli_name} completions bash)\""),
"# add to ~/.bashrc".to_string(),
),
(
"fish",
format!("{cli_name} completions fish | source"),
"# add to ~/.config/fish/config.fish".to_string(),
),
(
"nushell",
format!("see `{cli_name} completions nushell`"),
"# add to config.nu".to_string(),
),
(
"zsh",
format!("eval \"$({cli_name} completions zsh)\""),
"# add to ~/.zshrc".to_string(),
),
];
let shell_w = completions_rows
.iter()
.map(|(shell, _, _)| shell.len())
.max()
.unwrap_or(0);
let cmd_w = completions_rows
.iter()
.map(|(_, cmd, _)| cmd.len())
.max()
.unwrap_or(0);
vec![
BuiltinCommand {
name: "completions",
description: "Generate shell completion script",
args_fields: vec![FieldMeta {
name: "shell",
cli_name: "shell".to_string(),
description: Some("Shell to generate completions for"),
field_type: crate::schema::FieldType::Enum(
["bash", "fish", "nushell", "zsh"]
.into_iter()
.map(|value| value.to_string())
.collect(),
),
required: true,
default: None,
alias: None,
deprecated: false,
env_name: None,
}],
hint: Some(format!(
"Setup:\n{}",
completions_rows
.iter()
.map(|(shell, cmd, comment)| {
format!(" {:<shell_w$} {:<cmd_w$} {}", shell, cmd, comment)
})
.collect::<Vec<_>>()
.join("\n"),
)),
subcommands: Vec::new(),
},
BuiltinCommand {
name: "mcp",
description: "Register as MCP server",
args_fields: Vec::new(),
hint: None,
subcommands: vec![BuiltinSubcommand {
name: "add",
description: "Register as MCP server",
options_fields: vec![
FieldMeta {
name: "agent",
cli_name: "agent".to_string(),
description: Some("Target a specific agent (e.g. claude-code, cursor)"),
field_type: crate::schema::FieldType::String,
required: false,
default: None,
alias: None,
deprecated: false,
env_name: None,
},
FieldMeta {
name: "command",
cli_name: "command".to_string(),
description: Some(
"Override the command agents will run (e.g. \"my-cli --mcp\")",
),
field_type: crate::schema::FieldType::String,
required: false,
default: None,
alias: Some('c'),
deprecated: false,
env_name: None,
},
FieldMeta {
name: "no_global",
cli_name: "no-global".to_string(),
description: Some("Install to project instead of globally"),
field_type: crate::schema::FieldType::Boolean,
required: false,
default: Some(Value::Bool(false)),
alias: None,
deprecated: false,
env_name: None,
},
],
option_aliases: HashMap::from([(String::from("command"), 'c')]),
}],
},
BuiltinCommand {
name: "skills",
description: "Sync skill files to agents",
args_fields: Vec::new(),
hint: None,
subcommands: vec![BuiltinSubcommand {
name: "add",
description: "Sync skill files to agents",
options_fields: vec![
FieldMeta {
name: "depth",
cli_name: "depth".to_string(),
description: Some("Grouping depth for skill files (default: 1)"),
field_type: crate::schema::FieldType::Number,
required: false,
default: Some(Value::Number(serde_json::Number::from(1))),
alias: None,
deprecated: false,
env_name: None,
},
FieldMeta {
name: "no_global",
cli_name: "no-global".to_string(),
description: Some("Install to project instead of globally"),
field_type: crate::schema::FieldType::Boolean,
required: false,
default: Some(Value::Bool(false)),
alias: None,
deprecated: false,
env_name: None,
},
],
option_aliases: HashMap::new(),
}],
},
]
}
fn format_builtin_help(cli_name: &str, builtin: &BuiltinCommand) -> String {
help::format_root(
&format!("{cli_name} {}", builtin.name),
&FormatRootOptions {
aliases: None,
config_flag: None,
commands: builtin
.subcommands
.iter()
.map(|sub| CommandSummary {
name: sub.name.to_string(),
description: Some(sub.description.to_string()),
})
.collect(),
description: Some(builtin.description.to_string()),
root: false,
version: None,
},
)
}
fn format_builtin_subcommand_help(
cli_name: &str,
builtin: &BuiltinCommand,
sub_name: &str,
) -> String {
let sub = builtin.subcommands.iter().find(|sub| sub.name == sub_name);
help::format_command(
&format!("{cli_name} {} {sub_name}", builtin.name),
&FormatCommandOptions {
aliases: None,
args_fields: Vec::new(),
config_flag: None,
commands: Vec::new(),
description: sub.map(|item| item.description.to_string()),
env_fields: Vec::new(),
examples: Vec::new(),
hint: None,
hide_global_options: true,
options_fields: sub
.map(|item| item.options_fields.clone())
.unwrap_or_default(),
option_aliases: sub
.map(|item| item.option_aliases.clone())
.unwrap_or_default(),
root: false,
version: None,
},
)
}
fn builtin_command_index(tokens: &[String], cli_name: &str, builtin_name: &str) -> Option<usize> {
if tokens.first().map(|token| token.as_str()) == Some(builtin_name) {
return Some(0);
}
if tokens.first().map(|token| token.as_str()) == Some(cli_name)
&& tokens.get(1).map(|token| token.as_str()) == Some(builtin_name)
{
return Some(1);
}
None
}
fn convert_to_completion_commands(
commands: &BTreeMap<String, CommandEntry>,
) -> BTreeMap<String, crate::completions::CommandEntry> {
let mut result = BTreeMap::new();
for (name, entry) in commands {
match entry {
CommandEntry::Leaf(def) => {
result.insert(
name.clone(),
crate::completions::CommandEntry {
is_group: false,
description: def.description.clone(),
commands: BTreeMap::new(),
options_fields: def.options_fields.clone(),
aliases: def
.aliases
.iter()
.map(|(key, value)| (key.clone(), *value))
.collect(),
},
);
}
CommandEntry::Group {
description,
commands: sub_commands,
..
} => {
result.insert(
name.clone(),
crate::completions::CommandEntry {
is_group: true,
description: description.clone(),
commands: convert_to_completion_commands(sub_commands),
options_fields: Vec::new(),
aliases: BTreeMap::new(),
},
);
}
CommandEntry::FetchGateway { .. } => {}
}
}
result
}
fn completion_root_command(
root_command: Option<&Arc<CommandDef>>,
) -> Option<crate::completions::CommandDef> {
root_command.map(|command| crate::completions::CommandDef {
options_fields: command.options_fields.clone(),
aliases: command
.aliases
.iter()
.map(|(key, value)| (key.clone(), *value))
.collect(),
})
}
fn completion_output(
cli_name: &str,
aliases: &[String],
commands: &BTreeMap<String, CommandEntry>,
root_command: Option<&Arc<CommandDef>>,
argv: &[String],
complete_shell: Option<&str>,
complete_index: Option<&str>,
) -> Option<String> {
let shell = crate::completions::Shell::from_str(complete_shell?)?;
let separator = argv.iter().position(|token| token == "--");
let words = separator
.map(|index| argv[(index + 1)..].to_vec())
.unwrap_or_else(|| argv.to_vec());
if words.is_empty() {
let names = std::iter::once(cli_name.to_string())
.chain(aliases.iter().cloned())
.collect::<Vec<_>>();
return Some(
names
.iter()
.map(|name| crate::completions::register(shell, name))
.collect::<Vec<_>>()
.join("\n"),
);
}
let index = complete_index
.and_then(|value| value.parse::<usize>().ok())
.unwrap_or_else(|| words.len().saturating_sub(1));
let commands = convert_to_completion_commands(commands);
let root = completion_root_command(root_command);
let mut candidates = crate::completions::complete(&commands, root.as_ref(), &words, index);
let builtins = builtin_commands(cli_name);
let current = words.get(index).map(|word| word.as_str()).unwrap_or("");
let mut non_flags = words
.iter()
.take(index)
.filter(|word| !word.starts_with('-'))
.map(|word| word.to_string())
.collect::<Vec<_>>();
if let Some(first) = non_flags.first()
&& (first == cli_name || aliases.iter().any(|alias| alias == first))
{
non_flags.remove(0);
}
if non_flags.is_empty() {
for builtin in &builtins {
if builtin.name.starts_with(current)
&& !candidates
.iter()
.any(|candidate| candidate.value == builtin.name)
{
candidates.push(crate::completions::Candidate {
value: builtin.name.to_string(),
description: Some(builtin.description.to_string()),
no_space: !builtin.subcommands.is_empty(),
});
}
}
} else if non_flags.len() == 1
&& let Some(parent) = non_flags.last()
&& let Some(builtin) = builtins.iter().find(|builtin| builtin.name == parent)
{
for subcommand in &builtin.subcommands {
if subcommand.name.starts_with(current) {
candidates.push(crate::completions::Candidate {
value: subcommand.name.to_string(),
description: Some(subcommand.description.to_string()),
no_space: false,
});
}
}
}
Some(crate::completions::format(shell, &candidates))
}
fn format_config_schema(
root_command: Option<&Arc<CommandDef>>,
commands: &BTreeMap<String, CommandEntry>,
) -> Result<String, Box<dyn std::error::Error>> {
let root_options = root_command
.map(|command| command.options_fields.as_slice())
.unwrap_or(&[]);
let schema = crate::config_schema::from_command_tree(commands, root_options);
Ok(serde_json::to_string_pretty(&schema)?)
}
struct StreamingOptions<'a> {
path: &'a str,
start: std::time::Instant,
format: Format,
format_explicit: bool,
human: bool,
render_output: bool,
verbose: bool,
}
async fn handle_streaming(
mut stream: std::pin::Pin<Box<dyn futures::Stream<Item = Value> + Send>>,
opts: StreamingOptions<'_>,
) {
let StreamingOptions {
path,
start,
format,
format_explicit,
human,
render_output,
verbose,
} = opts;
use futures::StreamExt;
let use_jsonl = format == Format::Jsonl;
let incremental = use_jsonl || (!format_explicit && format == Format::Toon);
if incremental {
while let Some(value) = stream.next().await {
if use_jsonl {
let chunk = serde_json::json!({ "type": "chunk", "data": value });
writeln_stdout(&serde_json::to_string(&chunk).unwrap_or_default());
} else if render_output {
writeln_stdout(&format_value(&value, format));
}
}
if use_jsonl {
let done = serde_json::json!({
"type": "done",
"ok": true,
"meta": {
"command": path,
"duration": format!("{}ms", start.elapsed().as_millis()),
}
});
writeln_stdout(&serde_json::to_string(&done).unwrap_or_default());
}
} else {
let mut chunks: Vec<Value> = Vec::new();
while let Some(value) = stream.next().await {
chunks.push(value);
}
let data = Value::Array(chunks);
let duration_str = format!("{}ms", start.elapsed().as_millis());
if verbose {
let envelope = serde_json::json!({
"ok": true,
"data": data,
"meta": {
"command": path,
"duration": duration_str,
}
});
writeln_stdout(&format_value(&envelope, format));
} else if human {
if render_output {
writeln_stdout(&format_value(&data, format));
}
} else {
writeln_stdout(&format_value(&data, format));
}
}
}
#[cfg(feature = "mcp")]
fn convert_to_mcp_commands(
commands: &BTreeMap<String, CommandEntry>,
) -> BTreeMap<String, crate::mcp::CommandEntry> {
let mut result = BTreeMap::new();
for (name, entry) in commands {
match entry {
CommandEntry::Leaf(def) => {
result.insert(
name.clone(),
crate::mcp::CommandEntry {
is_group: false,
description: def.description.clone(),
commands: BTreeMap::new(),
args_fields: def.args_fields.clone(),
options_fields: def.options_fields.clone(),
output_schema: def.output_schema.clone(),
},
);
}
CommandEntry::Group {
description,
commands: sub,
..
} => {
result.insert(
name.clone(),
crate::mcp::CommandEntry {
is_group: true,
description: description.clone(),
commands: convert_to_mcp_commands(sub),
args_fields: Vec::new(),
options_fields: Vec::new(),
output_schema: None,
},
);
}
CommandEntry::FetchGateway { .. } => {}
}
}
result
}
fn apply_token_ops(output: &str, builtin: &BuiltinFlags) -> Option<String> {
if builtin.token_count {
#[cfg(feature = "tokens")]
{
let count = tiktoken_rs::cl100k_base()
.ok()
.map(|bpe| bpe.encode_with_special_tokens(output).len())
.unwrap_or(0);
return Some(count.to_string());
}
#[cfg(not(feature = "tokens"))]
return Some(output.split_whitespace().count().to_string());
}
if builtin.token_offset.is_some() || builtin.token_limit.is_some() {
#[cfg(feature = "tokens")]
{
if let Ok(bpe) = tiktoken_rs::cl100k_base() {
let tokens = bpe.encode_with_special_tokens(output);
let offset = builtin.token_offset.unwrap_or(0);
let sliced = if offset < tokens.len() {
&tokens[offset..]
} else {
&[]
};
let limited = if let Some(limit) = builtin.token_limit {
&sliced[..limit.min(sliced.len())]
} else {
sliced
};
return Some(bpe.decode(limited.to_vec()).unwrap_or_default());
}
}
}
None
}
fn write_with_token_ops(output: &str, builtin: &BuiltinFlags, write_fn: fn(&str)) {
if let Some(token_output) = apply_token_ops(output, builtin) {
write_fn(&token_output);
} else {
write_fn(output);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_builtin_flags_basic() {
let argv: Vec<String> = vec![
"--verbose".to_string(),
"--json".to_string(),
"list".to_string(),
];
let result = extract_builtin_flags(&argv, None).unwrap();
assert!(result.verbose);
assert_eq!(result.format, Format::Json);
assert!(result.format_explicit);
assert_eq!(result.rest, vec!["list"]);
}
#[test]
fn test_extract_builtin_flags_help() {
let argv: Vec<String> = vec!["--help".to_string()];
let result = extract_builtin_flags(&argv, None).unwrap();
assert!(result.help);
assert!(result.rest.is_empty());
}
#[test]
fn test_extract_builtin_flags_format() {
let argv: Vec<String> = vec!["--format".to_string(), "yaml".to_string()];
let result = extract_builtin_flags(&argv, None).unwrap();
assert_eq!(result.format, Format::Yaml);
assert!(result.format_explicit);
}
#[test]
fn test_extract_builtin_flags_config() {
let argv: Vec<String> = vec![
"--config".to_string(),
"myconfig.json".to_string(),
"deploy".to_string(),
];
let result = extract_builtin_flags(&argv, Some("config")).unwrap();
assert_eq!(result.config_path, Some("myconfig.json".to_string()));
assert_eq!(result.rest, vec!["deploy"]);
}
#[test]
fn test_resolve_command_leaf() {
let mut commands = BTreeMap::new();
commands.insert(
"list".to_string(),
CommandEntry::Leaf(Arc::new(CommandDef {
name: "list".to_string(),
description: Some("List items".to_string()),
args_fields: vec![],
options_fields: vec![],
env_fields: vec![],
aliases: std::collections::HashMap::new(),
examples: vec![],
hint: None,
format: None,
output_policy: None,
handler: Box::new(NoopHandler),
middleware: vec![],
output_schema: None,
})),
);
let tokens = vec!["list".to_string(), "--verbose".to_string()];
match resolve_command(&commands, &tokens) {
ResolvedCommand::Leaf {
command,
path,
rest,
..
} => {
assert_eq!(path, "list");
assert_eq!(rest, vec!["--verbose"]);
assert_eq!(command.name, "list");
}
_ => panic!("Expected Leaf"),
}
}
#[test]
fn test_resolve_command_group() {
let mut sub_commands = BTreeMap::new();
sub_commands.insert(
"get".to_string(),
CommandEntry::Leaf(Arc::new(CommandDef {
name: "get".to_string(),
description: Some("Get a user".to_string()),
args_fields: vec![],
options_fields: vec![],
env_fields: vec![],
aliases: std::collections::HashMap::new(),
examples: vec![],
hint: None,
format: None,
output_policy: None,
handler: Box::new(NoopHandler),
middleware: vec![],
output_schema: None,
})),
);
let mut commands = BTreeMap::new();
commands.insert(
"users".to_string(),
CommandEntry::Group {
description: Some("User commands".to_string()),
commands: sub_commands,
middleware: vec![],
output_policy: None,
},
);
let tokens = vec!["users".to_string(), "get".to_string(), "alice".to_string()];
match resolve_command(&commands, &tokens) {
ResolvedCommand::Leaf {
command,
path,
rest,
..
} => {
assert_eq!(path, "users get");
assert_eq!(rest, vec!["alice"]);
assert_eq!(command.name, "get");
}
_ => panic!("Expected Leaf"),
}
}
#[test]
fn test_resolve_command_not_found() {
let commands = BTreeMap::new();
let tokens = vec!["nonexistent".to_string()];
match resolve_command(&commands, &tokens) {
ResolvedCommand::Error { error, path } => {
assert_eq!(error, "nonexistent");
assert!(path.is_empty());
}
_ => panic!("Expected Error"),
}
}
#[test]
fn test_collect_help_commands() {
let mut commands = BTreeMap::new();
commands.insert(
"deploy".to_string(),
CommandEntry::Leaf(Arc::new(CommandDef {
name: "deploy".to_string(),
description: Some("Deploy the app".to_string()),
args_fields: vec![],
options_fields: vec![],
env_fields: vec![],
aliases: std::collections::HashMap::new(),
examples: vec![],
hint: None,
format: None,
output_policy: None,
handler: Box::new(NoopHandler),
middleware: vec![],
output_schema: None,
})),
);
commands.insert(
"build".to_string(),
CommandEntry::Group {
description: Some("Build commands".to_string()),
commands: BTreeMap::new(),
middleware: vec![],
output_policy: None,
},
);
let summaries = collect_help_commands(&commands);
assert_eq!(summaries.len(), 2);
assert_eq!(summaries[0].name, "build");
assert_eq!(summaries[1].name, "deploy");
}
struct NoopHandler;
#[async_trait::async_trait]
impl crate::command::CommandHandler for NoopHandler {
async fn run(&self, _ctx: crate::command::CommandContext) -> CommandResult {
CommandResult::Ok {
data: Value::Null,
cta: None,
}
}
}
fn make_leaf_command(name: &str, description: Option<&str>) -> CommandDef {
CommandDef {
name: name.to_string(),
description: description.map(|value| value.to_string()),
args_fields: vec![],
options_fields: vec![],
env_fields: vec![],
aliases: HashMap::new(),
examples: vec![],
hint: None,
format: None,
output_policy: None,
handler: Box::new(NoopHandler),
middleware: vec![],
output_schema: None,
}
}
fn make_test_cli() -> Cli {
Cli::create("test")
.description("Test CLI")
.command("ping", make_leaf_command("ping", Some("Ping the server")))
}
#[test]
fn test_format_human_error() {
assert_eq!(
format_human_error("UNKNOWN", "Something went wrong"),
"Error: Something went wrong"
);
assert_eq!(
format_human_error("AUTH_FAILED", "Not logged in"),
"Error (AUTH_FAILED): Not logged in"
);
}
#[test]
fn test_extract_builtin_flags_config_schema() {
let argv: Vec<String> = vec!["--config-schema".to_string()];
let result = extract_builtin_flags(&argv, Some("config")).unwrap();
assert!(result.config_schema);
}
#[test]
fn test_completion_output_includes_builtins_at_root() {
let output = completion_output(
"test",
&[],
&make_test_cli().commands,
None,
&["--".to_string(), "test".to_string(), "".to_string()],
Some("bash"),
Some("1"),
)
.unwrap();
assert!(output.contains("completions"));
assert!(output.contains("mcp"));
assert!(output.contains("skills"));
}
#[test]
fn test_completion_output_includes_builtin_subcommands() {
let output = completion_output(
"test",
&[],
&make_test_cli().commands,
None,
&[
"--".to_string(),
"test".to_string(),
"skills".to_string(),
"".to_string(),
],
Some("bash"),
Some("2"),
)
.unwrap();
assert!(output.contains("add"));
}
#[tokio::test]
async fn test_serve_to_builtin_completions_help() {
let cli = make_test_cli();
let mut output = Vec::new();
let result = cli
.serve_to(
vec!["completions".to_string(), "--help".to_string()],
&mut output,
true,
)
.await
.unwrap();
assert_eq!(result, None);
let output = String::from_utf8(output).unwrap();
assert!(output.contains("test completions"));
assert!(output.contains("Generate shell completion script"));
}
#[tokio::test]
async fn test_serve_to_builtin_completions_shell() {
let cli = make_test_cli();
let mut output = Vec::new();
let result = cli
.serve_to(
vec!["completions".to_string(), "bash".to_string()],
&mut output,
false,
)
.await
.unwrap();
assert_eq!(result, None);
let output = String::from_utf8(output).unwrap();
assert!(output.contains("COMPLETE=\"bash\""));
}
#[tokio::test]
async fn test_serve_to_builtin_skills_help() {
let cli = make_test_cli();
let mut output = Vec::new();
let result = cli
.serve_to(vec!["skills".to_string()], &mut output, true)
.await
.unwrap();
assert_eq!(result, None);
let output = String::from_utf8(output).unwrap();
assert!(output.contains("test skills"));
assert!(output.contains("add"));
}
#[tokio::test]
async fn test_serve_to_builtin_skills_add_help() {
let cli = make_test_cli();
let mut output = Vec::new();
let result = cli
.serve_to(
vec![
"skills".to_string(),
"add".to_string(),
"--help".to_string(),
],
&mut output,
true,
)
.await
.unwrap();
assert_eq!(result, None);
let output = String::from_utf8(output).unwrap();
assert!(output.contains("test skills add"));
assert!(output.contains("--depth"));
assert!(output.contains("--no-global"));
}
#[tokio::test]
async fn test_serve_to_builtin_mcp_help() {
let cli = make_test_cli();
let mut output = Vec::new();
let result = cli
.serve_to(vec!["mcp".to_string()], &mut output, true)
.await
.unwrap();
assert_eq!(result, None);
let output = String::from_utf8(output).unwrap();
assert!(output.contains("test mcp"));
assert!(output.contains("add"));
}
#[tokio::test]
async fn test_serve_to_builtin_mcp_add_help() {
let cli = make_test_cli();
let mut output = Vec::new();
let result = cli
.serve_to(
vec!["mcp".to_string(), "add".to_string(), "--help".to_string()],
&mut output,
true,
)
.await
.unwrap();
assert_eq!(result, None);
let output = String::from_utf8(output).unwrap();
assert!(output.contains("test mcp add"));
assert!(output.contains("--command"));
assert!(output.contains("--agent"));
assert!(output.contains("--no-global"));
}
#[tokio::test]
async fn test_serve_to_config_schema() {
let cli = Cli::create("test")
.root(CommandDef {
name: "test".to_string(),
description: Some("Test CLI".to_string()),
args_fields: vec![],
options_fields: vec![FieldMeta {
name: "repo",
cli_name: "repo".to_string(),
description: Some("Repository path"),
field_type: crate::schema::FieldType::String,
required: false,
default: Some(Value::String(".".to_string())),
alias: None,
deprecated: false,
env_name: None,
}],
env_fields: vec![],
aliases: HashMap::new(),
examples: vec![],
hint: None,
format: None,
output_policy: None,
handler: Box::new(NoopHandler),
middleware: vec![],
output_schema: None,
})
.command("ping", make_leaf_command("ping", Some("Ping the server")))
.config(ConfigOptions {
flag: "config".to_string(),
files: vec!["test.config.json".to_string()],
});
let mut output = Vec::new();
let result = cli
.serve_to(vec!["--config-schema".to_string()], &mut output, false)
.await
.unwrap();
assert_eq!(result, None);
let output = String::from_utf8(output).unwrap();
let parsed: serde_json::Value = serde_json::from_str(output.trim()).unwrap();
assert_eq!(parsed["properties"]["$schema"]["type"], "string");
assert_eq!(
parsed["properties"]["options"]["properties"]["repo"]["type"],
"string"
);
assert!(parsed["properties"]["commands"]["properties"]["ping"].is_object());
}
struct EchoFetchHandler;
#[async_trait::async_trait]
impl crate::fetch::FetchHandler for EchoFetchHandler {
async fn handle(&self, request: crate::fetch::FetchInput) -> crate::fetch::FetchOutput {
let data = serde_json::json!({
"path": request.path,
"method": request.method,
"headers": request.headers.iter()
.map(|(k, v)| serde_json::json!([k, v]))
.collect::<Vec<_>>(),
"body": request.body,
"query": request.query.iter()
.map(|(k, v)| serde_json::json!([k, v]))
.collect::<Vec<_>>(),
});
crate::fetch::FetchOutput {
ok: true,
status: 200,
data,
headers: vec![],
}
}
}
struct ErrorFetchHandler;
#[async_trait::async_trait]
impl crate::fetch::FetchHandler for ErrorFetchHandler {
async fn handle(&self, _request: crate::fetch::FetchInput) -> crate::fetch::FetchOutput {
crate::fetch::FetchOutput {
ok: false,
status: 500,
data: serde_json::json!({ "message": "Internal Server Error" }),
headers: vec![],
}
}
}
#[test]
fn test_fetch_gateway_builder() {
let cli = Cli::create("test-cli").fetch_gateway(
"api",
EchoFetchHandler,
crate::fetch::FetchGatewayOptions {
description: Some("API gateway".to_string()),
base_path: Some("/v1".to_string()),
output_policy: None,
},
);
assert!(cli.commands.contains_key("api"));
match &cli.commands["api"] {
CommandEntry::FetchGateway {
description,
base_path,
..
} => {
assert_eq!(description.as_deref(), Some("API gateway"));
assert_eq!(base_path.as_deref(), Some("/v1"));
}
_ => panic!("Expected FetchGateway"),
}
}
#[test]
fn test_resolve_command_fetch_gateway() {
let mut commands = BTreeMap::new();
commands.insert(
"api".to_string(),
CommandEntry::FetchGateway {
description: Some("API gateway".to_string()),
base_path: Some("/v1".to_string()),
output_policy: None,
handler: Arc::new(EchoFetchHandler),
},
);
let tokens = vec![
"api".to_string(),
"users".to_string(),
"123".to_string(),
"--limit".to_string(),
"10".to_string(),
];
match resolve_command(&commands, &tokens) {
ResolvedCommand::Gateway {
path,
rest,
base_path,
..
} => {
assert_eq!(path, "api");
assert_eq!(rest, vec!["users", "123", "--limit", "10"]);
assert_eq!(base_path.as_deref(), Some("/v1"));
}
_ => panic!("Expected Gateway"),
}
}
#[test]
fn test_resolve_command_fetch_gateway_no_args() {
let mut commands = BTreeMap::new();
commands.insert(
"api".to_string(),
CommandEntry::FetchGateway {
description: None,
base_path: None,
output_policy: None,
handler: Arc::new(EchoFetchHandler),
},
);
let tokens = vec!["api".to_string()];
match resolve_command(&commands, &tokens) {
ResolvedCommand::Gateway {
path,
rest,
base_path,
..
} => {
assert_eq!(path, "api");
assert!(rest.is_empty());
assert!(base_path.is_none());
}
_ => panic!("Expected Gateway"),
}
}
#[tokio::test]
async fn test_serve_to_fetch_gateway_basic() {
let cli = Cli::create("test-cli").fetch_gateway(
"api",
EchoFetchHandler,
crate::fetch::FetchGatewayOptions {
description: None,
base_path: None,
output_policy: None,
},
);
let mut output = Vec::new();
let argv = vec!["api".to_string(), "users".to_string(), "123".to_string()];
let result = cli.serve_to(argv, &mut output, false).await.unwrap();
assert_eq!(result, None);
let output_str = String::from_utf8(output).unwrap();
let parsed: serde_json::Value = serde_json::from_str(output_str.trim()).unwrap();
assert_eq!(parsed["path"], "/users/123");
assert_eq!(parsed["method"], "GET");
}
#[tokio::test]
async fn test_serve_to_fetch_gateway_with_base_path() {
let cli = Cli::create("test-cli").fetch_gateway(
"api",
EchoFetchHandler,
crate::fetch::FetchGatewayOptions {
description: None,
base_path: Some("/v2".to_string()),
output_policy: None,
},
);
let mut output = Vec::new();
let argv = vec!["api".to_string(), "items".to_string()];
let result = cli.serve_to(argv, &mut output, false).await.unwrap();
assert_eq!(result, None);
let output_str = String::from_utf8(output).unwrap();
let parsed: serde_json::Value = serde_json::from_str(output_str.trim()).unwrap();
assert_eq!(parsed["path"], "/v2/items");
}
#[tokio::test]
async fn test_serve_to_fetch_gateway_with_curl_args() {
let cli = Cli::create("test-cli").fetch_gateway(
"api",
EchoFetchHandler,
crate::fetch::FetchGatewayOptions {
description: None,
base_path: None,
output_policy: None,
},
);
let mut output = Vec::new();
let argv = vec![
"api".to_string(),
"-X".to_string(),
"POST".to_string(),
"-d".to_string(),
r#"{"name":"test"}"#.to_string(),
"-H".to_string(),
"Content-Type: application/json".to_string(),
"users".to_string(),
];
let result = cli.serve_to(argv, &mut output, false).await.unwrap();
assert_eq!(result, None);
let output_str = String::from_utf8(output).unwrap();
let parsed: serde_json::Value = serde_json::from_str(output_str.trim()).unwrap();
assert_eq!(parsed["path"], "/users");
assert_eq!(parsed["method"], "POST");
assert_eq!(parsed["body"], r#"{"name":"test"}"#);
}
#[tokio::test]
async fn test_serve_to_fetch_gateway_error_returns_exit_code() {
let cli = Cli::create("test-cli").fetch_gateway(
"api",
ErrorFetchHandler,
crate::fetch::FetchGatewayOptions {
description: None,
base_path: None,
output_policy: None,
},
);
let mut output = Vec::new();
let argv = vec!["api".to_string(), "users".to_string()];
let result = cli.serve_to(argv, &mut output, false).await.unwrap();
assert_eq!(result, Some(1));
let output_str = String::from_utf8(output).unwrap();
let parsed: serde_json::Value = serde_json::from_str(output_str.trim()).unwrap();
assert_eq!(parsed["ok"], false);
assert_eq!(parsed["status"], 500);
}
#[test]
fn test_collect_help_commands_includes_fetch_gateway() {
let mut commands = BTreeMap::new();
commands.insert(
"api".to_string(),
CommandEntry::FetchGateway {
description: Some("API gateway".to_string()),
base_path: None,
output_policy: None,
handler: Arc::new(EchoFetchHandler),
},
);
commands.insert(
"deploy".to_string(),
CommandEntry::Leaf(Arc::new(CommandDef {
name: "deploy".to_string(),
description: Some("Deploy the app".to_string()),
args_fields: vec![],
options_fields: vec![],
env_fields: vec![],
aliases: std::collections::HashMap::new(),
examples: vec![],
hint: None,
format: None,
output_policy: None,
handler: Box::new(NoopHandler),
middleware: vec![],
output_schema: None,
})),
);
let summaries = collect_help_commands(&commands);
assert_eq!(summaries.len(), 2);
assert_eq!(summaries[0].name, "api");
assert_eq!(summaries[0].description.as_deref(), Some("API gateway"));
assert_eq!(summaries[1].name, "deploy");
}
#[test]
fn test_resolve_gateway_in_group() {
let mut sub_commands = BTreeMap::new();
sub_commands.insert(
"fetch".to_string(),
CommandEntry::FetchGateway {
description: Some("Fetch endpoint".to_string()),
base_path: Some("/api".to_string()),
output_policy: None,
handler: Arc::new(EchoFetchHandler),
},
);
let mut commands = BTreeMap::new();
commands.insert(
"service".to_string(),
CommandEntry::Group {
description: Some("Service commands".to_string()),
commands: sub_commands,
middleware: vec![],
output_policy: None,
},
);
let tokens = vec![
"service".to_string(),
"fetch".to_string(),
"users".to_string(),
];
match resolve_command(&commands, &tokens) {
ResolvedCommand::Gateway {
path,
rest,
base_path,
..
} => {
assert_eq!(path, "service fetch");
assert_eq!(rest, vec!["users"]);
assert_eq!(base_path.as_deref(), Some("/api"));
}
_ => panic!("Expected Gateway"),
}
}
}