cli

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

main.rs (25391B)


      1 #![forbid(unsafe_code)]
      2 
      3 mod cli;
      4 mod ops;
      5 mod out;
      6 mod registry;
      7 mod runtime;
      8 mod view;
      9 
     10 use std::io::Write;
     11 use std::process::ExitCode;
     12 use std::sync::atomic::{AtomicU64, Ordering};
     13 use std::time::{SystemTime, UNIX_EPOCH};
     14 
     15 use clap::Parser;
     16 use serde_json::Value;
     17 
     18 use crate::cli::input::runtime_invocation_args_from_target;
     19 use crate::cli::{TargetCliArgs, TargetOutputFormat};
     20 use crate::ops::exec::{
     21     BasketOperationService, CoreOperationService, FarmOperationService, ListingOperationService,
     22     MarketOperationService, OrderOperationService, RuntimeOperationService,
     23     ValidationOperationService,
     24 };
     25 use crate::ops::{
     26     OperationAdapter, OperationAdapterError, OperationNetworkMode, OperationOutputFormat,
     27     OperationRequest, OperationRequestPayload, OperationResultPayload, OperationService,
     28     TargetOperationRequest,
     29 };
     30 use crate::out::envelope::OutputEnvelope;
     31 use crate::registry::{NetworkRequirement, network_requirement, requires_local_signer_mode};
     32 use crate::runtime::config::{RuntimeConfig, SignerBackend};
     33 use crate::runtime::logging::initialize_logging;
     34 
     35 static REQUEST_SEQUENCE: AtomicU64 = AtomicU64::new(0);
     36 
     37 fn main() -> ExitCode {
     38     match run() {
     39         Ok(exit_code) => exit_code,
     40         Err(error) => {
     41             let _ = writeln!(std::io::stderr(), "{error}");
     42             error.exit_code()
     43         }
     44     }
     45 }
     46 
     47 fn run() -> Result<ExitCode, runtime::RuntimeError> {
     48     debug_assert!(registry::registry_linkage_is_valid());
     49     debug_assert!(ops::adapter_registry_linkage_is_valid());
     50     let args = TargetCliArgs::parse();
     51     let request =
     52         TargetOperationRequest::from_target_args(&args).map_err(operation_config_error)?;
     53     if let Err(error) = validate_pre_runtime_request_contract(&request) {
     54         let envelope = failure_envelope(&request, error);
     55         render_envelope(&envelope, args.format)?;
     56         return Ok(envelope_exit_code(&envelope));
     57     }
     58     let config = RuntimeConfig::from_system(&runtime_invocation_args_from_target(&args))?;
     59     let logging = initialize_logging(&config.logging)?;
     60     let envelope = match validate_request_contract(&request, &config) {
     61         Ok(()) => execute_request(request, &config, &logging),
     62         Err(error) => failure_envelope(&request, error),
     63     };
     64     render_envelope(&envelope, args.format)?;
     65     Ok(envelope_exit_code(&envelope))
     66 }
     67 
     68 fn execute_request(
     69     request: TargetOperationRequest,
     70     config: &RuntimeConfig,
     71     logging: &runtime::logging::LoggingState,
     72 ) -> OutputEnvelope {
     73     match request {
     74         TargetOperationRequest::WorkspaceInit(request) => {
     75             execute_with(CoreOperationService::new(config, logging), request)
     76         }
     77         TargetOperationRequest::WorkspaceGet(request) => {
     78             execute_with(CoreOperationService::new(config, logging), request)
     79         }
     80         TargetOperationRequest::HealthStatusGet(request) => {
     81             execute_with(CoreOperationService::new(config, logging), request)
     82         }
     83         TargetOperationRequest::HealthCheckRun(request) => {
     84             execute_with(CoreOperationService::new(config, logging), request)
     85         }
     86         TargetOperationRequest::ConfigGet(request) => {
     87             execute_with(CoreOperationService::new(config, logging), request)
     88         }
     89         TargetOperationRequest::AccountCreate(request) => {
     90             execute_with(CoreOperationService::new(config, logging), request)
     91         }
     92         TargetOperationRequest::AccountImport(request) => {
     93             execute_with(CoreOperationService::new(config, logging), request)
     94         }
     95         TargetOperationRequest::AccountAttachSecret(request) => {
     96             execute_with(CoreOperationService::new(config, logging), request)
     97         }
     98         TargetOperationRequest::AccountGet(request) => {
     99             execute_with(CoreOperationService::new(config, logging), request)
    100         }
    101         TargetOperationRequest::AccountList(request) => {
    102             execute_with(CoreOperationService::new(config, logging), request)
    103         }
    104         TargetOperationRequest::AccountRemove(request) => {
    105             execute_with(CoreOperationService::new(config, logging), request)
    106         }
    107         TargetOperationRequest::AccountSelectionGet(request) => {
    108             execute_with(CoreOperationService::new(config, logging), request)
    109         }
    110         TargetOperationRequest::AccountSelectionUpdate(request) => {
    111             execute_with(CoreOperationService::new(config, logging), request)
    112         }
    113         TargetOperationRequest::AccountSelectionClear(request) => {
    114             execute_with(CoreOperationService::new(config, logging), request)
    115         }
    116         TargetOperationRequest::StoreInit(request) => {
    117             execute_with(CoreOperationService::new(config, logging), request)
    118         }
    119         TargetOperationRequest::StoreStatusGet(request) => {
    120             execute_with(CoreOperationService::new(config, logging), request)
    121         }
    122         TargetOperationRequest::StoreExport(request) => {
    123             execute_with(CoreOperationService::new(config, logging), request)
    124         }
    125         TargetOperationRequest::StoreBackupCreate(request) => {
    126             execute_with(CoreOperationService::new(config, logging), request)
    127         }
    128         TargetOperationRequest::StoreBackupRestore(request) => {
    129             execute_with(CoreOperationService::new(config, logging), request)
    130         }
    131         TargetOperationRequest::SignerStatusGet(request) => {
    132             execute_with(RuntimeOperationService::new(config), request)
    133         }
    134         TargetOperationRequest::RelayList(request) => {
    135             execute_with(RuntimeOperationService::new(config), request)
    136         }
    137         TargetOperationRequest::SyncStatusGet(request) => {
    138             execute_with(RuntimeOperationService::new(config), request)
    139         }
    140         TargetOperationRequest::SyncPull(request) => {
    141             execute_with(RuntimeOperationService::new(config), request)
    142         }
    143         TargetOperationRequest::SyncPush(request) => {
    144             execute_with(RuntimeOperationService::new(config), request)
    145         }
    146         TargetOperationRequest::SyncWatch(request) => {
    147             execute_with(RuntimeOperationService::new(config), request)
    148         }
    149         TargetOperationRequest::FarmCreate(request) => {
    150             execute_with(FarmOperationService::new(config), request)
    151         }
    152         TargetOperationRequest::FarmGet(request) => {
    153             execute_with(FarmOperationService::new(config), request)
    154         }
    155         TargetOperationRequest::FarmRebind(request) => {
    156             execute_with(FarmOperationService::new(config), request)
    157         }
    158         TargetOperationRequest::FarmProfileUpdate(request) => {
    159             execute_with(FarmOperationService::new(config), request)
    160         }
    161         TargetOperationRequest::FarmLocationUpdate(request) => {
    162             execute_with(FarmOperationService::new(config), request)
    163         }
    164         TargetOperationRequest::FarmFulfillmentUpdate(request) => {
    165             execute_with(FarmOperationService::new(config), request)
    166         }
    167         TargetOperationRequest::FarmReadinessCheck(request) => {
    168             execute_with(FarmOperationService::new(config), request)
    169         }
    170         TargetOperationRequest::FarmPublish(request) => {
    171             execute_with(FarmOperationService::new(config), request)
    172         }
    173         TargetOperationRequest::ListingCreate(request) => {
    174             execute_with(ListingOperationService::new(config), request)
    175         }
    176         TargetOperationRequest::ListingGet(request) => {
    177             execute_with(ListingOperationService::new(config), request)
    178         }
    179         TargetOperationRequest::ListingList(request) => {
    180             execute_with(ListingOperationService::new(config), request)
    181         }
    182         TargetOperationRequest::ListingAppList(request) => {
    183             execute_with(ListingOperationService::new(config), request)
    184         }
    185         TargetOperationRequest::ListingAppExport(request) => {
    186             execute_with(ListingOperationService::new(config), request)
    187         }
    188         TargetOperationRequest::ListingUpdate(request) => {
    189             execute_with(ListingOperationService::new(config), request)
    190         }
    191         TargetOperationRequest::ListingValidate(request) => {
    192             execute_with(ListingOperationService::new(config), request)
    193         }
    194         TargetOperationRequest::ListingRebind(request) => {
    195             execute_with(ListingOperationService::new(config), request)
    196         }
    197         TargetOperationRequest::ListingPublish(request) => {
    198             execute_with(ListingOperationService::new(config), request)
    199         }
    200         TargetOperationRequest::ListingArchive(request) => {
    201             execute_with(ListingOperationService::new(config), request)
    202         }
    203         TargetOperationRequest::MarketRefresh(request) => {
    204             execute_with(MarketOperationService::new(config), request)
    205         }
    206         TargetOperationRequest::MarketProductSearch(request) => {
    207             execute_with(MarketOperationService::new(config), request)
    208         }
    209         TargetOperationRequest::MarketListingGet(request) => {
    210             execute_with(MarketOperationService::new(config), request)
    211         }
    212         TargetOperationRequest::BasketCreate(request) => {
    213             execute_with(BasketOperationService::new(config), request)
    214         }
    215         TargetOperationRequest::BasketGet(request) => {
    216             execute_with(BasketOperationService::new(config), request)
    217         }
    218         TargetOperationRequest::BasketList(request) => {
    219             execute_with(BasketOperationService::new(config), request)
    220         }
    221         TargetOperationRequest::BasketItemAdd(request) => {
    222             execute_with(BasketOperationService::new(config), request)
    223         }
    224         TargetOperationRequest::BasketItemUpdate(request) => {
    225             execute_with(BasketOperationService::new(config), request)
    226         }
    227         TargetOperationRequest::BasketItemRemove(request) => {
    228             execute_with(BasketOperationService::new(config), request)
    229         }
    230         TargetOperationRequest::BasketAdjustmentAdd(request) => {
    231             execute_with(BasketOperationService::new(config), request)
    232         }
    233         TargetOperationRequest::BasketAdjustmentRemove(request) => {
    234             execute_with(BasketOperationService::new(config), request)
    235         }
    236         TargetOperationRequest::BasketValidate(request) => {
    237             execute_with(BasketOperationService::new(config), request)
    238         }
    239         TargetOperationRequest::BasketQuoteCreate(request) => {
    240             execute_with(BasketOperationService::new(config), request)
    241         }
    242         TargetOperationRequest::OrderSubmit(request) => {
    243             execute_with(OrderOperationService::new(config), request)
    244         }
    245         TargetOperationRequest::OrderGet(request) => {
    246             execute_with(OrderOperationService::new(config), request)
    247         }
    248         TargetOperationRequest::OrderList(request) => {
    249             execute_with(OrderOperationService::new(config), request)
    250         }
    251         TargetOperationRequest::OrderAppList(request) => {
    252             execute_with(OrderOperationService::new(config), request)
    253         }
    254         TargetOperationRequest::OrderAppExport(request) => {
    255             execute_with(OrderOperationService::new(config), request)
    256         }
    257         TargetOperationRequest::OrderRebind(request) => {
    258             execute_with(OrderOperationService::new(config), request)
    259         }
    260         TargetOperationRequest::OrderAccept(request) => {
    261             execute_with(OrderOperationService::new(config), request)
    262         }
    263         TargetOperationRequest::OrderDecline(request) => {
    264             execute_with(OrderOperationService::new(config), request)
    265         }
    266         TargetOperationRequest::OrderCancel(request) => {
    267             execute_with(OrderOperationService::new(config), request)
    268         }
    269         TargetOperationRequest::OrderRevisionPropose(request) => {
    270             execute_with(OrderOperationService::new(config), request)
    271         }
    272         TargetOperationRequest::OrderRevisionAccept(request) => {
    273             execute_with(OrderOperationService::new(config), request)
    274         }
    275         TargetOperationRequest::OrderRevisionDecline(request) => {
    276             execute_with(OrderOperationService::new(config), request)
    277         }
    278         TargetOperationRequest::OrderStatusGet(request) => {
    279             execute_with(OrderOperationService::new(config), request)
    280         }
    281         TargetOperationRequest::OrderEventList(request) => {
    282             execute_with(OrderOperationService::new(config), request)
    283         }
    284         TargetOperationRequest::OrderEventWatch(request) => {
    285             execute_with(OrderOperationService::new(config), request)
    286         }
    287         TargetOperationRequest::ValidationReceiptGet(request) => {
    288             execute_with(ValidationOperationService::new(config), request)
    289         }
    290         TargetOperationRequest::ValidationReceiptList(request) => {
    291             execute_with(ValidationOperationService::new(config), request)
    292         }
    293         TargetOperationRequest::ValidationReceiptVerify(request) => {
    294             execute_with(ValidationOperationService::new(config), request)
    295         }
    296     }
    297 }
    298 
    299 fn execute_with<S, P>(service: S, request: OperationRequest<P>) -> OutputEnvelope
    300 where
    301     S: OperationService<P>,
    302     P: OperationRequestPayload,
    303     S::Result: OperationResultPayload,
    304 {
    305     let operation_id = request.operation_id().to_owned();
    306     let envelope_context = request
    307         .context
    308         .envelope_context(next_request_id(&operation_id));
    309     match OperationAdapter::new(service)
    310         .execute(request)
    311         .and_then(|result| result.to_envelope(envelope_context.clone()))
    312     {
    313         Ok(envelope) => envelope,
    314         Err(error) => {
    315             OutputEnvelope::failure(operation_id, error.to_output_error(), envelope_context)
    316         }
    317     }
    318 }
    319 
    320 fn validate_request_contract(
    321     request: &TargetOperationRequest,
    322     config: &RuntimeConfig,
    323 ) -> Result<(), OperationAdapterError> {
    324     validate_pre_runtime_request_contract(request)?;
    325     validate_publish_transport_contract(request, config)?;
    326     validate_signer_mode_contract(request, config)?;
    327     validate_network_contract(request, config)?;
    328     Ok(())
    329 }
    330 
    331 fn validate_pre_runtime_request_contract(
    332     request: &TargetOperationRequest,
    333 ) -> Result<(), OperationAdapterError> {
    334     let spec = request.spec();
    335     if matches!(
    336         request.context().output_format,
    337         OperationOutputFormat::Ndjson
    338     ) && !spec.supports_ndjson
    339     {
    340         return Err(OperationAdapterError::InvalidInput {
    341             operation_id: spec.operation_id.to_owned(),
    342             message: format!("`{}` does not support --format ndjson", spec.cli_path),
    343         });
    344     }
    345     if request.context().dry_run && !spec.supports_dry_run {
    346         return Err(OperationAdapterError::InvalidInput {
    347             operation_id: spec.operation_id.to_owned(),
    348             message: format!("`{}` does not support --dry-run", spec.cli_path),
    349         });
    350     }
    351     Ok(())
    352 }
    353 
    354 fn validate_signer_mode_contract(
    355     request: &TargetOperationRequest,
    356     config: &RuntimeConfig,
    357 ) -> Result<(), OperationAdapterError> {
    358     let spec = request.spec();
    359     if matches!(config.signer.backend, SignerBackend::Myc)
    360         && requires_local_signer_mode_for_publish_transport(spec.operation_id, config)
    361     {
    362         return Err(OperationAdapterError::SignerModeDeferred {
    363             operation_id: spec.operation_id.to_owned(),
    364             message: format!(
    365                 "`{}` cannot run with signer mode `myc`; use signer mode `local`",
    366                 spec.cli_path
    367             ),
    368         });
    369     }
    370     Ok(())
    371 }
    372 
    373 fn validate_network_contract(
    374     request: &TargetOperationRequest,
    375     config: &RuntimeConfig,
    376 ) -> Result<(), OperationAdapterError> {
    377     let spec = request.spec();
    378     let requirement = network_requirement(spec.operation_id);
    379     match request.context().network_mode {
    380         OperationNetworkMode::Default => Ok(()),
    381         OperationNetworkMode::Offline => {
    382             if allows_offline_local_mutation(spec.operation_id) {
    383                 return Ok(());
    384             }
    385             if let NetworkRequirement::External {
    386                 dry_run_requires_network,
    387             } = requirement
    388                 && (!request.context().dry_run || dry_run_requires_network)
    389             {
    390                 return Err(OperationAdapterError::OfflineForbidden {
    391                     operation_id: spec.operation_id.to_owned(),
    392                     message: format!(
    393                         "`{}` requires relay, provider, or workflow network access",
    394                         spec.cli_path
    395                     ),
    396                 });
    397             }
    398             Ok(())
    399         }
    400         OperationNetworkMode::Online => {
    401             if let NetworkRequirement::External {
    402                 dry_run_requires_network,
    403             } = requirement
    404                 && (!request.context().dry_run || dry_run_requires_network)
    405                 && requires_pre_runtime_relay_target(spec.operation_id)
    406                 && config.relay.urls.is_empty()
    407             {
    408                 return Err(OperationAdapterError::NetworkUnavailable {
    409                     operation_id: spec.operation_id.to_owned(),
    410                     message: format!(
    411                         "`{}` requires at least one configured relay for online execution",
    412                         spec.cli_path
    413                     ),
    414                 });
    415             }
    416             Ok(())
    417         }
    418     }
    419 }
    420 
    421 fn requires_local_signer_mode_for_publish_transport(
    422     operation_id: &str,
    423     config: &RuntimeConfig,
    424 ) -> bool {
    425     let _ = config;
    426     requires_local_signer_mode(operation_id)
    427 }
    428 
    429 fn requires_pre_runtime_relay_target(operation_id: &str) -> bool {
    430     !is_publish_transport_routed_operation(operation_id)
    431 }
    432 
    433 fn allows_offline_local_mutation(operation_id: &str) -> bool {
    434     matches!(operation_id, "listing.publish")
    435 }
    436 
    437 fn validate_publish_transport_contract(
    438     request: &TargetOperationRequest,
    439     config: &RuntimeConfig,
    440 ) -> Result<(), OperationAdapterError> {
    441     let _ = request;
    442     let _ = config;
    443     Ok(())
    444 }
    445 
    446 fn is_publish_transport_routed_operation(operation_id: &str) -> bool {
    447     matches!(
    448         operation_id,
    449         "farm.publish" | "listing.publish" | "listing.update" | "listing.archive"
    450     )
    451 }
    452 
    453 fn failure_envelope(
    454     request: &TargetOperationRequest,
    455     error: OperationAdapterError,
    456 ) -> OutputEnvelope {
    457     OutputEnvelope::failure(
    458         request.operation_id(),
    459         error.to_output_error(),
    460         request
    461             .context()
    462             .envelope_context(next_request_id(request.operation_id())),
    463     )
    464 }
    465 
    466 fn next_request_id(operation_id: &str) -> String {
    467     let sequence = REQUEST_SEQUENCE.fetch_add(1, Ordering::Relaxed);
    468     let timestamp = SystemTime::now()
    469         .duration_since(UNIX_EPOCH)
    470         .map(|duration| duration.as_nanos())
    471         .unwrap_or_default();
    472     format!(
    473         "req_{}_{}_{}_{}",
    474         operation_id.replace('.', "_"),
    475         std::process::id(),
    476         timestamp,
    477         sequence
    478     )
    479 }
    480 
    481 fn render_envelope(
    482     envelope: &OutputEnvelope,
    483     format: TargetOutputFormat,
    484 ) -> Result<(), runtime::RuntimeError> {
    485     let stdout = std::io::stdout();
    486     let mut handle = stdout.lock();
    487     match format {
    488         TargetOutputFormat::Human => {
    489             render_human_envelope(&mut handle, envelope)?;
    490         }
    491         TargetOutputFormat::Json => {
    492             serde_json::to_writer_pretty(&mut handle, envelope)?;
    493         }
    494         TargetOutputFormat::Ndjson => {
    495             for frame in envelope.to_ndjson_frames() {
    496                 serde_json::to_writer(&mut handle, &frame)?;
    497                 writeln!(handle)?;
    498             }
    499             return Ok(());
    500         }
    501     }
    502     writeln!(handle)?;
    503     Ok(())
    504 }
    505 
    506 fn render_human_envelope(
    507     handle: &mut impl Write,
    508     envelope: &OutputEnvelope,
    509 ) -> Result<(), runtime::RuntimeError> {
    510     writeln!(
    511         handle,
    512         "{}: {}",
    513         envelope.operation_id,
    514         human_envelope_status(envelope)
    515     )?;
    516     writeln!(handle, "request_id: {}", envelope.request_id)?;
    517     if let Some(error) = envelope.errors.first() {
    518         writeln!(handle, "error: {}", error.code)?;
    519         writeln!(handle, "message: {}", error.message)?;
    520     }
    521     let display = human_display_source(envelope);
    522     if !envelope.errors.is_empty()
    523         && let Some(state) = human_state(display)
    524     {
    525         writeln!(handle, "state: {state}")?;
    526     }
    527     if let Some(mode) = human_publish_transport(display) {
    528         writeln!(handle, "publish_transport: {mode}")?;
    529     }
    530     if let Some(state) = human_publish_state(display) {
    531         writeln!(handle, "publish_state: {state}")?;
    532     }
    533     if let Some(state) = human_proof_state(display) {
    534         writeln!(handle, "proof_state: {state}")?;
    535     }
    536     if let Some(system) = human_proof_system(display) {
    537         writeln!(handle, "proof_system: {system}")?;
    538     }
    539     if let Some(verified) = human_cryptographic_proof_verified(display) {
    540         writeln!(handle, "cryptographic_proof_verified: {verified}")?;
    541     }
    542     if let Some(reason) = human_reason(display) {
    543         writeln!(handle, "reason: {reason}")?;
    544     }
    545     let actions = human_actions(envelope, display);
    546     if !actions.is_empty() {
    547         writeln!(handle, "next:")?;
    548         for action in actions {
    549             writeln!(handle, "- {action}")?;
    550         }
    551     }
    552     Ok(())
    553 }
    554 
    555 fn human_display_source(envelope: &OutputEnvelope) -> &Value {
    556     if !envelope.result.is_null() {
    557         return &envelope.result;
    558     }
    559     envelope
    560         .errors
    561         .first()
    562         .and_then(|error| error.detail.as_ref())
    563         .unwrap_or(&envelope.result)
    564 }
    565 
    566 fn human_state(result: &Value) -> Option<&str> {
    567     human_string_path(result, &["state"])
    568 }
    569 
    570 fn human_publish_transport(result: &Value) -> Option<&str> {
    571     human_string_path(result, &["publish", "mode"])
    572         .or_else(|| human_string_path(result, &["checks", "publish", "mode"]))
    573         .or_else(|| human_string_path(result, &["publish_transport"]))
    574 }
    575 
    576 fn human_publish_state(result: &Value) -> Option<&str> {
    577     human_string_path(result, &["publish", "state"])
    578         .or_else(|| human_string_path(result, &["checks", "publish", "state"]))
    579         .or_else(|| human_string_path(result, &["publish_state"]))
    580 }
    581 
    582 fn human_proof_state(result: &Value) -> Option<&str> {
    583     human_string_path(result, &["proof_verification", "state"])
    584         .or_else(|| human_string_path(result, &["proof_verification_state"]))
    585 }
    586 
    587 fn human_proof_system(result: &Value) -> Option<&str> {
    588     human_string_path(result, &["proof_verification", "proof_system"])
    589         .or_else(|| human_string_path(result, &["receipt", "proof", "system"]))
    590         .or_else(|| human_string_path(result, &["proof_system"]))
    591 }
    592 
    593 fn human_cryptographic_proof_verified(result: &Value) -> Option<bool> {
    594     human_bool_path(
    595         result,
    596         &["proof_verification", "cryptographic_proof_verified"],
    597     )
    598 }
    599 
    600 fn human_reason(result: &Value) -> Option<&str> {
    601     human_string_path(result, &["reason"])
    602         .or_else(|| human_string_path(result, &["publish", "reason"]))
    603         .or_else(|| human_string_path(result, &["checks", "publish", "reason"]))
    604         .or_else(|| human_string_path(result, &["store", "reason"]))
    605         .or_else(|| human_string_path(result, &["checks", "store", "reason"]))
    606         .or_else(|| human_string_path(result, &["checks", "account", "reason"]))
    607 }
    608 
    609 fn human_actions(envelope: &OutputEnvelope, display: &Value) -> Vec<String> {
    610     let mut actions = display
    611         .get("actions")
    612         .and_then(Value::as_array)
    613         .into_iter()
    614         .flatten()
    615         .filter_map(Value::as_str)
    616         .map(str::to_owned)
    617         .collect::<Vec<_>>();
    618     if actions.is_empty() {
    619         actions = envelope
    620             .next_actions
    621             .iter()
    622             .map(|action| {
    623                 action
    624                     .command
    625                     .clone()
    626                     .or_else(|| action.description.clone())
    627                     .unwrap_or_else(|| action.label.clone())
    628             })
    629             .collect();
    630     }
    631     actions.into_iter().fold(Vec::new(), |mut unique, action| {
    632         if !unique.contains(&action) {
    633             unique.push(action);
    634         }
    635         unique
    636     })
    637 }
    638 
    639 fn human_string_path<'a>(value: &'a Value, path: &[&str]) -> Option<&'a str> {
    640     let mut current = value;
    641     for segment in path {
    642         current = current.get(*segment)?;
    643     }
    644     current.as_str().filter(|value| !value.trim().is_empty())
    645 }
    646 
    647 fn human_bool_path(value: &Value, path: &[&str]) -> Option<bool> {
    648     let mut current = value;
    649     for segment in path {
    650         current = current.get(*segment)?;
    651     }
    652     current.as_bool()
    653 }
    654 
    655 fn human_envelope_status(envelope: &OutputEnvelope) -> &str {
    656     if !envelope.errors.is_empty() {
    657         return "error";
    658     }
    659     if let Some(state) = envelope
    660         .result
    661         .get("state")
    662         .and_then(|value| value.as_str())
    663     {
    664         return state;
    665     }
    666     if envelope.dry_run {
    667         return "dry_run";
    668     }
    669     "ok"
    670 }
    671 
    672 fn envelope_exit_code(envelope: &OutputEnvelope) -> ExitCode {
    673     envelope
    674         .errors
    675         .first()
    676         .map(|error| ExitCode::from(error.exit_code))
    677         .unwrap_or_else(|| ExitCode::from(0))
    678 }
    679 
    680 fn operation_config_error(error: OperationAdapterError) -> runtime::RuntimeError {
    681     runtime::RuntimeError::Config(error.to_string())
    682 }