Honor JSON output for skills and MCP inventory commands

The skills and mcp inventory handlers were still emitting prose tables even when the global --output-format json flag was set. This wires structured JSON renderers into the command handlers and CLI dispatch so direct invocations and resumed slash-command execution both return machine-readable payloads while preserving existing text output in the REPL path.

Constraint: Must preserve existing text output and help behavior for interactive slash commands
Rejected: Parse existing prose tables into JSON at the CLI edge | brittle and loses structured fields
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep text and JSON variants driven by the same command parsing branches so --output-format stays deterministic across entry points
Tested: cargo test -p commands
Tested: cargo test -p rusty-claude-cli
Not-tested: Manual invocation against a live user skills registry or external MCP services
This commit is contained in:
Yeachan-Heo
2026-04-05 17:29:54 +00:00
parent 2dd05bfcef
commit 136cedf1cc
2 changed files with 548 additions and 24 deletions

View File

@@ -31,9 +31,10 @@ use api::{
};
use commands::{
handle_agents_slash_command, handle_mcp_slash_command, handle_plugins_slash_command,
handle_skills_slash_command, render_slash_command_help, resume_supported_slash_commands,
slash_command_specs, validate_slash_command_input, SlashCommand,
handle_agents_slash_command, handle_mcp_slash_command, handle_mcp_slash_command_json,
handle_plugins_slash_command, handle_skills_slash_command, handle_skills_slash_command_json,
render_slash_command_help, resume_supported_slash_commands, slash_command_specs,
validate_slash_command_input, SlashCommand,
};
use compat_harness::{extract_manifest, UpstreamPaths};
use init::initialize_repo;
@@ -111,8 +112,14 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
CliAction::DumpManifests => dump_manifests(),
CliAction::BootstrapPlan => print_bootstrap_plan(),
CliAction::Agents { args } => LiveCli::print_agents(args.as_deref())?,
CliAction::Mcp { args } => LiveCli::print_mcp(args.as_deref())?,
CliAction::Skills { args } => LiveCli::print_skills(args.as_deref())?,
CliAction::Mcp {
args,
output_format,
} => LiveCli::print_mcp(args.as_deref(), output_format)?,
CliAction::Skills {
args,
output_format,
} => LiveCli::print_skills(args.as_deref(), output_format)?,
CliAction::PrintSystemPrompt { cwd, date } => print_system_prompt(cwd, date),
CliAction::Version => print_version(),
CliAction::ResumeSession {
@@ -156,9 +163,11 @@ enum CliAction {
},
Mcp {
args: Option<String>,
output_format: CliOutputFormat,
},
Skills {
args: Option<String>,
output_format: CliOutputFormat,
},
PrintSystemPrompt {
cwd: PathBuf,
@@ -370,9 +379,11 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
}),
"mcp" => Ok(CliAction::Mcp {
args: join_optional_args(&rest[1..]),
output_format,
}),
"skills" => Ok(CliAction::Skills {
args: join_optional_args(&rest[1..]),
output_format,
}),
"system-prompt" => parse_system_prompt_args(&rest[1..]),
"login" => Ok(CliAction::Login),
@@ -391,7 +402,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
permission_mode,
})
}
other if other.starts_with('/') => parse_direct_slash_cli_action(&rest),
other if other.starts_with('/') => parse_direct_slash_cli_action(&rest, output_format),
_other => Ok(CliAction::Prompt {
prompt: rest.join(" "),
model,
@@ -479,7 +490,10 @@ fn join_optional_args(args: &[String]) -> Option<String> {
(!trimmed.is_empty()).then(|| trimmed.to_string())
}
fn parse_direct_slash_cli_action(rest: &[String]) -> Result<CliAction, String> {
fn parse_direct_slash_cli_action(
rest: &[String],
output_format: CliOutputFormat,
) -> Result<CliAction, String> {
let raw = rest.join(" ");
match SlashCommand::parse(&raw) {
Ok(Some(SlashCommand::Help)) => Ok(CliAction::Help),
@@ -491,8 +505,12 @@ fn parse_direct_slash_cli_action(rest: &[String]) -> Result<CliAction, String> {
(Some(action), Some(target)) => Some(format!("{action} {target}")),
(None, Some(target)) => Some(target),
},
output_format,
}),
Ok(Some(SlashCommand::Skills { args })) => Ok(CliAction::Skills {
args,
output_format,
}),
Ok(Some(SlashCommand::Skills { args })) => Ok(CliAction::Skills { args }),
Ok(Some(SlashCommand::Unknown(name))) => Err(format_unknown_direct_slash_command(&name)),
Ok(Some(command)) => Err({
let _ = command;
@@ -1844,7 +1862,13 @@ fn run_resume_command(
};
Ok(ResumeCommandOutcome {
session: session.clone(),
message: Some(handle_mcp_slash_command(args.as_deref(), &cwd)?),
message: Some(match output_format {
CliOutputFormat::Text => json!({
"kind": "mcp",
"message": handle_mcp_slash_command(args.as_deref(), &cwd)?,
}),
CliOutputFormat::Json => handle_mcp_slash_command_json(args.as_deref(), &cwd)?,
}),
})
}
SlashCommand::Memory => Ok(ResumeCommandOutcome {
@@ -1888,7 +1912,15 @@ fn run_resume_command(
let cwd = env::current_dir()?;
Ok(ResumeCommandOutcome {
session: session.clone(),
message: Some(handle_skills_slash_command(args.as_deref(), &cwd)?),
message: Some(match output_format {
CliOutputFormat::Text => json!({
"kind": "skills",
"message": handle_skills_slash_command(args.as_deref(), &cwd)?,
}),
CliOutputFormat::Json => {
handle_skills_slash_command_json(args.as_deref(), &cwd)?
}
}),
})
}
SlashCommand::Doctor => Ok(ResumeCommandOutcome {
@@ -2777,7 +2809,7 @@ impl LiveCli {
(Some(action), Some(target)) => Some(format!("{action} {target}")),
(None, Some(target)) => Some(target.to_string()),
};
Self::print_mcp(args.as_deref())?;
Self::print_mcp(args.as_deref(), CliOutputFormat::Text)?;
false
}
SlashCommand::Memory => {
@@ -2811,7 +2843,7 @@ impl LiveCli {
false
}
SlashCommand::Skills { args } => {
Self::print_skills(args.as_deref())?;
Self::print_skills(args.as_deref(), CliOutputFormat::Text)?;
false
}
SlashCommand::Doctor => {
@@ -3095,15 +3127,33 @@ impl LiveCli {
Ok(())
}
fn print_mcp(args: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
fn print_mcp(
args: Option<&str>,
output_format: CliOutputFormat,
) -> Result<(), Box<dyn std::error::Error>> {
let cwd = env::current_dir()?;
println!("{}", handle_mcp_slash_command(args, &cwd)?);
match output_format {
CliOutputFormat::Text => println!("{}", handle_mcp_slash_command(args, &cwd)?),
CliOutputFormat::Json => println!(
"{}",
serialize_json_output(&handle_mcp_slash_command_json(args, &cwd)?)?
),
}
Ok(())
}
fn print_skills(args: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
fn print_skills(
args: Option<&str>,
output_format: CliOutputFormat,
) -> Result<(), Box<dyn std::error::Error>> {
let cwd = env::current_dir()?;
println!("{}", handle_skills_slash_command(args, &cwd)?);
match output_format {
CliOutputFormat::Text => println!("{}", handle_skills_slash_command(args, &cwd)?),
CliOutputFormat::Json => println!(
"{}",
serialize_json_output(&handle_skills_slash_command_json(args, &cwd)?)?
),
}
Ok(())
}
@@ -6498,11 +6548,17 @@ mod tests {
);
assert_eq!(
parse_args(&["mcp".to_string()]).expect("mcp should parse"),
CliAction::Mcp { args: None }
CliAction::Mcp {
args: None,
output_format: CliOutputFormat::Text,
}
);
assert_eq!(
parse_args(&["skills".to_string()]).expect("skills should parse"),
CliAction::Skills { args: None }
CliAction::Skills {
args: None,
output_format: CliOutputFormat::Text,
}
);
assert_eq!(
parse_args(&["agents".to_string(), "--help".to_string()])
@@ -6557,6 +6613,30 @@ mod tests {
);
}
#[test]
fn parses_json_output_for_mcp_and_skills_commands() {
assert_eq!(
parse_args(&["--output-format=json".to_string(), "mcp".to_string()])
.expect("json mcp should parse"),
CliAction::Mcp {
args: None,
output_format: CliOutputFormat::Json,
}
);
assert_eq!(
parse_args(&[
"--output-format=json".to_string(),
"/skills".to_string(),
"help".to_string(),
])
.expect("json /skills help should parse"),
CliAction::Skills {
args: Some("help".to_string()),
output_format: CliOutputFormat::Json,
}
);
}
#[test]
fn single_word_slash_command_names_return_guidance_instead_of_hitting_prompt_mode() {
let error = parse_args(&["cost".to_string()]).expect_err("cost should return guidance");
@@ -6591,18 +6671,23 @@ mod tests {
parse_args(&["/mcp".to_string(), "show".to_string(), "demo".to_string()])
.expect("/mcp show demo should parse"),
CliAction::Mcp {
args: Some("show demo".to_string())
args: Some("show demo".to_string()),
output_format: CliOutputFormat::Text,
}
);
assert_eq!(
parse_args(&["/skills".to_string()]).expect("/skills should parse"),
CliAction::Skills { args: None }
CliAction::Skills {
args: None,
output_format: CliOutputFormat::Text,
}
);
assert_eq!(
parse_args(&["/skills".to_string(), "help".to_string()])
.expect("/skills help should parse"),
CliAction::Skills {
args: Some("help".to_string())
args: Some("help".to_string()),
output_format: CliOutputFormat::Text,
}
);
assert_eq!(
@@ -6613,7 +6698,8 @@ mod tests {
])
.expect("/skills install should parse"),
CliAction::Skills {
args: Some("install ./fixtures/help-skill".to_string())
args: Some("install ./fixtures/help-skill".to_string()),
output_format: CliOutputFormat::Text,
}
);
let error = parse_args(&["/status".to_string()])