cli

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

commit 7e030b0414d2bad0e79a2b07a1b118e681f9231d
parent 5fcb1aa3ff6615a72c134bdd39e832b81a84c93e
Author: triesap <tyson@radroots.org>
Date:   Thu,  7 May 2026 05:00:00 +0000

cli: align listing publish readiness

- make radrootsd publish readiness capability-aware
- report missing bridge auth and signer-session binding
- mark configured daemon listing publish as executable
- cover readiness and not-implemented regressions

Diffstat:
Msrc/operation_core.rs | 85++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Mtests/target_cli.rs | 48+++++++++++++++++++++++++++++++++++++++++++++---
2 files changed, 108 insertions(+), 25 deletions(-)

diff --git a/src/operation_core.rs b/src/operation_core.rs @@ -30,7 +30,9 @@ use crate::runtime::accounts::{ resolve_account_selector, secret_backend_status, select_account, snapshot, unresolved_account_reason, }; -use crate::runtime::config::{PublishMode, RuntimeConfig, SignerBackend}; +use crate::runtime::config::{ + PublishMode, RuntimeConfig, SIGNER_REMOTE_NIP46_CAPABILITY, SignerBackend, +}; use crate::runtime::logging::LoggingState; use crate::runtime_args::LocalExportFormatArg; @@ -722,27 +724,25 @@ fn publish_runtime_view( }, } } - PublishMode::Radrootsd => PublishRuntimeView { - mode: config.publish.mode.as_str().to_owned(), - source, - transport_family: config.publish.mode.transport_family().to_owned(), - state: "unavailable".to_owned(), - executable: false, - reason: Some( - "radrootsd publish mode is configured but the radrootsd publish transport is not implemented" - .to_owned(), - ), - signed_write_required, - relay, - provider: PublishProviderRuntimeView { - provider_runtime_id: "radrootsd".to_owned(), - state: "unavailable".to_owned(), - source: "publish mode · local first".to_owned(), - reason: Some( - "radrootsd publish transport is reserved for a future implementation".to_owned(), - ), - }, - }, + PublishMode::Radrootsd => { + let (state, executable, reason) = radrootsd_publish_readiness(config); + PublishRuntimeView { + mode: config.publish.mode.as_str().to_owned(), + source, + transport_family: config.publish.mode.transport_family().to_owned(), + state: state.to_owned(), + executable, + reason: reason.clone(), + signed_write_required, + relay, + provider: PublishProviderRuntimeView { + provider_runtime_id: "radrootsd".to_owned(), + state: state.to_owned(), + source: "publish mode · local first".to_owned(), + reason, + }, + } + } } } @@ -802,6 +802,47 @@ fn nostr_relay_publish_readiness( ("ready", true, None) } +fn radrootsd_publish_readiness(config: &RuntimeConfig) -> (&'static str, bool, Option<String>) { + if config.rpc.bridge_bearer_token.is_none() { + return ( + "unconfigured", + false, + Some( + "radrootsd listing publish requires bridge bearer token configuration from RADROOTS_RPC_BEARER_TOKEN" + .to_owned(), + ), + ); + } + + if !radrootsd_signer_session_binding_configured(config) { + return ( + "unconfigured", + false, + Some( + "radrootsd listing publish requires a signer.remote_nip46 capability binding with signer_session_ref for config and health readiness" + .to_owned(), + ), + ); + } + + ( + "ready", + true, + Some( + "radrootsd bridge endpoint, bridge auth, and signer-session binding are configured; live daemon readiness is verified when listing publish runs" + .to_owned(), + ), + ) +} + +fn radrootsd_signer_session_binding_configured(config: &RuntimeConfig) -> bool { + config + .capability_binding(SIGNER_REMOTE_NIP46_CAPABILITY) + .and_then(|binding| binding.signer_session_ref.as_deref()) + .map(str::trim) + .is_some_and(|value| !value.is_empty()) +} + fn health_status_state(store_state: &str, publish: &PublishRuntimeView) -> &'static str { if store_state == "ready" && publish_runtime_ready(publish) { "ready" diff --git a/tests/target_cli.rs b/tests/target_cli.rs @@ -210,8 +210,9 @@ fn config_get_exposes_resolved_publish_state() { "user config · local first" ); assert_eq!(value["result"]["publish"]["transport_family"], "radrootsd"); - assert_eq!(value["result"]["publish"]["state"], "unavailable"); + assert_eq!(value["result"]["publish"]["state"], "unconfigured"); assert_eq!(value["result"]["publish"]["executable"], false); + assert_contains(&value["result"]["publish"]["reason"], "bridge bearer token"); assert_eq!( value["result"]["publish"]["provider"]["provider_runtime_id"], "radrootsd" @@ -220,6 +221,43 @@ fn config_get_exposes_resolved_publish_state() { } #[test] +fn config_get_marks_radrootsd_listing_publish_ready_with_bridge_auth_and_session_binding() { + let sandbox = RadrootsCliSandbox::new(); + sandbox.write_app_config( + r#"[publish] +mode = "radrootsd" + +[[capability_binding]] +capability = "signer.remote_nip46" +provider = "myc" +target_kind = "explicit_endpoint" +target = "http://myc.invalid" +signer_session_ref = "session_ready" +"#, + ); + + let mut command = sandbox.command(); + command + .env("RADROOTS_RPC_BEARER_TOKEN", "bridge_test") + .args(["--format", "json", "config", "get"]); + let output = command.output().expect("run config get"); + let value: Value = serde_json::from_slice(&output.stdout).expect("json output"); + + assert!(output.status.success()); + assert_eq!(value["operation_id"], "config.get"); + assert_eq!(value["result"]["publish"]["mode"], "radrootsd"); + assert_eq!(value["result"]["publish"]["relay"]["ready"], false); + assert_eq!(value["result"]["publish"]["state"], "ready"); + assert_eq!(value["result"]["publish"]["executable"], true); + assert_contains( + &value["result"]["publish"]["reason"], + "live daemon readiness is verified when listing publish runs", + ); + assert_eq!(value["result"]["publish"]["provider"]["state"], "ready"); + assert_eq!(value["result"]["rpc"]["bridge_auth_configured"], true); +} + +#[test] fn config_get_distinguishes_relay_ready_from_missing_signed_write_account() { let sandbox = RadrootsCliSandbox::new(); @@ -348,8 +386,9 @@ fn health_surfaces_publish_state_under_deferred_signer_mode() { assert_eq!(value["result"]["publish"]["executable"], false); assert_eq!( value["result"]["publish"]["provider"]["state"], - "unavailable" + "unconfigured" ); + assert_contains(&value["result"]["publish"]["reason"], "bridge bearer token"); assert_eq!(value["errors"].as_array().expect("errors").len(), 0); } @@ -389,7 +428,10 @@ fn health_check_exposes_publish_readiness() { assert_eq!(value["operation_id"], "health.check.run"); assert_eq!(value["result"]["state"], "needs_attention"); assert_eq!(value["result"]["checks"]["publish"]["mode"], "radrootsd"); - assert_eq!(value["result"]["checks"]["publish"]["state"], "unavailable"); + assert_eq!( + value["result"]["checks"]["publish"]["state"], + "unconfigured" + ); assert_eq!(value["result"]["checks"]["publish"]["executable"], false); assert_eq!(value["errors"].as_array().expect("errors").len(), 0); }