diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 1b4acf8f..ecbf3edd 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -6210,7 +6210,7 @@ fn render_config_report(section: Option<&str>) -> Result, + section: Option<&str>, ) -> Result> { let cwd = env::current_dir()?; let loader = ConfigLoader::default_for(&cwd); @@ -6243,13 +6243,52 @@ fn render_config_json( }) .collect(); - Ok(serde_json::json!({ + let base = serde_json::json!({ "kind": "config", "cwd": cwd.display().to_string(), "loaded_files": loaded_paths.len(), "merged_keys": runtime_config.merged().len(), "files": files, - })) + }); + + if let Some(section) = section { + let section_rendered: Option = match section { + "env" => runtime_config.get("env").map(|v| v.render()), + "hooks" => runtime_config.get("hooks").map(|v| v.render()), + "model" => runtime_config.get("model").map(|v| v.render()), + "plugins" => runtime_config + .get("plugins") + .or_else(|| runtime_config.get("enabledPlugins")) + .map(|v| v.render()), + other => { + return Ok(serde_json::json!({ + "kind": "config", + "section": other, + "ok": false, + "error": format!("Unsupported config section '{other}'. Use env, hooks, model, or plugins."), + "cwd": cwd.display().to_string(), + "loaded_files": loaded_paths.len(), + "files": files, + })); + } + }; + // Parse the rendered JSON string back into serde_json::Value so that + // section_value is a real JSON object/array in the envelope, not a quoted string. + let section_value: serde_json::Value = section_rendered + .as_deref() + .and_then(|s| serde_json::from_str(s).ok()) + .unwrap_or(serde_json::Value::Null); + let mut obj = base; + let map = obj.as_object_mut().expect("base is object"); + map.insert( + "section".to_string(), + serde_json::Value::String(section.to_string()), + ); + map.insert("section_value".to_string(), section_value); + return Ok(obj); + } + + Ok(base) } fn render_memory_report() -> Result> { 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 5aaafab2..3c7d1f90 100644 --- a/rust/crates/rusty-claude-cli/tests/output_format_contract.rs +++ b/rust/crates/rusty-claude-cli/tests/output_format_contract.rs @@ -433,6 +433,44 @@ fn resumed_version_and_init_emit_structured_json_when_requested() { assert!(root.join("CLAUDE.md").exists()); } +#[test] +fn config_section_json_emits_section_and_value() { + let root = unique_temp_dir("config-section-json"); + fs::create_dir_all(&root).expect("temp dir should exist"); + + // Without a section: should return base envelope (no section field). + let base = assert_json_command(&root, &["--output-format", "json", "config"]); + assert_eq!(base["kind"], "config"); + assert!(base["loaded_files"].is_number()); + assert!(base["merged_keys"].is_number()); + assert!( + base.get("section").is_none(), + "no section field without section arg" + ); + + // With a known section: should add section + section_value fields. + for section in &["model", "env", "hooks", "plugins"] { + let result = assert_json_command(&root, &["--output-format", "json", "config", section]); + assert_eq!(result["kind"], "config", "section={section}"); + assert_eq!( + result["section"].as_str(), + Some(*section), + "section field must match requested section, got {result:?}" + ); + assert!( + result.get("section_value").is_some(), + "section_value field must be present for section={section}" + ); + } + + // With an unsupported section: should return ok:false + error field. + let bad = assert_json_command(&root, &["--output-format", "json", "config", "unknown"]); + assert_eq!(bad["kind"], "config"); + assert_eq!(bad["ok"], false); + assert!(bad["error"].as_str().is_some()); + assert!(bad["section"].as_str().is_some()); +} + fn assert_json_command(current_dir: &Path, args: &[&str]) -> Value { assert_json_command_with_env(current_dir, args, &[]) }