cli

Command-line interface for Radroots
git clone https://radroots.dev/git/cli.git
Log | Files | Refs | README | LICENSE

commit 5c201a9d09da2068d93b6e006f9cf2fe55b5e7dd
parent 520e2820b8f629a2d2ccda229066a071279dfab9
Author: triesap <tyson@radroots.org>
Date:   Mon, 13 Jul 2026 06:20:03 +0000

cli: show transport operation capabilities

- add grouped deliver and fetch capabilities to status views
- render capability columns in terminal transport tables
- propagate SDK sync transport capability summaries
- assert operation capability JSON in transport and sync tests

Diffstat:
Msrc/out/terminal/renderers/runtime.rs | 8++++++++
Msrc/runtime/sync.rs | 43+++++++++++++++++++++++++++++++++++++++++--
Msrc/runtime/transport.rs | 9+++++++--
Msrc/view/runtime.rs | 8++++++++
Mtests/target_cli.rs | 29+++++++++++++++++++++++++++++
5 files changed, 93 insertions(+), 4 deletions(-)

diff --git a/src/out/terminal/renderers/runtime.rs b/src/out/terminal/renderers/runtime.rs @@ -195,6 +195,12 @@ fn push_transport_status_table(document: &mut TerminalDocument, result: &Value, common::bool_path(transport, &["usable_for_delivery"]) .map(|value| if value { "yes" } else { "no" }.to_owned()) .unwrap_or_default(), + common::bool_path(transport, &["capabilities", "deliver"]) + .map(|value| if value { "yes" } else { "no" }.to_owned()) + .unwrap_or_default(), + common::bool_path(transport, &["capabilities", "fetch"]) + .map(|value| if value { "yes" } else { "no" }.to_owned()) + .unwrap_or_default(), common::string(transport, &["endpoint_uri"]).unwrap_or_default(), ]) }) @@ -207,6 +213,8 @@ fn push_transport_status_table(document: &mut TerminalDocument, result: &Value, TerminalTableColumn::new("Configured", 10, 10), TerminalTableColumn::new("Implementation", 14, 24), TerminalTableColumn::new("Usable", 6, 6), + TerminalTableColumn::new("Deliver", 7, 7), + TerminalTableColumn::new("Fetch", 5, 5), TerminalTableColumn::new("Endpoint", 8, 32), ], rows, diff --git a/src/runtime/sync.rs b/src/runtime/sync.rs @@ -42,7 +42,7 @@ use crate::runtime::sdk::{ use crate::view::runtime::{ SyncActionView, SyncFreshnessView, SyncQueueView, SyncRunFreshnessView, SyncStatusView, SyncTransportStatusView, SyncTransportTargetView, SyncWatchFrameView, SyncWatchView, - TransportTargetFailureView, + TransportOperationCapabilitiesView, TransportTargetFailureView, }; const SYNC_SOURCE: &str = "local replica ยท local first"; @@ -613,6 +613,10 @@ fn sdk_transport_status_view( configured: status.configured, implementation: status.implementation.clone(), usable_for_delivery: status.usable_for_delivery, + capabilities: TransportOperationCapabilitiesView { + deliver: status.capabilities.deliver, + fetch: status.capabilities.fetch, + }, message: status.message.clone(), } } @@ -1015,6 +1019,10 @@ fn sync_transport_status_view(status: RadrootsTransportStatus) -> SyncTransportS configured: status.configured, implementation: transport_implementation_label(status.implementation).to_owned(), usable_for_delivery: status.usable_for_delivery, + capabilities: TransportOperationCapabilitiesView { + deliver: status.capabilities.deliver, + fetch: status.capabilities.fetch, + }, message: status.message, } } @@ -1774,7 +1782,8 @@ mod tests { PushOutboxEventReceipt, PushOutboxEventState, PushOutboxReceipt, PushOutboxTargetOutcomeKind, PushOutboxTargetReceipt, PushOutboxTransportOutcomeKind, SyncEventStoreStatus, SyncOutboxStatus, SyncStatusReceipt, SyncStatusSource, - SyncTransportProfileSummary, SyncTransportStatusSummary, SyncTransportTargetSummary, + SyncTransportOperationCapabilitiesSummary, SyncTransportProfileSummary, + SyncTransportStatusSummary, SyncTransportTargetSummary, }; use radroots_secret_vault::RadrootsSecretBackend; use radroots_transport::{ @@ -1868,6 +1877,8 @@ mod tests { assert!(!view.transport_statuses[0].configured); assert_eq!(view.transport_statuses[0].implementation, "real"); assert!(!view.transport_statuses[0].usable_for_delivery); + assert!(!view.transport_statuses[0].capabilities.deliver); + assert!(!view.transport_statuses[0].capabilities.fetch); assert!(view.target_transport_endpoints.is_empty()); assert_eq!( view.actions, @@ -1900,6 +1911,8 @@ mod tests { assert_eq!(view.transport_statuses.len(), 2); assert_eq!(view.transport_statuses[0].transport, "nostr"); assert!(view.transport_statuses[0].usable_for_delivery); + assert!(view.transport_statuses[0].capabilities.deliver); + assert!(!view.transport_statuses[0].capabilities.fetch); assert_eq!(view.transport_statuses[1].transport, "reticulum"); assert!(view.transport_statuses[1].configured); assert_eq!( @@ -1907,6 +1920,8 @@ mod tests { "preview_unavailable" ); assert!(!view.transport_statuses[1].usable_for_delivery); + assert!(!view.transport_statuses[1].capabilities.deliver); + assert!(!view.transport_statuses[1].capabilities.fetch); assert_eq!( view.target_transport_endpoints, vec!["wss://relay.example.com"] @@ -1944,6 +1959,8 @@ mod tests { assert!(!view.transport_statuses[0].configured); assert_eq!(view.transport_statuses[0].implementation, "real"); assert!(!view.transport_statuses[0].usable_for_delivery); + assert!(!view.transport_statuses[0].capabilities.deliver); + assert!(!view.transport_statuses[0].capabilities.fetch); assert_eq!(view.transport_statuses[1].transport, "reticulum"); assert!(view.transport_statuses[1].configured); assert_eq!( @@ -1951,6 +1968,8 @@ mod tests { "preview_unavailable" ); assert!(!view.transport_statuses[1].usable_for_delivery); + assert!(!view.transport_statuses[1].capabilities.deliver); + assert!(!view.transport_statuses[1].capabilities.fetch); assert!(view.target_transport_endpoints.is_empty()); assert_eq!(view.actions, vec!["radroots transport profile get"]); } @@ -1997,6 +2016,8 @@ mod tests { "preview_unavailable" ); assert!(!view.transport_statuses[0].usable_for_delivery); + assert!(!view.transport_statuses[0].capabilities.deliver); + assert!(!view.transport_statuses[0].capabilities.fetch); assert_eq!(view.fetched_count, None); assert!(view.target_transport_endpoints.is_empty()); assert!( @@ -2097,6 +2118,10 @@ mod tests { configured: true, implementation: "real".to_owned(), usable_for_delivery: true, + capabilities: SyncTransportOperationCapabilitiesSummary { + deliver: true, + fetch: false, + }, message: "Nostr relay transport is configured for delivery".to_owned(), }, SyncTransportStatusSummary { @@ -2106,6 +2131,10 @@ mod tests { configured: true, implementation: "preview_unavailable".to_owned(), usable_for_delivery: false, + capabilities: SyncTransportOperationCapabilitiesSummary { + deliver: false, + fetch: false, + }, message: RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE.to_owned(), }, ], @@ -2126,6 +2155,8 @@ mod tests { "preview_unavailable" ); assert!(!view.transport_statuses[1].usable_for_delivery); + assert!(!view.transport_statuses[1].capabilities.deliver); + assert!(!view.transport_statuses[1].capabilities.fetch); } #[test] @@ -2482,6 +2513,10 @@ mod tests { configured: true, implementation: "real".to_owned(), usable_for_delivery: true, + capabilities: SyncTransportOperationCapabilitiesSummary { + deliver: true, + fetch: false, + }, message: "Nostr relay transport is configured for delivery".to_owned(), }], }, @@ -2532,6 +2567,10 @@ mod tests { configured: true, implementation: "preview_unavailable".to_owned(), usable_for_delivery: false, + capabilities: SyncTransportOperationCapabilitiesSummary { + deliver: false, + fetch: false, + }, message: RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE.to_owned(), }], }, diff --git a/src/runtime/transport.rs b/src/runtime/transport.rs @@ -18,8 +18,9 @@ use crate::runtime::RuntimeError; use crate::runtime::config::{RuntimeConfig, TransportProfileKind}; use crate::runtime::sdk::{CliSdkAdapterError, CliSdkSession, sdk_nostr_relay_url_policy}; use crate::view::runtime::{ - TransportOutboxPushView, TransportOutboxStatusView, TransportProfileSummaryView, - TransportProfileView, TransportRuntimeStatusView, TransportStatusView, + TransportOperationCapabilitiesView, TransportOutboxPushView, TransportOutboxStatusView, + TransportProfileSummaryView, TransportProfileView, TransportRuntimeStatusView, + TransportStatusView, }; const TRANSPORT_SOURCE: &str = "transport profile config"; @@ -551,6 +552,10 @@ fn transport_runtime_status_view(status: RadrootsTransportStatus) -> TransportRu configured: status.configured, implementation: transport_implementation_label(status.implementation).to_owned(), usable_for_delivery: status.usable_for_delivery, + capabilities: TransportOperationCapabilitiesView { + deliver: status.capabilities.deliver, + fetch: status.capabilities.fetch, + }, message: status.message, } } diff --git a/src/view/runtime.rs b/src/view/runtime.rs @@ -3318,10 +3318,17 @@ pub struct TransportRuntimeStatusView { pub configured: bool, pub implementation: String, pub usable_for_delivery: bool, + pub capabilities: TransportOperationCapabilitiesView, pub message: String, } #[derive(Debug, Clone, Serialize)] +pub struct TransportOperationCapabilitiesView { + pub deliver: bool, + pub fetch: bool, +} + +#[derive(Debug, Clone, Serialize)] pub struct TransportOutboxStatusView { pub state: String, pub source: String, @@ -3569,6 +3576,7 @@ pub struct SyncTransportStatusView { pub configured: bool, pub implementation: String, pub usable_for_delivery: bool, + pub capabilities: TransportOperationCapabilitiesView, pub message: String, } diff --git a/tests/target_cli.rs b/tests/target_cli.rs @@ -107,6 +107,11 @@ fn assert_no_removed_transport_status_fields(status: &Value) { } } +fn assert_transport_capabilities(status: &Value, deliver: bool, fetch: bool) { + assert_eq!(status["capabilities"]["deliver"], deliver); + assert_eq!(status["capabilities"]["fetch"], fetch); +} + fn write_proxy_transport_config(sandbox: &RadrootsCliSandbox, body: &str) -> PathBuf { sandbox.write_app_config( format!("[transport]\nprofile = \"proxy\"\n\n[transport.proxy]\n{body}").as_str(), @@ -1445,6 +1450,7 @@ fn transport_profile_reticulum_preview_output_is_transport_specific() { result["transport_statuses"][0]["usable_for_delivery"], false ); + assert_transport_capabilities(&result["transport_statuses"][0], false, false); assert_eq!( result["transport_statuses"][0]["message"], RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE @@ -1492,6 +1498,7 @@ fn transport_profile_local_only_output_uses_profile_and_local_status_fields() { assert_eq!(local["implementation"], "real"); assert_eq!(local["configured"], true); assert_eq!(local["usable_for_delivery"], false); + assert_transport_capabilities(local, false, false); assert_eq!( local["message"], "Local-only profile writes only to local state" @@ -1550,6 +1557,7 @@ fn transport_profile_set_hybrid_persists_nostr_and_reticulum_preview_config() { value["result"]["transport_statuses"][0]["usable_for_delivery"], true ); + assert_transport_capabilities(&value["result"]["transport_statuses"][0], true, false); assert_eq!( value["result"]["transport_statuses"][1]["transport"], "reticulum" @@ -1563,6 +1571,7 @@ fn transport_profile_set_hybrid_persists_nostr_and_reticulum_preview_config() { value["result"]["transport_statuses"][1]["usable_for_delivery"], false ); + assert_transport_capabilities(&value["result"]["transport_statuses"][1], false, false); assert_no_removed_transport_status_fields(&value["result"]["transport_statuses"][0]); assert_no_removed_transport_status_fields(&value["result"]["transport_statuses"][1]); assert_eq!( @@ -1623,10 +1632,12 @@ fn transport_status_hybrid_output_reports_nostr_and_reticulum_rows() { assert_eq!(nostr["implementation"], "real"); assert_eq!(nostr["configured"], true); assert_eq!(nostr["usable_for_delivery"], true); + assert_transport_capabilities(nostr, true, false); assert_eq!(reticulum["transport"], "reticulum"); assert_eq!(reticulum["implementation"], "preview_unavailable"); assert_eq!(reticulum["configured"], true); assert_eq!(reticulum["usable_for_delivery"], false); + assert_transport_capabilities(reticulum, false, false); assert_eq!(reticulum["message"], RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE); assert_no_removed_transport_status_fields(nostr); assert_no_removed_transport_status_fields(reticulum); @@ -1652,6 +1663,7 @@ fn transport_status_nostr_output_uses_canonical_transport_row() { assert_eq!(nostr["implementation"], "real"); assert_eq!(nostr["configured"], true); assert_eq!(nostr["usable_for_delivery"], true); + assert_transport_capabilities(nostr, true, false); assert_no_removed_transport_status_fields(nostr); } @@ -1680,6 +1692,7 @@ fn transport_status_reticulum_preview_output_reports_unusable_preview_state() { assert_eq!(active["implementation"], "preview_unavailable"); assert_eq!(active["configured"], true); assert_eq!(active["usable_for_delivery"], false); + assert_transport_capabilities(active, false, false); assert_eq!(active["message"], RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE); assert!( !active["message"] @@ -2004,6 +2017,10 @@ fn transport_source_boundary_rejects_removed_relay_and_publish_proxy_surfaces() "pub configured: bool,", "pub implementation: String,", "pub usable_for_delivery: bool,", + "pub capabilities: TransportOperationCapabilitiesView,", + "pub struct TransportOperationCapabilitiesView", + "pub deliver: bool,", + "pub fetch: bool,", "pub message: String,", ] { assert!( @@ -2194,6 +2211,7 @@ fn sync_transport_status_source_boundary_rejects_retired_relay_shaped_generic_ou "pub configured: bool,", "pub implementation: String,", "pub usable_for_delivery: bool,", + "pub capabilities: TransportOperationCapabilitiesView,", "pub message: String,", "pub target_transport_endpoints: Vec<String>,", "pub attempted_transport_endpoints: Vec<String>,", @@ -2211,6 +2229,15 @@ fn sync_transport_status_source_boundary_rejects_retired_relay_shaped_generic_ou "pub struct SyncTransportStatusView", "pub struct TransportTargetFailureView", ); + for required in [ + "pub capabilities: TransportOperationCapabilitiesView,", + "pub message: String,", + ] { + assert!( + sync_status_view_source.contains(required), + "sync transport status view must retain canonical field `{required}`" + ); + } for forbidden in [ "pub transport_kind:", "pub implementation_state:", @@ -2583,6 +2610,7 @@ fn transport_profile_set_proxy_persists_token_file_without_printing_token_materi value["result"]["transport_statuses"][0]["usable_for_delivery"], true ); + assert_transport_capabilities(&value["result"]["transport_statuses"][0], true, false); assert_no_removed_transport_status_fields(&value["result"]["transport_statuses"][0]); assert_eq!(value["result"]["proxy_token_source"], "token_file"); assert_eq!( @@ -2646,6 +2674,7 @@ fn transport_profile_set_proxy_persists_token_secret_id_without_printing_token_m value["result"]["transport_statuses"][0]["usable_for_delivery"], true ); + assert_transport_capabilities(&value["result"]["transport_statuses"][0], true, false); assert_no_removed_transport_status_fields(&value["result"]["transport_statuses"][0]); assert_eq!(value["result"]["proxy_token_source"], "token_secret_id"); assert!(value["result"]["proxy_token_file"].is_null());