cli

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

listing.rs (24670B)


      1 use std::path::PathBuf;
      2 
      3 use serde::Serialize;
      4 use serde_json::Value;
      5 
      6 use crate::cli::global::{
      7     ListingAppRecordExportArgs, ListingCreateArgs, ListingFileArgs, ListingMutationArgs,
      8     ListingRebindArgs, RecordLookupArgs,
      9 };
     10 use crate::ops::{
     11     ListingAppExportRequest, ListingAppExportResult, ListingAppListRequest, ListingAppListResult,
     12     ListingArchiveRequest, ListingArchiveResult, ListingCreateRequest, ListingCreateResult,
     13     ListingGetRequest, ListingGetResult, ListingListRequest, ListingListResult,
     14     ListingPublishRequest, ListingPublishResult, ListingRebindRequest, ListingRebindResult,
     15     ListingUpdateRequest, ListingUpdateResult, ListingValidateRequest, ListingValidateResult,
     16     OperationAdapterError, OperationNetworkMode, OperationRequest, OperationRequestData,
     17     OperationRequestPayload, OperationResult, OperationResultData, OperationService,
     18 };
     19 use crate::runtime::RuntimeError;
     20 use crate::runtime::config::RuntimeConfig;
     21 use crate::view::runtime::{CommandDisposition, ListingAppRecordExportView, ListingMutationView};
     22 
     23 pub struct ListingOperationService<'a> {
     24     config: &'a RuntimeConfig,
     25 }
     26 
     27 impl<'a> ListingOperationService<'a> {
     28     pub fn new(config: &'a RuntimeConfig) -> Self {
     29         Self { config }
     30     }
     31 }
     32 
     33 impl OperationService<ListingCreateRequest> for ListingOperationService<'_> {
     34     type Result = ListingCreateResult;
     35 
     36     fn execute(
     37         &self,
     38         request: OperationRequest<ListingCreateRequest>,
     39     ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
     40         let args = ListingCreateArgs {
     41             output: optional_path(&request, "output"),
     42             key: string_input(&request, "key"),
     43             title: string_input(&request, "title"),
     44             category: string_input(&request, "category"),
     45             summary: string_input(&request, "summary"),
     46             bin_id: string_input(&request, "bin_id"),
     47             quantity_amount: string_input(&request, "quantity_amount"),
     48             quantity_unit: string_input(&request, "quantity_unit"),
     49             price_amount: string_input(&request, "price_amount"),
     50             price_currency: string_input(&request, "price_currency"),
     51             price_per_amount: string_input(&request, "price_per_amount"),
     52             price_per_unit: string_input(&request, "price_per_unit"),
     53             available: string_input(&request, "available"),
     54             label: string_input(&request, "label"),
     55             discount_id: string_input(&request, "discount_id"),
     56             discount_label: string_input(&request, "discount_label"),
     57             discount_kind: string_input(&request, "discount_kind"),
     58             discount_value: string_input(&request, "discount_value"),
     59             discount_amount: string_input(&request, "discount_amount"),
     60             discount_currency: string_input(&request, "discount_currency"),
     61         };
     62         if request.context.dry_run {
     63             let view = map_runtime(
     64                 request.operation_id(),
     65                 crate::runtime::listing::scaffold_preflight(self.config, &args),
     66             )?;
     67             return serialized_operation_result::<ListingCreateResult, _>(&view);
     68         }
     69 
     70         let view = map_runtime(
     71             request.operation_id(),
     72             crate::runtime::listing::scaffold(self.config, &args),
     73         )?;
     74         serialized_operation_result::<ListingCreateResult, _>(&view)
     75     }
     76 }
     77 
     78 impl OperationService<ListingGetRequest> for ListingOperationService<'_> {
     79     type Result = ListingGetResult;
     80 
     81     fn execute(
     82         &self,
     83         request: OperationRequest<ListingGetRequest>,
     84     ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
     85         let args = RecordLookupArgs {
     86             key: required_string(&request, "key")?,
     87         };
     88         let view = map_runtime(
     89             request.operation_id(),
     90             crate::runtime::listing::get(self.config, &args),
     91         )?;
     92         serialized_operation_result::<ListingGetResult, _>(&view)
     93     }
     94 }
     95 
     96 impl OperationService<ListingListRequest> for ListingOperationService<'_> {
     97     type Result = ListingListResult;
     98 
     99     fn execute(
    100         &self,
    101         request: OperationRequest<ListingListRequest>,
    102     ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
    103         let view = map_runtime(
    104             request.operation_id(),
    105             crate::runtime::listing::list(self.config),
    106         )?;
    107         serialized_operation_result::<ListingListResult, _>(&view)
    108     }
    109 }
    110 
    111 impl OperationService<ListingAppListRequest> for ListingOperationService<'_> {
    112     type Result = ListingAppListResult;
    113 
    114     fn execute(
    115         &self,
    116         request: OperationRequest<ListingAppListRequest>,
    117     ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
    118         let view = map_runtime(
    119             request.operation_id(),
    120             crate::runtime::listing::app_record_list(self.config),
    121         )?;
    122         serialized_operation_result::<ListingAppListResult, _>(&view)
    123     }
    124 }
    125 
    126 impl OperationService<ListingAppExportRequest> for ListingOperationService<'_> {
    127     type Result = ListingAppExportResult;
    128 
    129     fn execute(
    130         &self,
    131         request: OperationRequest<ListingAppExportRequest>,
    132     ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
    133         let args = ListingAppRecordExportArgs {
    134             record_id: required_string(&request, "record_id")?,
    135             output: optional_path(&request, "output"),
    136         };
    137         let mut config = self.config.clone();
    138         if request.context.dry_run {
    139             config.output.dry_run = true;
    140         }
    141         let view = map_runtime(
    142             request.operation_id(),
    143             crate::runtime::listing::app_record_export(&config, &args),
    144         )?;
    145         listing_app_record_export_result::<ListingAppExportResult>(request.operation_id(), &view)
    146     }
    147 }
    148 
    149 impl OperationService<ListingUpdateRequest> for ListingOperationService<'_> {
    150     type Result = ListingUpdateResult;
    151 
    152     fn execute(
    153         &self,
    154         request: OperationRequest<ListingUpdateRequest>,
    155     ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
    156         if !request.context.dry_run {
    157             require_approval(&request)?;
    158         }
    159         let args = mutation_args(&request)?;
    160         let config = mutation_config(self.config, &request);
    161         let view = crate::runtime::listing::update(&config, &args).map_err(|error| {
    162             OperationAdapterError::sdk_adapter_failure(request.operation_id(), error)
    163         })?;
    164         mutation_result::<ListingUpdateResult>(request.operation_id(), &view)
    165     }
    166 }
    167 
    168 impl OperationService<ListingValidateRequest> for ListingOperationService<'_> {
    169     type Result = ListingValidateResult;
    170 
    171     fn execute(
    172         &self,
    173         request: OperationRequest<ListingValidateRequest>,
    174     ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
    175         let args = ListingFileArgs {
    176             file: required_path(&request, "file")?,
    177         };
    178         let view = map_runtime(
    179             request.operation_id(),
    180             crate::runtime::listing::validate(self.config, &args),
    181         )?;
    182         serialized_operation_result::<ListingValidateResult, _>(&view)
    183     }
    184 }
    185 
    186 impl OperationService<ListingRebindRequest> for ListingOperationService<'_> {
    187     type Result = ListingRebindResult;
    188 
    189     fn execute(
    190         &self,
    191         request: OperationRequest<ListingRebindRequest>,
    192     ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
    193         let args = ListingRebindArgs {
    194             file: required_path(&request, "file")?,
    195             selector: required_string(&request, "selector")?,
    196             farm_d_tag: string_input(&request, "farm_d_tag"),
    197         };
    198         if request.context.dry_run {
    199             let view = map_runtime(
    200                 request.operation_id(),
    201                 crate::runtime::listing::rebind_preflight(self.config, &args),
    202             )?;
    203             return serialized_operation_result::<ListingRebindResult, _>(&view);
    204         }
    205         require_approval(&request)?;
    206         let view = map_runtime(
    207             request.operation_id(),
    208             crate::runtime::listing::rebind(self.config, &args),
    209         )?;
    210         serialized_operation_result::<ListingRebindResult, _>(&view)
    211     }
    212 }
    213 
    214 impl OperationService<ListingPublishRequest> for ListingOperationService<'_> {
    215     type Result = ListingPublishResult;
    216 
    217     fn execute(
    218         &self,
    219         request: OperationRequest<ListingPublishRequest>,
    220     ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
    221         if !request.context.dry_run {
    222             require_approval(&request)?;
    223         }
    224         let args = mutation_args(&request)?;
    225         let config = mutation_config(self.config, &request);
    226         let view = crate::runtime::listing::publish_via_sdk(&config, &args).map_err(|error| {
    227             OperationAdapterError::sdk_adapter_failure(request.operation_id(), error)
    228         })?;
    229         mutation_result::<ListingPublishResult>(request.operation_id(), &view)
    230     }
    231 }
    232 
    233 impl OperationService<ListingArchiveRequest> for ListingOperationService<'_> {
    234     type Result = ListingArchiveResult;
    235 
    236     fn execute(
    237         &self,
    238         request: OperationRequest<ListingArchiveRequest>,
    239     ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
    240         if !request.context.dry_run {
    241             require_approval(&request)?;
    242         }
    243         let args = mutation_args(&request)?;
    244         let config = mutation_config(self.config, &request);
    245         let view = crate::runtime::listing::archive(&config, &args).map_err(|error| {
    246             OperationAdapterError::sdk_adapter_failure(request.operation_id(), error)
    247         })?;
    248         mutation_result::<ListingArchiveResult>(request.operation_id(), &view)
    249     }
    250 }
    251 
    252 fn mutation_config<P>(config: &RuntimeConfig, request: &OperationRequest<P>) -> RuntimeConfig
    253 where
    254     P: OperationRequestPayload,
    255 {
    256     let mut config = config.clone();
    257     if request.context.dry_run {
    258         config.output.dry_run = true;
    259     }
    260     config
    261 }
    262 
    263 fn mutation_args<P>(
    264     request: &OperationRequest<P>,
    265 ) -> Result<ListingMutationArgs, OperationAdapterError>
    266 where
    267     P: OperationRequestPayload + OperationRequestData,
    268 {
    269     Ok(ListingMutationArgs {
    270         file: required_path(request, "file")?,
    271         idempotency_key: request
    272             .context
    273             .idempotency_key
    274             .clone()
    275             .or_else(|| string_input(request, "idempotency_key")),
    276         print_event: bool_input(request, "print_event").unwrap_or(false),
    277         offline: matches!(request.context.network_mode, OperationNetworkMode::Offline),
    278     })
    279 }
    280 
    281 fn require_approval<P>(request: &OperationRequest<P>) -> Result<(), OperationAdapterError>
    282 where
    283     P: OperationRequestPayload + OperationRequestData,
    284 {
    285     if request.context.requires_approval_token() {
    286         return Err(OperationAdapterError::approval_required(
    287             request.operation_id(),
    288         ));
    289     }
    290     Ok(())
    291 }
    292 
    293 fn serialized_operation_result<R, T>(value: &T) -> Result<OperationResult<R>, OperationAdapterError>
    294 where
    295     R: OperationResultData,
    296     T: Serialize,
    297 {
    298     OperationResult::new(R::from_serializable(value)?)
    299 }
    300 
    301 fn mutation_result<R>(
    302     operation_id: &str,
    303     view: &ListingMutationView,
    304 ) -> Result<OperationResult<R>, OperationAdapterError>
    305 where
    306     R: OperationResultData,
    307 {
    308     match view.disposition() {
    309         CommandDisposition::Success => serialized_operation_result::<R, _>(view),
    310         CommandDisposition::ExternalUnavailable if listing_relay_unavailable(view) => {
    311             Err(OperationAdapterError::network_unavailable_with_detail(
    312                 operation_id,
    313                 view.reason.clone().unwrap_or_else(|| {
    314                     format!(
    315                         "listing {} finished with state `{}`",
    316                         view.operation, view.state
    317                     )
    318                 }),
    319                 serde_json::to_value(view).unwrap_or(Value::Null),
    320             ))
    321         }
    322         disposition => Err(OperationAdapterError::from_command_disposition(
    323             operation_id,
    324             disposition,
    325             view.reason.clone().unwrap_or_else(|| {
    326                 format!(
    327                     "listing {} finished with state `{}`",
    328                     view.operation, view.state
    329                 )
    330             }),
    331         )),
    332     }
    333 }
    334 
    335 fn listing_relay_unavailable(view: &ListingMutationView) -> bool {
    336     matches!(
    337         view.source.as_str(),
    338         "direct Nostr relay publish · local key" | "SDK listing publish · configured signer"
    339     ) && (view.reason.as_deref().is_some_and(|reason| {
    340         reason.contains("configured relay")
    341             || reason.contains("direct relay connection failed")
    342             || reason.contains("SDK relay publish")
    343     }) || !view.target_relays.is_empty()
    344         || !view.connected_relays.is_empty()
    345         || !view.failed_relays.is_empty())
    346 }
    347 
    348 fn listing_app_record_export_result<R>(
    349     operation_id: &str,
    350     view: &ListingAppRecordExportView,
    351 ) -> Result<OperationResult<R>, OperationAdapterError>
    352 where
    353     R: OperationResultData,
    354 {
    355     match view.disposition() {
    356         CommandDisposition::Success => serialized_operation_result::<R, _>(view),
    357         CommandDisposition::NotFound => Err(OperationAdapterError::not_found_with_detail(
    358             operation_id,
    359             view.reason.clone().unwrap_or_else(|| {
    360                 format!(
    361                     "app-authored local record `{}` was not found",
    362                     view.record_id
    363                 )
    364             }),
    365             serde_json::to_value(view).unwrap_or(Value::Null),
    366         )),
    367         CommandDisposition::ValidationFailed => {
    368             Err(OperationAdapterError::validation_failed_with_detail(
    369                 operation_id,
    370                 view.reason.clone().unwrap_or_else(|| {
    371                     format!(
    372                         "app-authored local record `{}` cannot be exported",
    373                         view.record_id
    374                     )
    375                 }),
    376                 serde_json::to_value(view).unwrap_or(Value::Null),
    377             ))
    378         }
    379         disposition => Err(OperationAdapterError::from_command_disposition(
    380             operation_id,
    381             disposition,
    382             view.reason.clone().unwrap_or_else(|| {
    383                 format!(
    384                     "app-authored local record export finished with state `{}`",
    385                     view.state
    386                 )
    387             }),
    388         )),
    389     }
    390 }
    391 
    392 fn map_runtime<T>(
    393     operation_id: &str,
    394     result: Result<T, RuntimeError>,
    395 ) -> Result<T, OperationAdapterError> {
    396     result.map_err(|error| OperationAdapterError::runtime_failure(operation_id, error))
    397 }
    398 
    399 fn required_string<P>(
    400     request: &OperationRequest<P>,
    401     key: &str,
    402 ) -> Result<String, OperationAdapterError>
    403 where
    404     P: OperationRequestPayload + OperationRequestData,
    405 {
    406     string_input(request, key).ok_or_else(|| {
    407         invalid_input(
    408             request.operation_id(),
    409             format!("missing required `{key}` input"),
    410         )
    411     })
    412 }
    413 
    414 fn required_path<P>(
    415     request: &OperationRequest<P>,
    416     key: &str,
    417 ) -> Result<PathBuf, OperationAdapterError>
    418 where
    419     P: OperationRequestPayload + OperationRequestData,
    420 {
    421     optional_path(request, key).ok_or_else(|| {
    422         invalid_input(
    423             request.operation_id(),
    424             format!("missing required `{key}` input"),
    425         )
    426     })
    427 }
    428 
    429 fn optional_path<P>(request: &OperationRequest<P>, key: &str) -> Option<PathBuf>
    430 where
    431     P: OperationRequestPayload + OperationRequestData,
    432 {
    433     string_input(request, key).map(PathBuf::from)
    434 }
    435 
    436 fn string_input<P>(request: &OperationRequest<P>, key: &str) -> Option<String>
    437 where
    438     P: OperationRequestPayload + OperationRequestData,
    439 {
    440     request
    441         .payload
    442         .input()
    443         .get(key)
    444         .and_then(Value::as_str)
    445         .map(str::to_owned)
    446 }
    447 
    448 fn bool_input<P>(request: &OperationRequest<P>, key: &str) -> Option<bool>
    449 where
    450     P: OperationRequestPayload + OperationRequestData,
    451 {
    452     request.payload.input().get(key).and_then(Value::as_bool)
    453 }
    454 
    455 fn invalid_input(operation_id: &str, message: String) -> OperationAdapterError {
    456     OperationAdapterError::InvalidInput {
    457         operation_id: operation_id.to_owned(),
    458         message,
    459     }
    460 }
    461 
    462 #[cfg(test)]
    463 mod tests {
    464     use std::path::{Path, PathBuf};
    465 
    466     use radroots_runtime_paths::RadrootsMigrationReport;
    467     use radroots_secret_vault::RadrootsSecretBackend;
    468     use serde_json::{Map, Value};
    469     use tempfile::tempdir;
    470 
    471     use super::ListingOperationService;
    472     use crate::ops::{
    473         ListingArchiveRequest, ListingCreateRequest, ListingListRequest, ListingPublishRequest,
    474         OperationAdapter, OperationContext, OperationData, OperationRequest,
    475     };
    476     use crate::runtime::config::{
    477         AccountConfig, AccountSecretContractConfig, HyfConfig, IdentityConfig, InteractionConfig,
    478         LocalConfig, LoggingConfig, MigrationConfig, MycConfig, OutputConfig, OutputFormat,
    479         PathsConfig, PublishConfig, PublishTransport, PublishTransportSource, RelayConfig,
    480         RelayConfigSource, RelayPublishPolicy, RpcConfig, RuntimeConfig, SignerBackend,
    481         SignerConfig, Verbosity,
    482     };
    483 
    484     #[test]
    485     fn listing_service_requires_seller_actor_for_create_dry_run() {
    486         let dir = tempdir().expect("tempdir");
    487         let config = sample_config(dir.path());
    488         let service = OperationAdapter::new(ListingOperationService::new(&config));
    489         let mut context = OperationContext::default();
    490         context.dry_run = true;
    491         let request = OperationRequest::new(
    492             context.clone(),
    493             ListingCreateRequest::from_data(data(&[("key", "eggs"), ("title", "Eggs")])),
    494         )
    495         .expect("listing create request");
    496         let error = service
    497             .execute(request)
    498             .expect_err("listing create seller actor");
    499         let output_error = error.to_output_error();
    500 
    501         assert_eq!(output_error.code, "account_unresolved");
    502         assert!(output_error.detail.expect("detail")["seller_actor_source"] == "resolved_account");
    503     }
    504 
    505     #[test]
    506     fn listing_service_exposes_listing_list_operation() {
    507         let dir = tempdir().expect("tempdir");
    508         let config = sample_config(dir.path());
    509         let service = OperationAdapter::new(ListingOperationService::new(&config));
    510         let request =
    511             OperationRequest::new(OperationContext::default(), ListingListRequest::default())
    512                 .expect("listing list request");
    513         let envelope = service
    514             .execute(request)
    515             .expect("listing list result")
    516             .to_envelope(OperationContext::default().envelope_context("req_listing_list"))
    517             .expect("listing list envelope");
    518 
    519         assert_eq!(envelope.operation_id, "listing.list");
    520         assert_eq!(envelope.result["state"], "empty");
    521         assert_eq!(envelope.result["count"], 0);
    522     }
    523 
    524     #[test]
    525     fn listing_publish_and_archive_require_approval_unless_dry_run() {
    526         let dir = tempdir().expect("tempdir");
    527         let config = sample_config(dir.path());
    528         let service = OperationAdapter::new(ListingOperationService::new(&config));
    529         let publish = OperationRequest::new(
    530             OperationContext::default(),
    531             ListingPublishRequest::from_data(data(&[("file", "listing.toml")])),
    532         )
    533         .expect("listing publish request");
    534         let publish_error = service.execute(publish).expect_err("approval required");
    535         assert!(format!("{publish_error}").contains("approval_token"));
    536         assert_eq!(publish_error.to_output_error().code, "approval_required");
    537         assert_eq!(publish_error.to_output_error().exit_code, 6);
    538 
    539         let mut context = OperationContext::default();
    540         context.dry_run = true;
    541         let archive = OperationRequest::new(
    542             context.clone(),
    543             ListingArchiveRequest::from_data(data(&[("file", "listing.toml")])),
    544         )
    545         .expect("listing archive request");
    546         let archive_error = service.execute(archive).expect_err("archive preflight");
    547         assert!(!format!("{archive_error}").contains("approval_token"));
    548     }
    549 
    550     fn sample_config(root: &Path) -> RuntimeConfig {
    551         let data = root.join("data");
    552         let logs = root.join("logs");
    553         let secrets = root.join("secrets");
    554         RuntimeConfig {
    555             output: OutputConfig {
    556                 format: OutputFormat::Human,
    557                 verbosity: Verbosity::Normal,
    558                 color: true,
    559                 dry_run: false,
    560             },
    561             interaction: InteractionConfig {
    562                 input_enabled: true,
    563                 assume_yes: false,
    564                 stdin_tty: false,
    565                 stdout_tty: false,
    566                 prompts_allowed: false,
    567                 confirmations_allowed: false,
    568             },
    569             paths: PathsConfig {
    570                 profile: "interactive_user".into(),
    571                 profile_source: "test".into(),
    572                 allowed_profiles: vec!["interactive_user".into(), "repo_local".into()],
    573                 root_source: "test".into(),
    574                 repo_local_root: None,
    575                 repo_local_root_source: None,
    576                 subordinate_path_override_source: "runtime_config".into(),
    577                 app_namespace: "apps/cli".into(),
    578                 shared_accounts_namespace: "shared/accounts".into(),
    579                 shared_identities_namespace: "shared/identities".into(),
    580                 app_config_path: root.join("config/apps/cli/config.toml"),
    581                 workspace_config_path: None,
    582                 app_data_root: data.join("apps/cli"),
    583                 app_logs_root: logs.join("apps/cli"),
    584                 shared_accounts_data_root: data.join("shared/accounts"),
    585                 shared_accounts_secrets_root: secrets.join("shared/accounts"),
    586                 default_identity_path: secrets.join("shared/identities/default.json"),
    587             },
    588             migration: MigrationConfig {
    589                 report: RadrootsMigrationReport::empty(),
    590             },
    591             logging: LoggingConfig {
    592                 filter: "info".into(),
    593                 directory: None,
    594                 stdout: false,
    595             },
    596             account: AccountConfig {
    597                 selector: None,
    598                 store_path: data.join("shared/accounts/store.json"),
    599                 secrets_dir: secrets.join("shared/accounts"),
    600                 secret_backend: RadrootsSecretBackend::EncryptedFile,
    601                 secret_fallback: None,
    602             },
    603             account_secret_contract: AccountSecretContractConfig {
    604                 default_backend: "host_vault".into(),
    605                 default_fallback: Some("encrypted_file".into()),
    606                 allowed_backends: vec!["host_vault".into(), "encrypted_file".into()],
    607                 host_vault_policy: Some("desktop".into()),
    608                 uses_protected_store: true,
    609             },
    610             identity: IdentityConfig {
    611                 path: secrets.join("shared/identities/default.json"),
    612             },
    613             signer: SignerConfig {
    614                 backend: SignerBackend::Local,
    615             },
    616             publish: PublishConfig {
    617                 transport: PublishTransport::DirectNostrRelay,
    618                 source: PublishTransportSource::Defaults,
    619                 radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
    620             },
    621             relay: RelayConfig {
    622                 urls: Vec::new(),
    623                 publish_policy: RelayPublishPolicy::Any,
    624                 source: RelayConfigSource::Defaults,
    625             },
    626             local: LocalConfig {
    627                 root: data.join("apps/cli/replica"),
    628                 replica_db_path: data.join("apps/cli/replica/replica.sqlite"),
    629                 backups_dir: data.join("apps/cli/replica/backups"),
    630                 exports_dir: data.join("apps/cli/replica/exports"),
    631             },
    632             myc: MycConfig {
    633                 executable: PathBuf::from("myc"),
    634                 status_timeout_ms: 2_000,
    635             },
    636             hyf: HyfConfig {
    637                 enabled: false,
    638                 executable: PathBuf::from("hyfd"),
    639             },
    640             rpc: RpcConfig {
    641                 url: "http://127.0.0.1:7070".into(),
    642             },
    643             rhi: crate::runtime::config::RhiConfig {
    644                 trusted_worker_pubkeys: Vec::new(),
    645             },
    646             capability_bindings: Vec::new(),
    647         }
    648     }
    649 
    650     fn data(entries: &[(&str, &str)]) -> OperationData {
    651         entries
    652             .iter()
    653             .map(|(key, value)| ((*key).to_owned(), Value::String((*value).to_owned())))
    654             .collect::<Map<String, Value>>()
    655     }
    656 }