feat(cli): add JSON/YAML output format to provider list command

Closes #1745

Add -o/--output flag support (json, yaml, table) to openshell provider list
by migrating it to use the existing generic output formatter from #1750.
This follows the established pattern used by sandbox list, gateway list,
and provider list-profiles commands.

Changes:
- crates/openshell-cli/src/run.rs: Add provider_to_json() helper that maps
  Provider proto fields to JSON, including only credential keys (not values)
  for security. Add output parameter to provider_list() and insert early-return
  for structured formats.
- crates/openshell-cli/src/main.rs: Add output field to ProviderCommands::List
  with conflicts_with constraints between names and output flags. Update
  invocation to pass output parameter.
- crates/openshell-cli/tests/provider_commands_integration.rs: Update
  provider_list() call to include output parameter.

Security: The provider_to_json() helper only exposes credential keys, never
credential values, preventing CWE-532 (insertion of sensitive information
into output).
This commit is contained in:
Jeff MAURY
2026-06-15 17:58:45 +02:00
parent ec197a43ef
commit a26a6341af
3 changed files with 68 additions and 3 deletions
+8 -2
View File
@@ -777,8 +777,12 @@ enum ProviderCommands {
offset: u32,
/// Print only provider names, one per line.
#[arg(long)]
#[arg(long, conflicts_with = "output")]
names: bool,
/// Output format.
#[arg(short = 'o', long = "output", value_enum, default_value_t = OutputFormat::Table, conflicts_with = "names")]
output: OutputFormat,
},
/// List available provider profiles.
@@ -2902,8 +2906,10 @@ async fn main() -> Result<()> {
limit,
offset,
names,
output,
} => {
run::provider_list(endpoint, limit, offset, names, &tls).await?;
run::provider_list(endpoint, limit, offset, names, output.as_str(), &tls)
.await?;
}
ProviderCommands::ListProfiles { output } => {
run::provider_list_profiles(endpoint, output.as_str(), &tls).await?;
+59
View File
@@ -4778,11 +4778,65 @@ pub async fn provider_get(server: &str, name: &str, tls: &TlsOptions) -> Result<
Ok(())
}
fn provider_to_json(provider: &Provider) -> serde_json::Value {
let mut obj = serde_json::Map::new();
// Core fields
obj.insert("id".to_string(), serde_json::json!(provider.object_id()));
obj.insert(
"name".to_string(),
serde_json::json!(provider.object_name()),
);
obj.insert("type".to_string(), serde_json::json!(provider.r#type));
// Credential keys (NEVER values - security)
let credential_keys: Vec<String> = provider.credentials.keys().cloned().collect();
obj.insert(
"credential_keys".to_string(),
serde_json::json!(credential_keys),
);
// Config (non-secret configuration)
if !provider.config.is_empty() {
obj.insert("config".to_string(), serde_json::json!(provider.config));
}
// Metadata fields (only if metadata exists)
if let Some(meta) = &provider.metadata {
if !meta.labels.is_empty() {
obj.insert("labels".to_string(), serde_json::json!(meta.labels));
}
if meta.resource_version != 0 {
obj.insert(
"resource_version".to_string(),
serde_json::json!(meta.resource_version),
);
}
if meta.created_at_ms != 0 {
obj.insert(
"created_at_ms".to_string(),
serde_json::json!(meta.created_at_ms),
);
}
}
// Credential expiration times (only if present)
if !provider.credential_expires_at_ms.is_empty() {
obj.insert(
"credential_expires_at_ms".to_string(),
serde_json::json!(provider.credential_expires_at_ms),
);
}
serde_json::Value::Object(obj)
}
pub async fn provider_list(
server: &str,
limit: u32,
offset: u32,
names_only: bool,
output: &str,
tls: &TlsOptions,
) -> Result<()> {
let mut client = grpc_client(server, tls).await?;
@@ -4792,6 +4846,11 @@ pub async fn provider_list(
.into_diagnostic()?;
let providers = response.into_inner().providers;
// Handle structured output formats (json, yaml)
if crate::output::print_output_collection(output, &providers, provider_to_json)? {
return Ok(());
}
if providers.is_empty() {
if !names_only {
println!("No providers found.");
@@ -997,7 +997,7 @@ async fn provider_cli_run_functions_support_full_crud_flow() {
run::provider_get(&ts.endpoint, "my-claude", &ts.tls)
.await
.expect("provider get");
run::provider_list(&ts.endpoint, 100, 0, false, &ts.tls)
run::provider_list(&ts.endpoint, 100, 0, false, "table", &ts.tls)
.await
.expect("provider list");