fix(#773): config --output-format json now surfaces deprecation warnings in warnings[] array instead of only stderr text

This commit is contained in:
YeonGyu-Kim
2026-05-27 03:05:14 +09:00
parent 212f0b2ad4
commit 727a1ea4a3
3 changed files with 77 additions and 1 deletions

View File

@@ -346,6 +346,70 @@ impl ConfigLoader {
feature_config,
})
}
/// Like [`load`] but also returns the list of validation warnings collected during
/// loading, without emitting them to stderr. Callers that want to surface warnings
/// through a structured channel (e.g. the JSON config envelope) should use this.
/// #773: enables JSON-mode callers to include `warnings` in their output envelope
/// instead of receiving unstructured text on stderr.
pub fn load_collecting_warnings(&self) -> Result<(RuntimeConfig, Vec<String>), ConfigError> {
let mut merged = BTreeMap::new();
let mut loaded_entries = Vec::new();
let mut mcp_servers = BTreeMap::new();
let mut all_warnings: Vec<String> = Vec::new();
for entry in self.discover() {
crate::config_validate::check_unsupported_format(&entry.path)?;
let Some(parsed) = read_optional_json_object(&entry.path)? else {
continue;
};
let validation = crate::config_validate::validate_config_file(
&parsed.object,
&parsed.source,
&entry.path,
);
if !validation.is_ok() {
let first_error = &validation.errors[0];
return Err(ConfigError::Parse(first_error.to_string()));
}
all_warnings.extend(validation.warnings.iter().map(|w| w.to_string()));
validate_optional_hooks_config(&parsed.object, &entry.path)?;
merge_mcp_servers(&mut mcp_servers, entry.source, &parsed.object, &entry.path)?;
deep_merge_objects(&mut merged, &parsed.object);
loaded_entries.push(entry);
}
// Still emit to stderr for non-JSON callers that go through the normal load() path;
// here we just *also* return them so callers can surface them structurally.
for warning in &all_warnings {
emit_config_warning_once(warning);
}
let merged_value = JsonValue::Object(merged.clone());
let feature_config = RuntimeFeatureConfig {
hooks: parse_optional_hooks_config(&merged_value)?,
plugins: parse_optional_plugin_config(&merged_value)?,
mcp: McpConfigCollection {
servers: mcp_servers,
},
oauth: parse_optional_oauth_config(&merged_value, "merged settings.oauth")?,
model: parse_optional_model(&merged_value),
aliases: parse_optional_aliases(&merged_value)?,
permission_mode: parse_optional_permission_mode(&merged_value)?,
permission_rules: parse_optional_permission_rules(&merged_value)?,
sandbox: parse_optional_sandbox_config(&merged_value)?,
provider_fallbacks: parse_optional_provider_fallbacks(&merged_value)?,
trusted_roots: parse_optional_trusted_roots(&merged_value)?,
};
let config = RuntimeConfig {
merged,
loaded_entries,
feature_config,
};
Ok((config, all_warnings))
}
}
impl RuntimeConfig {

View File

@@ -7874,7 +7874,9 @@ fn render_config_json(
let cwd = env::current_dir()?;
let loader = ConfigLoader::default_for(&cwd);
let discovered = loader.discover();
let runtime_config = loader.load()?;
// #773: use load_collecting_warnings so deprecation warnings are surfaced in the
// JSON envelope instead of only as unstructured stderr text.
let (runtime_config, config_warnings) = loader.load_collecting_warnings()?;
let loaded_paths: Vec<_> = runtime_config
.loaded_entries()
@@ -7902,6 +7904,11 @@ fn render_config_json(
})
.collect();
let warnings_json: Vec<serde_json::Value> = config_warnings
.iter()
.map(|w| serde_json::Value::String(w.clone()))
.collect();
let base = serde_json::json!({
"kind": "config",
"action": if section.is_some() { "show" } else { "list" },
@@ -7910,6 +7917,9 @@ fn render_config_json(
"loaded_files": loaded_paths.len(),
"merged_keys": runtime_config.merged().len(),
"files": files,
// #773: deprecation warnings surfaced structurally so JSON-mode callers
// don't need to strip unstructured text from stderr
"warnings": warnings_json,
});
if let Some(section) = section {