diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 90c0e776..08a1db03 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -4220,17 +4220,31 @@ fn run_resume_command( let cwd = env::current_dir()?; let payload = plugins_command_payload_for(&cwd, action.as_deref(), target.as_deref())?; let action_str = action.as_deref().unwrap_or("list"); - let json = serde_json::json!({ + let enabled_count = payload + .plugins + .iter() + .filter(|p| p.get("enabled").and_then(|v| v.as_bool()).unwrap_or(false)) + .count(); + let disabled_count = payload.plugins.len().saturating_sub(enabled_count); + let mut json = serde_json::json!({ "kind": "plugin", "action": action_str, - "target": target, "status": payload.status, + "summary": { + "total": payload.plugins.len(), + "enabled": enabled_count, + "disabled": disabled_count, + "load_failures": payload.load_failures.len(), + }, "config_load_error": payload.config_load_error, - "message": &payload.message, - "reload_runtime": payload.reload_runtime, "plugins": payload.plugins, "load_failures": payload.load_failures, }); + if action_str != "list" { + json["target"] = serde_json::json!(target); + json["reload_runtime"] = serde_json::json!(payload.reload_runtime); + json["message"] = serde_json::json!(&payload.message); + } Ok(ResumeCommandOutcome { session: session.clone(), message: Some(payload.message), @@ -5936,20 +5950,36 @@ impl LiveCli { let payload = plugins_command_payload_for(&cwd, action, target)?; match output_format { CliOutputFormat::Text => println!("{}", payload.message), - CliOutputFormat::Json => println!( - "{}", - serde_json::to_string_pretty(&json!({ + CliOutputFormat::Json => { + let action_str = action.unwrap_or("list"); + let enabled_count = payload + .plugins + .iter() + .filter(|p| p.get("enabled").and_then(|v| v.as_bool()).unwrap_or(false)) + .count(); + let disabled_count = payload.plugins.len().saturating_sub(enabled_count); + let mut obj = json!({ "kind": "plugin", - "action": action.unwrap_or("list"), - "target": target, + "action": action_str, "status": payload.status, + "summary": { + "total": payload.plugins.len(), + "enabled": enabled_count, + "disabled": disabled_count, + "load_failures": payload.load_failures.len(), + }, "config_load_error": payload.config_load_error, - "message": payload.message, - "reload_runtime": payload.reload_runtime, "plugins": payload.plugins, "load_failures": payload.load_failures, - }))? - ), + }); + // Only include operation-result fields for mutating actions + if action_str != "list" { + obj["target"] = json!(target); + obj["reload_runtime"] = json!(payload.reload_runtime); + obj["message"] = json!(payload.message); + } + println!("{}", serde_json::to_string_pretty(&obj)?); + } } Ok(()) } diff --git a/rust/crates/rusty-claude-cli/tests/output_format_contract.rs b/rust/crates/rusty-claude-cli/tests/output_format_contract.rs index b8148a0e..ad7cd706 100644 --- a/rust/crates/rusty-claude-cli/tests/output_format_contract.rs +++ b/rust/crates/rusty-claude-cli/tests/output_format_contract.rs @@ -202,13 +202,31 @@ fn inventory_commands_emit_structured_json_when_requested() { assert_eq!(plugins["action"], "list"); assert_eq!(plugins["status"], "ok"); assert!(plugins["config_load_error"].is_null()); + // reload_runtime and target are operation-result fields; list response omits them (#703) assert!( - plugins["reload_runtime"].is_boolean(), - "plugins reload_runtime should be a boolean" + !plugins + .as_object() + .map_or(false, |o| o.contains_key("reload_runtime")), + "plugins list should not include reload_runtime" ); assert!( - plugins["target"].is_null(), - "plugins target should be null when no plugin is targeted" + !plugins + .as_object() + .map_or(false, |o| o.contains_key("target")), + "plugins list should not include target" + ); + // #703: structured summary replaces prose message + assert!( + plugins["summary"]["total"].is_number(), + "plugins list should have summary.total" + ); + assert!( + plugins["summary"]["enabled"].is_number(), + "plugins list should have summary.enabled" + ); + assert!( + plugins["summary"]["disabled"].is_number(), + "plugins list should have summary.disabled" ); assert_eq!(plugins["status"], "ok"); let plugin_entries = plugins["plugins"].as_array().expect("plugins array"); @@ -691,13 +709,22 @@ fn resumed_inventory_commands_emit_structured_json_when_requested() { assert_eq!(plugins["action"], "list"); assert_eq!(plugins["status"], "ok"); assert!(plugins["config_load_error"].is_null()); + // reload_runtime and target are operation-result fields; list response omits them (#703) assert!( - plugins["reload_runtime"].is_boolean(), - "plugins reload_runtime should be a boolean" + !plugins + .as_object() + .map_or(false, |o| o.contains_key("reload_runtime")), + "plugins list should not include reload_runtime" ); assert!( - plugins["target"].is_null(), - "plugins target should be null when no plugin is targeted" + !plugins + .as_object() + .map_or(false, |o| o.contains_key("target")), + "plugins list should not include target" + ); + assert!( + plugins["summary"]["total"].is_number(), + "plugins list should have summary.total" ); }