diff --git a/crates/openshell-cli/src/main.rs b/crates/openshell-cli/src/main.rs index d1fc30308..f69ef43bb 100644 --- a/crates/openshell-cli/src/main.rs +++ b/crates/openshell-cli/src/main.rs @@ -773,8 +773,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. @@ -2906,8 +2910,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?; diff --git a/crates/openshell-cli/src/run.rs b/crates/openshell-cli/src/run.rs index 5801c6629..06d4325e0 100644 --- a/crates/openshell-cli/src/run.rs +++ b/crates/openshell-cli/src/run.rs @@ -4718,11 +4718,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 = 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?; @@ -4732,6 +4786,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."); diff --git a/crates/openshell-cli/tests/provider_commands_integration.rs b/crates/openshell-cli/tests/provider_commands_integration.rs index ed78c6659..c2aa71b8b 100644 --- a/crates/openshell-cli/tests/provider_commands_integration.rs +++ b/crates/openshell-cli/tests/provider_commands_integration.rs @@ -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");