cli

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

commit 7e9023919e2ad6b1748be4ea17d2213321bfb864
parent fb2711c02bec2c399a5a533d19568b759ec68332
Author: triesap <tyson@radroots.org>
Date:   Sat, 27 Jun 2026 08:27:15 +0000

cli: harden farm recovery actions

- centralize approval-aware farm rebind recovery commands
- include dry-run and approval-token actions for stale farm bindings
- reuse farm-bound seller recovery actions from listing defaults
- prove terminal and JSON recovery output in signer tests

Diffstat:
Msrc/runtime/farm.rs | 39+++++++++++++++++++++++----------------
Msrc/runtime/listing.rs | 5+----
Mtests/signer_runtime_modes.rs | 53++++++++++++++++++++++++++++++++++++++++++++++++++---
3 files changed, 74 insertions(+), 23 deletions(-)

diff --git a/src/runtime/farm.rs b/src/runtime/farm.rs @@ -212,10 +212,7 @@ fn rebind_inner( "farm seller binding updated".to_owned() }), actions: if dry_run { - vec![format!( - "radroots --approval-token approve farm rebind {}", - args.selector - )] + vec![farm_rebind_live_action(args.selector.as_str())] } else { vec!["radroots farm readiness check".to_owned()] }, @@ -522,8 +519,7 @@ pub fn status( }; let mut actions = Vec::new(); if account.is_none() { - actions.push("radroots account import <path>".to_owned()); - actions.push("radroots farm rebind <selector>".to_owned()); + actions.extend(farm_bound_seller_recovery_actions("<selector>")); } else if draft_missing.is_empty() { actions.extend(publish.actions.clone()); } else { @@ -722,10 +718,7 @@ pub fn publish( resolved.document.selection.account ), vec!["Farm-bound seller account".to_owned()], - vec![ - "radroots account import <path>".to_owned(), - "radroots farm rebind <selector>".to_owned(), - ], + farm_bound_seller_recovery_actions("<selector>"), config.output.dry_run, true, resolved.document.selection.account.clone(), @@ -1583,7 +1576,7 @@ fn init_document( && document.selection.account != account.record.account_id.to_string() { let message = format!( - "account mismatch: farm config is bound to seller account `{}`; use `radroots farm rebind {}` to change the farm-bound seller account", + "account mismatch: farm config is bound to seller account `{}`; use `radroots --approval-token approve farm rebind {}` to change the farm-bound seller account", document.selection.account, account.record.account_id ); return Err(account::AccountRuntimeFailure::mismatch_with_detail( @@ -1592,7 +1585,7 @@ fn init_document( "seller_actor_source": FARM_SELLER_ACTOR_SOURCE, "farm_bound_seller_account_id": document.selection.account, "attempted_seller_account_id": account.record.account_id.to_string(), - "actions": [format!("radroots farm rebind {}", account.record.account_id)], + "actions": farm_rebind_recovery_actions(account.record.account_id.as_str()), }), ) .into()); @@ -1768,7 +1761,7 @@ fn farm_setup_actions( ) -> Vec<String> { let mut actions = vec!["radroots farm readiness check".to_owned()]; if account.is_none() { - actions.extend(farm_bound_seller_recovery_actions()); + actions.extend(farm_bound_seller_recovery_actions("<selector>")); return actions; } if farm_config::missing_fields(document).is_empty() @@ -1785,13 +1778,27 @@ fn missing_farm_bound_seller_reason(account_id: &str) -> String { format!("farm-bound seller account `{account_id}` is not present in the local account store") } -fn farm_bound_seller_recovery_actions() -> Vec<String> { +pub(crate) fn farm_bound_seller_recovery_actions(selector: &str) -> Vec<String> { + let mut actions = vec!["radroots account import <path>".to_owned()]; + actions.extend(farm_rebind_recovery_actions(selector)); + actions +} + +pub(crate) fn farm_rebind_recovery_actions(selector: &str) -> Vec<String> { vec![ - "radroots account import <path>".to_owned(), - "radroots farm rebind <selector>".to_owned(), + farm_rebind_dry_run_action(selector), + farm_rebind_live_action(selector), ] } +pub(crate) fn farm_rebind_dry_run_action(selector: &str) -> String { + format!("radroots --dry-run farm rebind {selector}") +} + +pub(crate) fn farm_rebind_live_action(selector: &str) -> String { + format!("radroots --approval-token approve farm rebind {selector}") +} + fn account_recovery_actions() -> Vec<String> { vec![ "radroots account import <path>".to_owned(), diff --git a/src/runtime/listing.rs b/src/runtime/listing.rs @@ -2901,10 +2901,7 @@ fn authoring_defaults(config: &RuntimeConfig) -> Result<ListingAuthoringDefaults json!({ "seller_actor_source": "farm_config", "farm_bound_seller_account_id": account_id, - "actions": [ - "radroots account import <path>", - "radroots farm rebind <selector>", - ], + "actions": crate::runtime::farm::farm_bound_seller_recovery_actions("<selector>"), }), ) .into()); diff --git a/tests/signer_runtime_modes.rs b/tests/signer_runtime_modes.rs @@ -1571,7 +1571,46 @@ fn farm_rebind_is_explicit_and_publish_defaults_ignore_ambient_selection() { ); assert_next_action_present( &retarget, - format!("radroots farm rebind {second_account_id}").as_str(), + format!("radroots --dry-run farm rebind {second_account_id}").as_str(), + ); + assert_next_action_present( + &retarget, + format!("radroots --approval-token approve farm rebind {second_account_id}").as_str(), + ); + + let terminal_retarget_output = sandbox + .command() + .args([ + "farm", + "create", + "--name", + "Green Farm Retarget", + "--location", + "farmstand", + "--city", + "San Francisco", + "--country", + "US", + "--geohash", + "9q8yy", + "--delivery-method", + "pickup", + ]) + .output() + .expect("terminal retarget"); + assert!(!terminal_retarget_output.status.success()); + assert!(terminal_retarget_output.stdout.is_empty()); + let terminal_stderr = + String::from_utf8(terminal_retarget_output.stderr).expect("terminal stderr"); + assert!( + terminal_stderr.contains("Next\n radroots --dry-run farm rebind"), + "{terminal_stderr}" + ); + assert!( + terminal_stderr.contains( + format!("radroots --approval-token approve farm rebind {second_account_id}").as_str(), + ), + "{terminal_stderr}" ); let (missing_rebind_output, missing_rebind) = sandbox.json_output(&[ @@ -1790,7 +1829,11 @@ fn missing_farm_bound_seller_blocks_listing_create_and_guides_setup_repair() { assert_eq!(updated["operation_id"], "farm.profile.update"); assert_contains(&updated["result"]["reason"], "farm-bound seller account"); assert_action_present(&updated, "radroots account import <path>"); - assert_action_present(&updated, "radroots farm rebind <selector>"); + assert_action_present(&updated, "radroots --dry-run farm rebind <selector>"); + assert_action_present( + &updated, + "radroots --approval-token approve farm rebind <selector>", + ); let listing_path = sandbox.root().join("missing-seller-listing.toml"); let (listing_output, listing) = sandbox.json_output(&[ @@ -1841,7 +1884,11 @@ fn missing_farm_bound_seller_blocks_listing_create_and_guides_setup_repair() { first_account_id ); assert_next_action_present(&listing, "radroots account import <path>"); - assert_next_action_present(&listing, "radroots farm rebind <selector>"); + assert_next_action_present(&listing, "radroots --dry-run farm rebind <selector>"); + assert_next_action_present( + &listing, + "radroots --approval-token approve farm rebind <selector>", + ); assert!(!listing_path.exists()); }