cli

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

commit 407db3816d0c961e63c724a78aba98fd5cba39a3
parent 4cb5dba0d772cb3500d60c18e6b37077ea77cd32
Author: triesap <tyson@radroots.org>
Date:   Mon, 27 Apr 2026 08:03:06 +0000

cli: remove legacy command model

Diffstat:
Dsrc/cli.rs | 1126-------------------------------------------------------------------------------
Dsrc/commands/doctor.rs | 414-------------------------------------------------------------------------------
Dsrc/commands/farm.rs | 123-------------------------------------------------------------------------------
Dsrc/commands/find.rs | 17-----------------
Dsrc/commands/identity.rs | 202-------------------------------------------------------------------------------
Dsrc/commands/job.rs | 16----------------
Dsrc/commands/listing.rs | 127-------------------------------------------------------------------------------
Dsrc/commands/local.rs | 73-------------------------------------------------------------------------
Dsrc/commands/market.rs | 163-------------------------------------------------------------------------------
Dsrc/commands/mod.rs | 185-------------------------------------------------------------------------------
Dsrc/commands/myc.rs | 19-------------------
Dsrc/commands/net.rs | 21---------------------
Dsrc/commands/order.rs | 148-------------------------------------------------------------------------------
Dsrc/commands/relay.rs | 20--------------------
Dsrc/commands/rpc.rs | 10----------
Dsrc/commands/runtime.rs | 388-------------------------------------------------------------------------------
Dsrc/commands/sell.rs | 150-------------------------------------------------------------------------------
Dsrc/commands/signer.rs | 163-------------------------------------------------------------------------------
Dsrc/commands/sync.rs | 46----------------------------------------------
Dsrc/commands/workflow.rs | 44--------------------------------------------
Msrc/domain/runtime.rs | 114-------------------------------------------------------------------------------
Msrc/main.rs | 21+++++++++------------
Msrc/operation_basket.rs | 6++----
Msrc/operation_core.rs | 4+---
Msrc/operation_farm.rs | 12+++++-------
Msrc/operation_listing.rs | 10+++++-----
Msrc/operation_market.rs | 8+++-----
Msrc/operation_order.rs | 14++------------
Msrc/operation_runtime.rs | 4+---
Dsrc/render/mod.rs | 4876-------------------------------------------------------------------------------
Msrc/runtime/config.rs | 221+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Msrc/runtime/daemon.rs | 538+------------------------------------------------------------------------------
Msrc/runtime/farm.rs | 149++++---------------------------------------------------------------------------
Msrc/runtime/find.rs | 4++--
Dsrc/runtime/job.rs | 278-------------------------------------------------------------------------------
Msrc/runtime/listing.rs | 554++-----------------------------------------------------------------------------
Msrc/runtime/local.rs | 2+-
Msrc/runtime/mod.rs | 4----
Msrc/runtime/network.rs | 31+------------------------------
Msrc/runtime/order.rs | 15++++++++++-----
Msrc/runtime/signer.rs | 21---------------------
Msrc/runtime/sync.rs | 2+-
Dsrc/runtime/workflow.rs | 300-------------------------------------------------------------------------------
Asrc/runtime_args.rs | 192+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
44 files changed, 374 insertions(+), 10461 deletions(-)

diff --git a/src/cli.rs b/src/cli.rs @@ -1,1126 +0,0 @@ -#![allow(dead_code)] - -use clap::{ - ArgAction, Args, CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum, - error::ErrorKind, -}; -use std::ffi::{OsStr, OsString}; -use std::path::PathBuf; - -use crate::runtime::config::OutputFormat; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] -pub enum OutputFormatArg { - Human, - Json, - Ndjson, -} - -impl OutputFormatArg { - pub fn as_output_format(self) -> OutputFormat { - match self { - Self::Human => OutputFormat::Human, - Self::Json => OutputFormat::Json, - Self::Ndjson => OutputFormat::Ndjson, - } - } -} - -#[derive(Debug, Parser, Clone)] -#[command(name = "radroots")] -#[command(version)] -pub struct CliArgs { - #[arg(skip)] - pub output_format: Option<OutputFormatArg>, - #[arg(long, global = true, action = ArgAction::SetTrue)] - pub json: bool, - #[arg(long, global = true, action = ArgAction::SetTrue)] - pub ndjson: bool, - #[arg(long = "env-file", global = true)] - pub env_file: Option<PathBuf>, - #[arg(long, global = true, action = ArgAction::SetTrue)] - pub quiet: bool, - #[arg(long, global = true, action = ArgAction::SetTrue)] - pub verbose: bool, - #[arg(long, global = true, action = ArgAction::SetTrue)] - pub trace: bool, - #[arg(long = "dry-run", global = true, action = ArgAction::SetTrue)] - pub dry_run: bool, - #[arg(long = "no-color", global = true, action = ArgAction::SetTrue)] - pub no_color: bool, - #[arg( - long = "no-input", - global = true, - visible_alias = "non-interactive", - action = ArgAction::SetTrue - )] - pub no_input: bool, - #[arg(long, global = true, action = ArgAction::SetTrue)] - pub yes: bool, - #[arg(long, global = true)] - pub log_filter: Option<String>, - #[arg(long, global = true)] - pub log_dir: Option<PathBuf>, - #[arg(long = "log-stdout", global = true, action = ArgAction::SetTrue)] - pub log_stdout: bool, - #[arg(long = "no-log-stdout", global = true, action = ArgAction::SetTrue)] - pub no_log_stdout: bool, - #[arg(long, global = true)] - pub account: Option<String>, - #[arg(long, global = true)] - pub identity_path: Option<PathBuf>, - #[arg(long, global = true)] - pub signer: Option<String>, - #[arg(long, global = true)] - pub relay: Vec<String>, - #[arg(long, global = true)] - pub myc_executable: Option<PathBuf>, - #[arg(long = "myc-status-timeout-ms", global = true)] - pub myc_status_timeout_ms: Option<u64>, - #[arg(long = "hyf-enabled", global = true, action = ArgAction::SetTrue)] - pub hyf_enabled: bool, - #[arg(long = "no-hyf-enabled", global = true, action = ArgAction::SetTrue)] - pub no_hyf_enabled: bool, - #[arg(long = "hyf-executable", global = true)] - pub hyf_executable: Option<PathBuf>, - #[command(subcommand)] - pub command: Command, -} - -impl CliArgs { - pub fn parse() -> Self { - Self::try_parse().unwrap_or_else(|error| error.exit()) - } - - pub fn try_parse() -> Result<Self, clap::Error> { - Self::try_parse_from(std::env::args_os()) - } - - #[cfg(test)] - pub fn parse_from<I, T>(itr: I) -> Self - where - I: IntoIterator<Item = T>, - T: Into<OsString> + Clone, - { - Self::try_parse_from(itr).unwrap_or_else(|error| error.exit()) - } - - pub fn try_parse_from<I, T>(itr: I) -> Result<Self, clap::Error> - where - I: IntoIterator<Item = T>, - T: Into<OsString> + Clone, - { - let args = itr.into_iter().map(Into::into).collect::<Vec<_>>(); - let (filtered_args, output_format) = extract_global_output_format(args)?; - let mut command = Self::build_command(); - let matches = command.try_get_matches_from_mut(filtered_args)?; - let mut parsed = <Self as FromArgMatches>::from_arg_matches(&matches)?; - parsed.output_format = output_format; - Ok(parsed) - } - - pub fn build_command() -> clap::Command { - <Self as CommandFactory>::command() - } - - fn command_error(message: impl Into<String>, kind: ErrorKind) -> clap::Error { - let mut command = Self::build_command(); - command.error(kind, message.into()) - } -} - -fn extract_global_output_format( - args: Vec<OsString>, -) -> Result<(Vec<OsString>, Option<OutputFormatArg>), clap::Error> { - let mut iter = args.into_iter(); - let Some(program) = iter.next() else { - return Ok((Vec::new(), None)); - }; - - let mut filtered_args = vec![program]; - let mut output_format = None; - let mut command_tokens = Vec::new(); - let mut skip_known_global_value = false; - - while let Some(arg) = iter.next() { - if skip_known_global_value { - filtered_args.push(arg); - skip_known_global_value = false; - continue; - } - - if let Some((flag, value)) = split_long_option(arg.as_os_str()) { - if flag == "output" && !matches_local_output_context(command_tokens.as_slice()) { - output_format = Some(parse_output_format_value(value)?); - continue; - } - - if matches_known_global_value_option(flag) { - filtered_args.push(arg); - continue; - } - } - - if arg == OsStr::new("--output") { - if matches_local_output_context(command_tokens.as_slice()) { - filtered_args.push(arg); - continue; - } - - let Some(value) = iter.next() else { - return Err(CliArgs::command_error( - "`--output` requires a value", - ErrorKind::InvalidValue, - )); - }; - output_format = Some(parse_output_format_value(value.as_os_str())?); - continue; - } - - if let Some(flag) = long_option_name(arg.as_os_str()) { - if matches_known_global_value_option(flag) { - skip_known_global_value = true; - } - } - - if let Some(token) = arg.to_str() { - if !token.starts_with('-') { - command_tokens.push(token.to_owned()); - } - } - - filtered_args.push(arg); - } - - Ok((filtered_args, output_format)) -} - -fn parse_output_format_value(value: &OsStr) -> Result<OutputFormatArg, clap::Error> { - let Some(value) = value.to_str() else { - return Err(CliArgs::command_error( - "`--output` must be one of: human, json, ndjson", - ErrorKind::InvalidUtf8, - )); - }; - - OutputFormatArg::from_str(value, false).map_err(|_| { - CliArgs::command_error( - format!("invalid value `{value}` for `--output`; expected one of: human, json, ndjson"), - ErrorKind::InvalidValue, - ) - }) -} - -fn long_option_name(arg: &OsStr) -> Option<&str> { - let token = arg.to_str()?; - token - .strip_prefix("--") - .map(|rest| rest.split_once('=').map_or(rest, |(flag, _value)| flag)) -} - -fn split_long_option(arg: &OsStr) -> Option<(&str, &OsStr)> { - let token = arg.to_str()?; - let (flag, value) = token.strip_prefix("--")?.split_once('=')?; - Some((flag, OsStr::new(value))) -} - -fn matches_known_global_value_option(flag: &str) -> bool { - matches!( - flag, - "env-file" - | "log-filter" - | "log-dir" - | "account" - | "identity-path" - | "signer" - | "relay" - | "myc-executable" - | "hyf-executable" - ) -} - -fn matches_local_output_context(command_tokens: &[String]) -> bool { - matches!( - command_tokens, - [local, export, ..] if local == "local" && export == "export" - ) || matches!( - command_tokens, - [local, backup, ..] if local == "local" && backup == "backup" - ) || matches!( - command_tokens, - [listing, new, ..] if listing == "listing" && new == "new" - ) || matches!( - command_tokens, - [sell, add, ..] if sell == "sell" && add == "add" - ) -} - -#[derive(Debug, Clone, Subcommand)] -pub enum Command { - #[command(about = "Create, import, and manage local accounts")] - Account(AccountArgs), - #[command(about = "Show effective configuration")] - Config(ConfigArgs), - #[command(about = "Check readiness and suggest next steps")] - Doctor, - #[command(about = "Set up and publish your farm")] - Farm(FarmArgs), - #[command(about = "Advanced search command")] - Find(FindArgs), - #[command(about = "Inspect background jobs")] - Job(JobArgs), - #[command(about = "Advanced listing commands")] - Listing(ListingArgs), - #[command(about = "Manage local market data storage")] - Local(LocalArgs), - #[command(about = "Update local market data and search listings")] - Market(MarketArgs), - #[command(about = "Show myc status")] - Myc(MycArgs), - #[command(about = "Show network posture")] - Net(NetArgs), - #[command(about = "Create and manage order requests")] - Order(OrderArgs), - #[command(about = "Show relay configuration")] - Relay(RelayArgs), - #[command(about = "Show runtime bridge status")] - Rpc(RpcArgs), - #[command(about = "Create, check, and publish listings")] - Sell(SellArgs), - #[command(about = "Guided first-time setup for sellers and buyers")] - Setup(SetupArgs), - Runtime(RuntimeArgs), - #[command(about = "Show signer readiness")] - Signer(SignerArgs), - #[command(about = "Show what is ready and what needs attention")] - Status, - #[command(about = "Inspect sync status and watch updates")] - Sync(SyncArgs), -} - -impl Command { - pub fn display_name(&self) -> &'static str { - match self { - Self::Account(account) => match account.command { - AccountCommand::New => "account create", - AccountCommand::Import(_) => "account import", - AccountCommand::Whoami => "account view", - AccountCommand::Ls => "account list", - AccountCommand::Use(_) => "account select", - AccountCommand::ClearDefault => "account clear-default", - AccountCommand::Remove(_) => "account remove", - }, - Self::Config(config) => match config.command { - ConfigCommand::Show => "config show", - }, - Self::Doctor => "doctor", - Self::Farm(farm) => match farm.command { - FarmCommand::Init(_) => "farm init", - FarmCommand::Set(_) => "farm set", - FarmCommand::Publish(_) => "farm publish", - FarmCommand::Setup(_) => "farm setup", - FarmCommand::Status(_) => "farm check", - FarmCommand::Get(_) => "farm show", - }, - Self::Find(_) => "find", - Self::Job(job) => match job.command { - JobCommand::Ls => "job list", - JobCommand::Get(_) => "job get", - JobCommand::Watch(_) => "job watch", - }, - Self::Listing(listing) => match listing.command { - ListingCommand::New(_) => "listing new", - ListingCommand::Validate(_) => "listing validate", - ListingCommand::Get(_) => "listing get", - ListingCommand::Publish(_) => "listing publish", - ListingCommand::Update(_) => "listing update", - ListingCommand::Archive(_) => "listing archive", - }, - Self::Local(local) => match local.command { - LocalCommand::Init => "local init", - LocalCommand::Status => "local status", - LocalCommand::Export(_) => "local export", - LocalCommand::Backup(_) => "local backup", - }, - Self::Market(market) => match market.command { - MarketCommand::Update => "market update", - MarketCommand::Search(_) => "market search", - MarketCommand::View(_) => "market view", - }, - Self::Myc(myc) => match myc.command { - MycCommand::Status => "myc status", - }, - Self::Net(net) => match net.command { - NetCommand::Status => "net status", - }, - Self::Order(order) => match order.command { - OrderCommand::New(_) => "order create", - OrderCommand::Get(_) => "order view", - OrderCommand::Ls => "order list", - OrderCommand::Submit(_) => "order submit", - OrderCommand::Watch(_) => "order watch", - OrderCommand::Cancel(_) => "order cancel", - OrderCommand::History => "order history", - }, - Self::Relay(relay) => match relay.command { - RelayCommand::Ls => "relay list", - }, - Self::Rpc(rpc) => match rpc.command { - RpcCommand::Status => "rpc status", - RpcCommand::Sessions => "rpc sessions", - }, - Self::Sell(sell) => match sell.command { - SellCommand::Add(_) => "sell add", - SellCommand::Show(_) => "sell show", - SellCommand::Check(_) => "sell check", - SellCommand::Publish(_) => "sell publish", - SellCommand::Update(_) => "sell update", - SellCommand::Pause(_) => "sell pause", - SellCommand::Reprice(_) => "sell reprice", - SellCommand::Restock(_) => "sell restock", - }, - Self::Setup(args) => match args.role { - SetupRoleArg::Seller => "setup seller", - SetupRoleArg::Buyer => "setup buyer", - SetupRoleArg::Both => "setup both", - }, - Self::Runtime(runtime) => match &runtime.command { - RuntimeCommand::Install(_) => "runtime install", - RuntimeCommand::Uninstall(_) => "runtime uninstall", - RuntimeCommand::Status(_) => "runtime status", - RuntimeCommand::Start(_) => "runtime start", - RuntimeCommand::Stop(_) => "runtime stop", - RuntimeCommand::Restart(_) => "runtime restart", - RuntimeCommand::Logs(_) => "runtime logs", - RuntimeCommand::Config(runtime_config) => match &runtime_config.command { - RuntimeConfigCommand::Show(_) => "runtime config show", - RuntimeConfigCommand::Set(_) => "runtime config set", - }, - }, - Self::Signer(signer) => match signer.command { - SignerCommand::Status => "signer status", - SignerCommand::Session(_) => "signer session", - }, - Self::Status => "status", - Self::Sync(sync) => match sync.command { - SyncCommand::Status => "sync status", - SyncCommand::Pull => "sync pull", - SyncCommand::Push => "sync push", - SyncCommand::Watch(_) => "sync watch", - }, - } - } - - pub fn supports_output_format(&self, format: OutputFormat) -> bool { - match format { - OutputFormat::Human | OutputFormat::Json => true, - OutputFormat::Ndjson => matches!( - self, - Self::Account(AccountArgs { - command: AccountCommand::Ls, - }) | Self::Relay(RelayArgs { - command: RelayCommand::Ls, - }) | Self::Job(JobArgs { - command: JobCommand::Ls, - }) | Self::Job(JobArgs { - command: JobCommand::Watch(_), - }) | Self::Rpc(RpcArgs { - command: RpcCommand::Sessions, - }) | Self::Order(OrderArgs { - command: OrderCommand::Ls | OrderCommand::Watch(_) | OrderCommand::History, - }) | Self::Sync(SyncArgs { - command: SyncCommand::Watch(_), - }) | Self::Find(_) - | Self::Market(MarketArgs { - command: MarketCommand::Search(_), - }) - ), - } - } - - pub fn supports_dry_run(&self) -> bool { - !matches!( - self, - Self::Account(AccountArgs { - command: AccountCommand::New - | AccountCommand::Import(_) - | AccountCommand::Use(_) - | AccountCommand::ClearDefault - | AccountCommand::Remove(_), - }) | Self::Farm(FarmArgs { - command: FarmCommand::Init(_) | FarmCommand::Set(_) | FarmCommand::Setup(_), - }) | Self::Local(LocalArgs { - command: LocalCommand::Init | LocalCommand::Export(_) | LocalCommand::Backup(_), - }) | Self::Sync(SyncArgs { - command: SyncCommand::Pull | SyncCommand::Push, - }) | Self::Listing(ListingArgs { - command: ListingCommand::New(_), - }) | Self::Market(MarketArgs { - command: MarketCommand::Update, - }) | Self::Order(OrderArgs { - command: OrderCommand::New(_) | OrderCommand::Cancel(_), - }) | Self::Sell(SellArgs { - command: SellCommand::Add(_), - }) | Self::Setup(_) - ) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] -pub enum SetupRoleArg { - Seller, - Buyer, - Both, -} - -#[derive(Debug, Clone, Args)] -pub struct SetupArgs { - #[arg(value_enum, default_value = "both")] - pub role: SetupRoleArg, -} - -#[derive(Debug, Clone, Args)] -pub struct ConfigArgs { - #[command(subcommand)] - pub command: ConfigCommand, -} - -#[derive(Debug, Clone, Subcommand)] -pub enum ConfigCommand { - Show, -} - -#[derive(Debug, Clone, Args)] -pub struct AccountArgs { - #[command(subcommand)] - pub command: AccountCommand, -} - -#[derive(Debug, Clone, Subcommand)] -pub enum AccountCommand { - #[command( - name = "create", - visible_alias = "new", - about = "Create a local account" - )] - New, - #[command(name = "import", about = "Import a watch-only local account")] - Import(AccountImportArgs), - #[command( - name = "view", - visible_alias = "whoami", - about = "Show the selected local account" - )] - Whoami, - #[command(name = "list", visible_alias = "ls", about = "List local accounts")] - Ls, - #[command( - name = "select", - visible_alias = "use", - about = "Select a local account" - )] - Use(AccountUseArgs), - #[command(name = "clear-default", about = "Clear the stored default account")] - ClearDefault, - #[command(name = "remove", about = "Remove a local account")] - Remove(AccountRemoveArgs), -} - -#[derive(Debug, Clone, Args)] -pub struct AccountImportArgs { - pub path: PathBuf, - #[arg(long, action = ArgAction::SetTrue)] - pub default: bool, -} - -#[derive(Debug, Clone, Args)] -pub struct AccountUseArgs { - pub selector: String, -} - -#[derive(Debug, Clone, Args)] -pub struct AccountRemoveArgs { - pub selector: String, -} - -#[derive(Debug, Clone, Args)] -pub struct MycArgs { - #[command(subcommand)] - pub command: MycCommand, -} - -#[derive(Debug, Clone, Subcommand)] -pub enum MycCommand { - Status, -} - -#[derive(Debug, Clone, Args)] -pub struct SignerArgs { - #[command(subcommand)] - pub command: SignerCommand, -} - -#[derive(Debug, Clone, Subcommand)] -pub enum SignerCommand { - Status, - Session(SignerSessionArgs), -} - -#[derive(Debug, Clone, Args)] -pub struct SignerSessionArgs { - #[command(subcommand)] - pub command: SignerSessionCommand, -} - -#[derive(Debug, Clone, Subcommand)] -pub enum SignerSessionCommand { - List, - Show { - session_id: String, - }, - ConnectBunker { - url: String, - }, - ConnectNostrconnect { - url: String, - #[arg(long)] - client_secret_key: String, - }, - PublicKey { - session_id: String, - }, - Authorize { - session_id: String, - }, - RequireAuth { - session_id: String, - #[arg(long)] - auth_url: String, - }, - Close { - session_id: String, - }, -} - -#[derive(Debug, Clone, Args)] -pub struct RelayArgs { - #[command(subcommand)] - pub command: RelayCommand, -} - -#[derive(Debug, Clone, Subcommand)] -pub enum RelayCommand { - #[command(name = "list", visible_alias = "ls", about = "List configured relays")] - Ls, -} - -#[derive(Debug, Clone, Args)] -pub struct FarmArgs { - #[command(subcommand)] - pub command: FarmCommand, -} - -#[derive(Debug, Clone, Subcommand)] -pub enum FarmCommand { - #[command(about = "Create or refresh a farm draft progressively")] - Init(FarmInitArgs), - #[command(about = "Set one farm draft field")] - Set(FarmSetArgs), - #[command(about = "Publish the current farm draft")] - Publish(FarmPublishArgs), - #[command(about = "Create or update a farm draft in one command")] - Setup(FarmSetupArgs), - #[command( - name = "check", - visible_alias = "status", - about = "Check farm readiness" - )] - Status(FarmScopedArgs), - #[command(name = "show", visible_alias = "get", about = "Show the farm draft")] - Get(FarmScopedArgs), -} - -#[derive(Debug, Clone, Args, Default)] -pub struct FarmPublishArgs { - #[arg(long, value_enum)] - pub scope: Option<FarmScopeArg>, - #[arg(long = "idempotency-key")] - pub idempotency_key: Option<String>, - #[arg(long = "signer-session-id")] - pub signer_session_id: Option<String>, - #[arg(long = "print-job", action = ArgAction::SetTrue)] - pub print_job: bool, - #[arg(long = "print-event", action = ArgAction::SetTrue)] - pub print_event: bool, -} - -#[derive(Debug, Clone, Args, Default)] -pub struct FarmScopedArgs { - #[arg(long, value_enum)] - pub scope: Option<FarmScopeArg>, -} - -#[derive(Debug, Clone, Args, Default)] -pub struct FarmInitArgs { - #[arg(long, value_enum)] - pub scope: Option<FarmScopeArg>, - #[arg(long = "farm-d-tag")] - pub farm_d_tag: Option<String>, - #[arg(long)] - pub name: Option<String>, - #[arg(long = "display-name")] - pub display_name: Option<String>, - #[arg(long)] - pub about: Option<String>, - #[arg(long)] - pub website: Option<String>, - #[arg(long)] - pub picture: Option<String>, - #[arg(long)] - pub banner: Option<String>, - #[arg(long)] - pub location: Option<String>, - #[arg(long)] - pub city: Option<String>, - #[arg(long)] - pub region: Option<String>, - #[arg(long)] - pub country: Option<String>, - #[arg(long = "delivery", visible_alias = "delivery-method")] - pub delivery_method: Option<String>, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] -pub enum FarmFieldArg { - Name, - #[value(name = "display_name", alias = "display-name")] - DisplayName, - About, - Website, - Picture, - Banner, - Location, - City, - Region, - Country, - Delivery, -} - -#[derive(Debug, Clone, Args)] -pub struct FarmSetArgs { - #[arg(long, value_enum)] - pub scope: Option<FarmScopeArg>, - #[arg(value_enum)] - pub field: FarmFieldArg, - #[arg(value_name = "value", num_args = 1..)] - pub value: Vec<String>, -} - -#[derive(Debug, Clone, Args)] -pub struct FarmSetupArgs { - #[arg(long, value_enum)] - pub scope: Option<FarmScopeArg>, - #[arg(long = "farm-d-tag")] - pub farm_d_tag: Option<String>, - #[arg(long)] - pub name: String, - #[arg(long = "display-name")] - pub display_name: Option<String>, - #[arg(long)] - pub about: Option<String>, - #[arg(long)] - pub website: Option<String>, - #[arg(long)] - pub picture: Option<String>, - #[arg(long)] - pub banner: Option<String>, - #[arg(long)] - pub location: String, - #[arg(long)] - pub city: Option<String>, - #[arg(long)] - pub region: Option<String>, - #[arg(long)] - pub country: Option<String>, - #[arg(long = "delivery-method", default_value = "pickup")] - pub delivery_method: String, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)] -pub enum FarmScopeArg { - User, - Workspace, -} - -#[derive(Debug, Clone, Args)] -pub struct NetArgs { - #[command(subcommand)] - pub command: NetCommand, -} - -#[derive(Debug, Clone, Subcommand)] -pub enum NetCommand { - Status, -} - -#[derive(Debug, Clone, Args)] -pub struct LocalArgs { - #[command(subcommand)] - pub command: LocalCommand, -} - -#[derive(Debug, Clone, Subcommand)] -pub enum LocalCommand { - Init, - Status, - Export(LocalExportArgs), - Backup(LocalBackupArgs), -} - -#[derive(Debug, Clone, Copy, clap::ValueEnum)] -pub enum LocalExportFormatArg { - Json, - Ndjson, -} - -impl LocalExportFormatArg { - pub fn as_str(self) -> &'static str { - match self { - Self::Json => "json", - Self::Ndjson => "ndjson", - } - } -} - -#[derive(Debug, Clone, Args)] -pub struct LocalExportArgs { - #[arg(long)] - pub format: LocalExportFormatArg, - #[arg(long)] - pub output: PathBuf, -} - -#[derive(Debug, Clone, Args)] -pub struct LocalBackupArgs { - #[arg(long)] - pub output: PathBuf, -} - -#[derive(Debug, Clone, Args)] -pub struct SyncArgs { - #[command(subcommand)] - pub command: SyncCommand, -} - -#[derive(Debug, Clone, Subcommand)] -pub enum SyncCommand { - Status, - Pull, - Push, - Watch(SyncWatchArgs), -} - -#[derive(Debug, Clone, Args)] -pub struct SyncWatchArgs { - #[arg(long)] - pub frames: usize, - #[arg(long, default_value_t = 1_000)] - pub interval_ms: u64, -} - -#[derive(Debug, Clone, Args)] -pub struct FindArgs { - #[arg(value_name = "query", num_args = 1..)] - pub query: Vec<String>, -} - -#[derive(Debug, Clone, Args)] -pub struct MarketArgs { - #[command(subcommand)] - pub command: MarketCommand, -} - -#[derive(Debug, Clone, Subcommand)] -pub enum MarketCommand { - #[command(about = "Update local market data")] - Update, - #[command(about = "Search listings in local market data")] - Search(FindArgs), - #[command(about = "View one published listing")] - View(RecordKeyArgs), -} - -#[derive(Debug, Clone, Args)] -pub struct ListingArgs { - #[command(subcommand)] - pub command: ListingCommand, -} - -#[derive(Debug, Clone, Subcommand)] -pub enum ListingCommand { - New(ListingNewArgs), - Validate(ListingFileArgs), - Get(RecordKeyArgs), - Publish(ListingMutationArgs), - Update(ListingMutationArgs), - Archive(ListingMutationArgs), -} - -#[derive(Debug, Clone, Args, Default)] -pub struct ListingNewArgs { - #[arg(long)] - pub output: Option<PathBuf>, - #[arg(long)] - pub key: Option<String>, - #[arg(long)] - pub title: Option<String>, - #[arg(long)] - pub category: Option<String>, - #[arg(long)] - pub summary: Option<String>, - #[arg(long = "bin-id")] - pub bin_id: Option<String>, - #[arg(long = "quantity-amount")] - pub quantity_amount: Option<String>, - #[arg(long = "quantity-unit")] - pub quantity_unit: Option<String>, - #[arg(long = "price-amount")] - pub price_amount: Option<String>, - #[arg(long = "price-currency")] - pub price_currency: Option<String>, - #[arg(long = "price-per-amount")] - pub price_per_amount: Option<String>, - #[arg(long = "price-per-unit")] - pub price_per_unit: Option<String>, - #[arg(long)] - pub available: Option<String>, - #[arg(long)] - pub label: Option<String>, -} - -#[derive(Debug, Clone, Args)] -pub struct ListingFileArgs { - pub file: PathBuf, -} - -#[derive(Debug, Clone, Args)] -pub struct ListingMutationArgs { - pub file: PathBuf, - #[arg(long)] - pub idempotency_key: Option<String>, - #[arg(long = "signer-session-id")] - pub signer_session_id: Option<String>, - #[arg(long = "print-job", action = ArgAction::SetTrue)] - pub print_job: bool, - #[arg(long = "print-event", action = ArgAction::SetTrue)] - pub print_event: bool, -} - -#[derive(Debug, Clone, Args)] -pub struct JobArgs { - #[command(subcommand)] - pub command: JobCommand, -} - -#[derive(Debug, Clone, Subcommand)] -pub enum JobCommand { - #[command(name = "list", visible_alias = "ls", about = "List background jobs")] - Ls, - #[command(about = "Show one background job")] - Get(RecordKeyArgs), - #[command(about = "Watch a background job")] - Watch(JobWatchArgs), -} - -#[derive(Debug, Clone, Args)] -pub struct JobWatchArgs { - pub key: String, - #[arg(long)] - pub frames: Option<usize>, - #[arg(long, default_value_t = 1_000)] - pub interval_ms: u64, -} - -#[derive(Debug, Clone, Args)] -pub struct RpcArgs { - #[command(subcommand)] - pub command: RpcCommand, -} - -#[derive(Debug, Clone, Subcommand)] -pub enum RpcCommand { - Status, - Sessions, -} - -#[derive(Debug, Clone, Args)] -pub struct RuntimeArgs { - #[command(subcommand)] - pub command: RuntimeCommand, -} - -#[derive(Debug, Clone, Subcommand)] -pub enum RuntimeCommand { - Install(RuntimeTargetArgs), - Uninstall(RuntimeTargetArgs), - Status(RuntimeTargetArgs), - Start(RuntimeTargetArgs), - Stop(RuntimeTargetArgs), - Restart(RuntimeTargetArgs), - Logs(RuntimeTargetArgs), - Config(RuntimeConfigArgs), -} - -#[derive(Debug, Clone, Args)] -pub struct RuntimeTargetArgs { - pub runtime: String, - #[arg(long)] - pub instance: Option<String>, -} - -#[derive(Debug, Clone, Args)] -pub struct RuntimeConfigArgs { - #[command(subcommand)] - pub command: RuntimeConfigCommand, -} - -#[derive(Debug, Clone, Subcommand)] -pub enum RuntimeConfigCommand { - Show(RuntimeTargetArgs), - Set(RuntimeConfigSetArgs), -} - -#[derive(Debug, Clone, Args)] -pub struct RuntimeConfigSetArgs { - #[command(flatten)] - pub target: RuntimeTargetArgs, - pub key: String, - pub value: String, -} - -#[derive(Debug, Clone, Args)] -pub struct OrderArgs { - #[command(subcommand)] - pub command: OrderCommand, -} - -#[derive(Debug, Clone, Subcommand)] -pub enum OrderCommand { - #[command( - name = "create", - visible_alias = "new", - about = "Create a local order draft" - )] - New(OrderNewArgs), - #[command(name = "view", visible_alias = "get", about = "Show one order")] - Get(RecordKeyArgs), - #[command(name = "list", visible_alias = "ls", about = "List local orders")] - Ls, - #[command(about = "Submit a local order draft")] - Submit(OrderSubmitArgs), - #[command(about = "Watch a submitted order")] - Watch(OrderWatchArgs), - #[command(about = "Explain durable order cancel availability")] - Cancel(RecordKeyArgs), - #[command(about = "Show submitted order history")] - History, -} - -#[derive(Debug, Clone, Args, Default)] -pub struct OrderNewArgs { - #[arg(long)] - pub listing: Option<String>, - #[arg(long = "listing-addr")] - pub listing_addr: Option<String>, - #[arg(long = "bin")] - pub bin_id: Option<String>, - #[arg(long = "qty")] - pub bin_count: Option<u32>, -} - -#[derive(Debug, Clone, Args)] -pub struct OrderSubmitArgs { - pub key: String, - #[arg(long, action = ArgAction::SetTrue)] - pub watch: bool, - #[arg(long)] - pub idempotency_key: Option<String>, - #[arg(long = "signer-session-id")] - pub signer_session_id: Option<String>, -} - -#[derive(Debug, Clone, Args)] -pub struct OrderWatchArgs { - pub key: String, - #[arg(long)] - pub frames: Option<usize>, - #[arg(long, default_value_t = 1_000)] - pub interval_ms: u64, -} - -#[derive(Debug, Clone, Args)] -pub struct RecordKeyArgs { - pub key: String, -} - -#[derive(Debug, Clone, Args)] -pub struct SellArgs { - #[command(subcommand)] - pub command: SellCommand, -} - -#[derive(Debug, Clone, Args)] -pub struct SellAddArgs { - pub product: String, - #[arg(long)] - pub file: Option<PathBuf>, - #[arg(long)] - pub title: Option<String>, - #[arg(long)] - pub category: Option<String>, - #[arg(long)] - pub summary: Option<String>, - #[arg(long = "pack")] - pub pack: Option<String>, - #[arg(long = "price")] - pub price_expr: Option<String>, - #[arg(long = "stock")] - pub stock: Option<String>, -} - -#[derive(Debug, Clone, Subcommand)] -pub enum SellCommand { - #[command(about = "Create a listing draft")] - Add(SellAddArgs), - #[command(about = "Show a local listing draft")] - Show(SellShowArgs), - #[command(about = "Check a listing draft")] - Check(ListingFileArgs), - #[command(about = "Publish a listing draft")] - Publish(ListingMutationArgs), - #[command(about = "Update a published listing from a draft")] - Update(ListingMutationArgs), - #[command(about = "Pause a published listing")] - Pause(ListingMutationArgs), - #[command(about = "Change the price in a local listing draft")] - Reprice(SellRepriceArgs), - #[command(about = "Change the available stock in a local listing draft")] - Restock(SellRestockArgs), -} - -#[derive(Debug, Clone, Args)] -pub struct SellShowArgs { - pub file: PathBuf, -} - -#[derive(Debug, Clone, Args)] -pub struct SellRepriceArgs { - pub file: PathBuf, - pub price_expr: String, -} - -#[derive(Debug, Clone, Args)] -pub struct SellRestockArgs { - pub file: PathBuf, - pub available: String, -} diff --git a/src/commands/doctor.rs b/src/commands/doctor.rs @@ -1,414 +0,0 @@ -use crate::domain::runtime::{ - CommandDisposition, CommandOutput, CommandView, DoctorCheckView, DoctorView, -}; -use crate::runtime::RuntimeError; -use crate::runtime::config::{RuntimeConfig, SignerBackend}; -use crate::runtime::logging::LoggingState; -use crate::runtime::provider::{resolve_hyf_provider, resolve_workflow_provider}; -use crate::runtime::signer::resolve_signer_status; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -enum DoctorSeverity { - Ok, - Warn, - ExternalFail, - InternalFail, -} - -impl DoctorSeverity { - fn status(self) -> &'static str { - match self { - Self::Ok => "ok", - Self::Warn => "warn", - Self::ExternalFail | Self::InternalFail => "fail", - } - } - - fn command_disposition(self) -> CommandDisposition { - match self { - Self::Ok => CommandDisposition::Success, - Self::Warn => CommandDisposition::Unconfigured, - Self::ExternalFail => CommandDisposition::ExternalUnavailable, - Self::InternalFail => CommandDisposition::InternalError, - } - } -} - -struct EvaluatedCheck { - severity: DoctorSeverity, - view: DoctorCheckView, - action: Option<&'static str>, -} - -pub fn report( - config: &RuntimeConfig, - logging: &LoggingState, -) -> Result<CommandOutput, RuntimeError> { - let mut checks = Vec::new(); - checks.push(config_check(config)); - let account_resolution = crate::runtime::accounts::resolve_account_resolution(config)?; - checks.push(account_check(config, &account_resolution)?); - checks.push(relay_check(config)); - - let signer = resolve_signer_status(config); - checks.push(signer_check(&signer)); - - if matches!(config.signer.backend, SignerBackend::Myc) { - if let Some(myc) = signer.myc.as_ref() { - checks.push(myc_check(myc)); - } - } - - checks.push(hyf_check(&resolve_hyf_provider(config))); - checks.push(workflow_check(&resolve_workflow_provider(config))); - checks.push(logging_check(config, logging)); - checks.push(binding_check(config)); - - let severity = checks - .iter() - .map(|check| check.severity) - .max() - .unwrap_or(DoctorSeverity::Ok); - let actions = collect_actions(&checks); - let view = DoctorView { - ok: severity == DoctorSeverity::Ok, - state: severity.status().to_owned(), - account_resolution: crate::runtime::accounts::account_resolution_view(&account_resolution), - checks: checks.into_iter().map(|check| check.view).collect(), - source: doctor_source(config), - actions, - }; - - Ok(match severity.command_disposition() { - CommandDisposition::Success => CommandOutput::success(CommandView::Doctor(view)), - CommandDisposition::Unconfigured => CommandOutput::unconfigured(CommandView::Doctor(view)), - CommandDisposition::ExternalUnavailable => { - CommandOutput::external_unavailable(CommandView::Doctor(view)) - } - CommandDisposition::Unsupported => CommandOutput::unsupported(CommandView::Doctor(view)), - CommandDisposition::InternalError => { - CommandOutput::internal_error(CommandView::Doctor(view)) - } - }) -} - -fn config_check(config: &RuntimeConfig) -> EvaluatedCheck { - let detail = match ( - config.paths.app_config_path.exists(), - config - .paths - .workspace_config_path - .as_ref() - .is_some_and(|path| path.exists()), - ) { - (false, false) => "defaults active".to_owned(), - (true, false) => "app config root present".to_owned(), - (false, true) => "workspace config root present".to_owned(), - (true, true) => "app and workspace config roots present".to_owned(), - }; - - EvaluatedCheck { - severity: DoctorSeverity::Ok, - view: DoctorCheckView { - name: "config".to_owned(), - status: "ok".to_owned(), - detail, - }, - action: None, - } -} - -fn account_check( - config: &RuntimeConfig, - account_resolution: &crate::runtime::accounts::AccountResolution, -) -> Result<EvaluatedCheck, RuntimeError> { - let snapshot = crate::runtime::accounts::snapshot(config)?; - if snapshot.accounts.is_empty() { - return Ok(EvaluatedCheck { - severity: DoctorSeverity::Warn, - view: DoctorCheckView { - name: "account".to_owned(), - status: "warn".to_owned(), - detail: format!( - "no local accounts found in {}", - config.account.store_path.display() - ), - }, - action: Some("radroots account new"), - }); - } - - match account_resolution.resolved_account.as_ref() { - Some(account) => { - let detail = match account_resolution.source { - crate::runtime::accounts::AccountResolutionSource::InvocationOverride => { - match account_resolution.default_account.as_ref() { - Some(default) if default.record.account_id != account.record.account_id => { - format!( - "resolved account {} via invocation override; default account {} remains stored", - account.record.account_id, default.record.account_id - ) - } - Some(default) => format!( - "resolved account {} via invocation override; default account {} is also stored", - account.record.account_id, default.record.account_id - ), - None => format!( - "resolved account {} via invocation override; no default account is stored", - account.record.account_id - ), - } - } - crate::runtime::accounts::AccountResolutionSource::DefaultAccount => { - format!( - "resolved account {} via default account", - account.record.account_id - ) - } - crate::runtime::accounts::AccountResolutionSource::None => { - format!("resolved account {}", account.record.account_id) - } - }; - Ok(EvaluatedCheck { - severity: DoctorSeverity::Ok, - view: DoctorCheckView { - name: "account".to_owned(), - status: "ok".to_owned(), - detail, - }, - action: None, - }) - } - None => Ok(EvaluatedCheck { - severity: DoctorSeverity::Warn, - view: DoctorCheckView { - name: "account".to_owned(), - status: "warn".to_owned(), - detail: crate::runtime::accounts::unresolved_account_reason(config)?, - }, - action: Some("radroots account ls"), - }), - } -} - -fn signer_check(signer: &crate::domain::runtime::SignerStatusView) -> EvaluatedCheck { - let (severity, detail, action) = match signer.state.as_str() { - "ready" => (DoctorSeverity::Ok, format!("{} ready", signer.mode), None), - "unconfigured" => ( - DoctorSeverity::Warn, - signer - .reason - .clone() - .unwrap_or_else(|| format!("{} signer is not configured", signer.mode)), - Some("radroots signer status get"), - ), - "degraded" | "unavailable" => ( - DoctorSeverity::ExternalFail, - signer - .reason - .clone() - .unwrap_or_else(|| format!("{} signer is unavailable", signer.mode)), - Some("radroots signer status get"), - ), - _ => ( - DoctorSeverity::InternalFail, - signer - .reason - .clone() - .unwrap_or_else(|| format!("{} signer reported an internal error", signer.mode)), - Some("radroots --format json signer status get"), - ), - }; - - EvaluatedCheck { - severity, - view: DoctorCheckView { - name: "signer".to_owned(), - status: severity.status().to_owned(), - detail, - }, - action, - } -} - -fn relay_check(config: &RuntimeConfig) -> EvaluatedCheck { - if config.relay.urls.is_empty() { - return EvaluatedCheck { - severity: DoctorSeverity::Warn, - view: DoctorCheckView { - name: "relays".to_owned(), - status: "warn".to_owned(), - detail: "no relays configured".to_owned(), - }, - action: Some("radroots relay ls"), - }; - } - - EvaluatedCheck { - severity: DoctorSeverity::Ok, - view: DoctorCheckView { - name: "relays".to_owned(), - status: "ok".to_owned(), - detail: format!( - "{} configured · policy {}", - config.relay.urls.len(), - config.relay.publish_policy.as_str() - ), - }, - action: None, - } -} - -fn myc_check(myc: &crate::domain::runtime::MycStatusView) -> EvaluatedCheck { - let (severity, detail, action) = match myc.state.as_str() { - "ready" => ( - DoctorSeverity::Ok, - myc.service_status - .clone() - .unwrap_or_else(|| "service ready".to_owned()), - None, - ), - "unconfigured" => ( - DoctorSeverity::Warn, - myc.reason - .clone() - .unwrap_or_else(|| "myc is not configured".to_owned()), - Some("radroots signer status get"), - ), - _ => ( - DoctorSeverity::ExternalFail, - myc.reason - .clone() - .unwrap_or_else(|| "myc is unavailable".to_owned()), - Some("radroots signer status get"), - ), - }; - - EvaluatedCheck { - severity, - view: DoctorCheckView { - name: "myc".to_owned(), - status: severity.status().to_owned(), - detail, - }, - action, - } -} - -fn hyf_check(hyf: &crate::runtime::provider::HyfProviderView) -> EvaluatedCheck { - let (severity, detail) = match hyf.state.as_str() { - "disabled" => ( - DoctorSeverity::Ok, - hyf.reason - .clone() - .unwrap_or_else(|| "disabled by config".to_owned()), - ), - "ready" => ( - DoctorSeverity::Ok, - hyf.reason - .clone() - .unwrap_or_else(|| "healthy · protocol 1 · deterministic available".to_owned()), - ), - _ => ( - DoctorSeverity::ExternalFail, - hyf.reason - .clone() - .unwrap_or_else(|| "hyf is unavailable".to_owned()), - ), - }; - - EvaluatedCheck { - severity, - view: DoctorCheckView { - name: "hyf".to_owned(), - status: severity.status().to_owned(), - detail, - }, - action: None, - } -} - -fn workflow_check(workflow: &crate::runtime::provider::WorkflowProviderView) -> EvaluatedCheck { - let severity = match workflow.state.as_str() { - "ready" => DoctorSeverity::Ok, - "not_configured" | "disabled" | "unavailable" => DoctorSeverity::Warn, - "unsupported" | "incompatible" => DoctorSeverity::ExternalFail, - _ => DoctorSeverity::InternalFail, - }; - - EvaluatedCheck { - severity, - view: DoctorCheckView { - name: "workflow".to_owned(), - status: severity.status().to_owned(), - detail: workflow.detail(), - }, - action: None, - } -} - -fn logging_check(config: &RuntimeConfig, logging: &LoggingState) -> EvaluatedCheck { - let detail = match (config.logging.stdout, logging.current_file.as_ref()) { - (true, Some(path)) => format!("stdout + file {}", path.display()), - (true, None) => "stdout only".to_owned(), - (false, Some(path)) => format!("file {}", path.display()), - (false, None) => "stdout off · no file sink".to_owned(), - }; - - EvaluatedCheck { - severity: DoctorSeverity::Ok, - view: DoctorCheckView { - name: "logging".to_owned(), - status: "ok".to_owned(), - detail, - }, - action: None, - } -} - -fn binding_check(config: &RuntimeConfig) -> EvaluatedCheck { - let inspections = config.inspect_capability_bindings(); - let mut configured = 0usize; - let mut disabled = 0usize; - let mut not_configured = 0usize; - for inspection in inspections { - match inspection.state.as_str() { - "configured" => configured += 1, - "disabled" => disabled += 1, - _ => not_configured += 1, - } - } - - EvaluatedCheck { - severity: DoctorSeverity::Ok, - view: DoctorCheckView { - name: "bindings".to_owned(), - status: "ok".to_owned(), - detail: format!( - "{configured} configured · {disabled} disabled · {not_configured} not configured" - ), - }, - action: None, - } -} - -fn collect_actions(checks: &[EvaluatedCheck]) -> Vec<String> { - let mut actions = Vec::new(); - for action in checks.iter().filter_map(|check| check.action) { - if !actions.iter().any(|existing| existing == action) { - actions.push(action.to_owned()); - } - } - actions -} - -fn doctor_source(config: &RuntimeConfig) -> String { - let mut sources = vec!["local diagnostics"]; - if matches!(config.signer.backend, SignerBackend::Myc) { - sources.push("myc status command"); - } - if config.hyf.enabled { - sources.push("hyf status control request"); - } - sources.join(" + ") -} diff --git a/src/commands/farm.rs b/src/commands/farm.rs @@ -1,123 +0,0 @@ -use crate::cli::{FarmInitArgs, FarmPublishArgs, FarmScopedArgs, FarmSetArgs, FarmSetupArgs}; -use crate::domain::runtime::{ - CommandDisposition, CommandOutput, CommandView, FarmGetView, FarmPublishView, FarmSetView, - FarmSetupView, FarmStatusView, -}; -use crate::runtime::RuntimeError; -use crate::runtime::config::RuntimeConfig; - -pub fn setup(config: &RuntimeConfig, args: &FarmSetupArgs) -> Result<CommandOutput, RuntimeError> { - let view = crate::runtime::farm::setup(config, args)?; - Ok(farm_setup_output(view)) -} - -pub fn init(config: &RuntimeConfig, args: &FarmInitArgs) -> Result<CommandOutput, RuntimeError> { - let view = crate::runtime::farm::init(config, args)?; - Ok(farm_setup_output(view)) -} - -pub fn set(config: &RuntimeConfig, args: &FarmSetArgs) -> Result<CommandOutput, RuntimeError> { - let view = crate::runtime::farm::set(config, args)?; - Ok(farm_set_output(view)) -} - -pub fn publish( - config: &RuntimeConfig, - args: &FarmPublishArgs, -) -> Result<CommandOutput, RuntimeError> { - let view = crate::runtime::farm::publish(config, args)?; - Ok(farm_publish_output(view)) -} - -pub fn status( - config: &RuntimeConfig, - args: &FarmScopedArgs, -) -> Result<CommandOutput, RuntimeError> { - let view = crate::runtime::farm::status(config, args)?; - Ok(farm_status_output(view)) -} - -pub fn get(config: &RuntimeConfig, args: &FarmScopedArgs) -> Result<CommandOutput, RuntimeError> { - let view = crate::runtime::farm::get(config, args)?; - Ok(farm_get_output(view)) -} - -fn farm_publish_output(view: FarmPublishView) -> CommandOutput { - match view.disposition() { - CommandDisposition::Success => CommandOutput::success(CommandView::FarmPublish(view)), - CommandDisposition::Unconfigured => { - CommandOutput::unconfigured(CommandView::FarmPublish(view)) - } - CommandDisposition::ExternalUnavailable => { - CommandOutput::external_unavailable(CommandView::FarmPublish(view)) - } - CommandDisposition::Unsupported => { - CommandOutput::unsupported(CommandView::FarmPublish(view)) - } - CommandDisposition::InternalError => { - CommandOutput::internal_error(CommandView::FarmPublish(view)) - } - } -} - -fn farm_setup_output(view: FarmSetupView) -> CommandOutput { - match view.disposition() { - CommandDisposition::Success => CommandOutput::success(CommandView::FarmSetup(view)), - CommandDisposition::Unconfigured => { - CommandOutput::unconfigured(CommandView::FarmSetup(view)) - } - CommandDisposition::ExternalUnavailable => { - CommandOutput::external_unavailable(CommandView::FarmSetup(view)) - } - CommandDisposition::Unsupported => CommandOutput::unsupported(CommandView::FarmSetup(view)), - CommandDisposition::InternalError => { - CommandOutput::internal_error(CommandView::FarmSetup(view)) - } - } -} - -fn farm_set_output(view: FarmSetView) -> CommandOutput { - match view.disposition() { - CommandDisposition::Success => CommandOutput::success(CommandView::FarmSet(view)), - CommandDisposition::Unconfigured => CommandOutput::unconfigured(CommandView::FarmSet(view)), - CommandDisposition::ExternalUnavailable => { - CommandOutput::external_unavailable(CommandView::FarmSet(view)) - } - CommandDisposition::Unsupported => CommandOutput::unsupported(CommandView::FarmSet(view)), - CommandDisposition::InternalError => { - CommandOutput::internal_error(CommandView::FarmSet(view)) - } - } -} - -fn farm_status_output(view: FarmStatusView) -> CommandOutput { - match view.disposition() { - CommandDisposition::Success => CommandOutput::success(CommandView::FarmStatus(view)), - CommandDisposition::Unconfigured => { - CommandOutput::unconfigured(CommandView::FarmStatus(view)) - } - CommandDisposition::ExternalUnavailable => { - CommandOutput::external_unavailable(CommandView::FarmStatus(view)) - } - CommandDisposition::Unsupported => { - CommandOutput::unsupported(CommandView::FarmStatus(view)) - } - CommandDisposition::InternalError => { - CommandOutput::internal_error(CommandView::FarmStatus(view)) - } - } -} - -fn farm_get_output(view: FarmGetView) -> CommandOutput { - match view.disposition() { - CommandDisposition::Success => CommandOutput::success(CommandView::FarmGet(view)), - CommandDisposition::Unconfigured => CommandOutput::unconfigured(CommandView::FarmGet(view)), - CommandDisposition::ExternalUnavailable => { - CommandOutput::external_unavailable(CommandView::FarmGet(view)) - } - CommandDisposition::Unsupported => CommandOutput::unsupported(CommandView::FarmGet(view)), - CommandDisposition::InternalError => { - CommandOutput::internal_error(CommandView::FarmGet(view)) - } - } -} diff --git a/src/commands/find.rs b/src/commands/find.rs @@ -1,17 +0,0 @@ -use crate::cli::FindArgs; -use crate::domain::runtime::{CommandDisposition, CommandOutput, CommandView}; -use crate::runtime::RuntimeError; -use crate::runtime::config::RuntimeConfig; - -pub fn search(config: &RuntimeConfig, args: &FindArgs) -> Result<CommandOutput, RuntimeError> { - let view = crate::runtime::find::search(config, args)?; - Ok(match view.disposition() { - CommandDisposition::Success => CommandOutput::success(CommandView::Find(view)), - CommandDisposition::Unconfigured => CommandOutput::unconfigured(CommandView::Find(view)), - CommandDisposition::ExternalUnavailable => { - CommandOutput::external_unavailable(CommandView::Find(view)) - } - CommandDisposition::Unsupported => CommandOutput::unsupported(CommandView::Find(view)), - CommandDisposition::InternalError => CommandOutput::internal_error(CommandView::Find(view)), - }) -} diff --git a/src/commands/identity.rs b/src/commands/identity.rs @@ -1,202 +0,0 @@ -use crate::cli::AccountImportArgs; -use crate::domain::runtime::{ - AccountClearDefaultView, AccountImportView, AccountListView, AccountNewView, AccountRemoveView, - AccountSummaryView, AccountUseView, AccountWhoamiView, CommandDisposition, CommandOutput, - CommandView, IdentityPublicView, -}; -use crate::runtime::RuntimeError; -use crate::runtime::accounts::{ - AccountCreateMode, AccountRecordView, SHARED_ACCOUNT_STORE_SOURCE, account_resolution_view, - account_summary_view, clear_default_account, create_or_migrate_default_account, - import_public_identity, remove_account as remove_stored_account, resolve_account_resolution, - select_account, snapshot, unresolved_account_reason, -}; -use crate::runtime::config::RuntimeConfig; - -pub fn init(config: &RuntimeConfig) -> Result<AccountNewView, RuntimeError> { - let result = create_or_migrate_default_account(config)?; - let account = account_summary(&result.account); - Ok(AccountNewView { - state: match result.mode { - AccountCreateMode::Created => "created".to_owned(), - AccountCreateMode::Migrated => "migrated".to_owned(), - }, - source: match result.mode { - AccountCreateMode::Created => SHARED_ACCOUNT_STORE_SOURCE.to_owned(), - AccountCreateMode::Migrated => "legacy shared identity import · local first".to_owned(), - }, - public_identity: IdentityPublicView::from_public_identity( - &result.account.record.public_identity, - ), - account, - actions: vec![ - "radroots account whoami".to_owned(), - "radroots account ls".to_owned(), - ], - }) -} - -pub fn show(config: &RuntimeConfig) -> Result<CommandOutput, RuntimeError> { - let resolution = resolve_account_resolution(config)?; - let snapshot = snapshot(config)?; - let view = match resolution.resolved_account.as_ref() { - Some(account) => AccountWhoamiView { - state: "ready".to_owned(), - source: SHARED_ACCOUNT_STORE_SOURCE.to_owned(), - reason: None, - account_resolution: account_resolution_view(&resolution), - public_identity: Some(IdentityPublicView::from_public_identity( - &account.record.public_identity, - )), - actions: Vec::new(), - }, - None => AccountWhoamiView { - state: "unconfigured".to_owned(), - source: SHARED_ACCOUNT_STORE_SOURCE.to_owned(), - reason: Some(unresolved_account_reason(config)?), - account_resolution: account_resolution_view(&resolution), - public_identity: None, - actions: unresolved_account_actions(snapshot.accounts.is_empty()), - }, - }; - - Ok(match view.disposition() { - CommandDisposition::Success => CommandOutput::success(CommandView::AccountWhoami(view)), - CommandDisposition::Unconfigured => { - CommandOutput::unconfigured(CommandView::AccountWhoami(view)) - } - CommandDisposition::ExternalUnavailable => { - CommandOutput::external_unavailable(CommandView::AccountWhoami(view)) - } - CommandDisposition::Unsupported => { - CommandOutput::unsupported(CommandView::AccountWhoami(view)) - } - CommandDisposition::InternalError => { - CommandOutput::internal_error(CommandView::AccountWhoami(view)) - } - }) -} - -pub fn import( - config: &RuntimeConfig, - args: &AccountImportArgs, -) -> Result<AccountImportView, RuntimeError> { - let account = import_public_identity(config, args.path.as_path(), args.default)?; - let account_view = account_summary(&account); - Ok(AccountImportView { - state: "imported".to_owned(), - source: "shared account store · watch-only import".to_owned(), - public_identity: IdentityPublicView::from_public_identity(&account.record.public_identity), - actions: if account.is_default { - vec![ - "radroots account view".to_owned(), - "radroots account list".to_owned(), - ] - } else { - vec![ - "radroots account list".to_owned(), - "radroots account select <selector>".to_owned(), - ] - }, - account: account_view, - }) -} - -pub fn list(config: &RuntimeConfig) -> Result<CommandOutput, RuntimeError> { - let snapshot = snapshot(config)?; - let accounts = snapshot - .accounts - .iter() - .map(account_summary) - .collect::<Vec<_>>(); - let actions = if accounts.is_empty() { - vec![ - "radroots account create".to_owned(), - "radroots account import <path>".to_owned(), - ] - } else { - Vec::new() - }; - Ok(CommandOutput::success(CommandView::AccountList( - AccountListView { - source: SHARED_ACCOUNT_STORE_SOURCE.to_owned(), - count: accounts.len(), - accounts, - actions, - }, - ))) -} - -pub fn use_account(config: &RuntimeConfig, selector: &str) -> Result<AccountUseView, RuntimeError> { - let account = select_account(config, selector)?; - Ok(AccountUseView { - state: "default".to_owned(), - source: SHARED_ACCOUNT_STORE_SOURCE.to_owned(), - default_account_id: account.record.account_id.to_string(), - account: account_summary(&account), - }) -} - -pub fn clear_default(config: &RuntimeConfig) -> Result<AccountClearDefaultView, RuntimeError> { - let result = clear_default_account(config)?; - let cleared_account = result.cleared_account.as_ref().map(account_summary); - Ok(AccountClearDefaultView { - state: if cleared_account.is_some() { - "cleared".to_owned() - } else { - "already_clear".to_owned() - }, - source: SHARED_ACCOUNT_STORE_SOURCE.to_owned(), - actions: follow_up_account_actions(result.remaining_account_count), - cleared_account, - remaining_account_count: result.remaining_account_count, - }) -} - -pub fn remove(config: &RuntimeConfig, selector: &str) -> Result<AccountRemoveView, RuntimeError> { - let result = remove_stored_account(config, selector)?; - Ok(AccountRemoveView { - state: "removed".to_owned(), - source: SHARED_ACCOUNT_STORE_SOURCE.to_owned(), - removed_account: account_summary(&result.removed_account), - default_cleared: result.default_cleared, - remaining_account_count: result.remaining_account_count, - actions: if result.default_cleared { - follow_up_account_actions(result.remaining_account_count) - } else { - Vec::new() - }, - }) -} - -fn account_summary(account: &AccountRecordView) -> AccountSummaryView { - account_summary_view(account) -} - -fn unresolved_account_actions(has_accounts: bool) -> Vec<String> { - if has_accounts { - vec![ - "radroots account list".to_owned(), - "radroots account select <selector>".to_owned(), - ] - } else { - vec![ - "radroots account create".to_owned(), - "radroots account import <path>".to_owned(), - ] - } -} - -fn follow_up_account_actions(remaining_account_count: usize) -> Vec<String> { - if remaining_account_count == 0 { - vec![ - "radroots account create".to_owned(), - "radroots account import <path>".to_owned(), - ] - } else { - vec![ - "radroots account list".to_owned(), - "radroots account select <selector>".to_owned(), - ] - } -} diff --git a/src/commands/job.rs b/src/commands/job.rs @@ -1,16 +0,0 @@ -use crate::cli::JobWatchArgs; -use crate::domain::runtime::CommandOutput; -use crate::runtime::RuntimeError; -use crate::runtime::config::RuntimeConfig; - -pub fn list(config: &RuntimeConfig) -> CommandOutput { - crate::runtime::job::list(config) -} - -pub fn get(config: &RuntimeConfig, job_id: &str) -> CommandOutput { - crate::runtime::job::get(config, job_id) -} - -pub fn watch(config: &RuntimeConfig, args: &JobWatchArgs) -> Result<CommandOutput, RuntimeError> { - crate::runtime::job::watch(config, args) -} diff --git a/src/commands/listing.rs b/src/commands/listing.rs @@ -1,127 +0,0 @@ -use crate::cli::{ListingFileArgs, ListingMutationArgs, ListingNewArgs, RecordKeyArgs}; -use crate::domain::runtime::{CommandOutput, CommandView}; -use crate::runtime::RuntimeError; -use crate::runtime::config::RuntimeConfig; - -pub fn new(config: &RuntimeConfig, args: &ListingNewArgs) -> Result<CommandOutput, RuntimeError> { - let view = crate::runtime::listing::scaffold(config, args)?; - Ok(match view.disposition() { - crate::domain::runtime::CommandDisposition::Success => { - CommandOutput::success(CommandView::ListingNew(view)) - } - crate::domain::runtime::CommandDisposition::Unconfigured => { - CommandOutput::unconfigured(CommandView::ListingNew(view)) - } - crate::domain::runtime::CommandDisposition::ExternalUnavailable => { - CommandOutput::external_unavailable(CommandView::ListingNew(view)) - } - crate::domain::runtime::CommandDisposition::Unsupported => { - CommandOutput::unsupported(CommandView::ListingNew(view)) - } - crate::domain::runtime::CommandDisposition::InternalError => { - CommandOutput::internal_error(CommandView::ListingNew(view)) - } - }) -} - -pub fn validate( - config: &RuntimeConfig, - args: &ListingFileArgs, -) -> Result<CommandOutput, RuntimeError> { - let view = crate::runtime::listing::validate(config, args)?; - Ok(CommandOutput::success(CommandView::ListingValidate(view))) -} - -pub fn get(config: &RuntimeConfig, args: &RecordKeyArgs) -> Result<CommandOutput, RuntimeError> { - let view = crate::runtime::listing::get(config, args)?; - let output = match view.disposition() { - crate::domain::runtime::CommandDisposition::Success => { - CommandOutput::success(CommandView::ListingGet(view)) - } - crate::domain::runtime::CommandDisposition::Unconfigured => { - CommandOutput::unconfigured(CommandView::ListingGet(view)) - } - crate::domain::runtime::CommandDisposition::ExternalUnavailable => { - CommandOutput::external_unavailable(CommandView::ListingGet(view)) - } - crate::domain::runtime::CommandDisposition::Unsupported => { - CommandOutput::unsupported(CommandView::ListingGet(view)) - } - crate::domain::runtime::CommandDisposition::InternalError => { - CommandOutput::internal_error(CommandView::ListingGet(view)) - } - }; - Ok(output) -} - -pub fn publish( - config: &RuntimeConfig, - args: &ListingMutationArgs, -) -> Result<CommandOutput, RuntimeError> { - let view = crate::runtime::listing::publish(config, args)?; - Ok(match view.disposition() { - crate::domain::runtime::CommandDisposition::Success => { - CommandOutput::success(CommandView::ListingMutation(view)) - } - crate::domain::runtime::CommandDisposition::Unconfigured => { - CommandOutput::unconfigured(CommandView::ListingMutation(view)) - } - crate::domain::runtime::CommandDisposition::ExternalUnavailable => { - CommandOutput::external_unavailable(CommandView::ListingMutation(view)) - } - crate::domain::runtime::CommandDisposition::Unsupported => { - CommandOutput::unsupported(CommandView::ListingMutation(view)) - } - crate::domain::runtime::CommandDisposition::InternalError => { - CommandOutput::internal_error(CommandView::ListingMutation(view)) - } - }) -} - -pub fn update( - config: &RuntimeConfig, - args: &ListingMutationArgs, -) -> Result<CommandOutput, RuntimeError> { - let view = crate::runtime::listing::update(config, args)?; - Ok(match view.disposition() { - crate::domain::runtime::CommandDisposition::Success => { - CommandOutput::success(CommandView::ListingMutation(view)) - } - crate::domain::runtime::CommandDisposition::Unconfigured => { - CommandOutput::unconfigured(CommandView::ListingMutation(view)) - } - crate::domain::runtime::CommandDisposition::ExternalUnavailable => { - CommandOutput::external_unavailable(CommandView::ListingMutation(view)) - } - crate::domain::runtime::CommandDisposition::Unsupported => { - CommandOutput::unsupported(CommandView::ListingMutation(view)) - } - crate::domain::runtime::CommandDisposition::InternalError => { - CommandOutput::internal_error(CommandView::ListingMutation(view)) - } - }) -} - -pub fn archive( - config: &RuntimeConfig, - args: &ListingMutationArgs, -) -> Result<CommandOutput, RuntimeError> { - let view = crate::runtime::listing::archive(config, args)?; - Ok(match view.disposition() { - crate::domain::runtime::CommandDisposition::Success => { - CommandOutput::success(CommandView::ListingMutation(view)) - } - crate::domain::runtime::CommandDisposition::Unconfigured => { - CommandOutput::unconfigured(CommandView::ListingMutation(view)) - } - crate::domain::runtime::CommandDisposition::ExternalUnavailable => { - CommandOutput::external_unavailable(CommandView::ListingMutation(view)) - } - crate::domain::runtime::CommandDisposition::Unsupported => { - CommandOutput::unsupported(CommandView::ListingMutation(view)) - } - crate::domain::runtime::CommandDisposition::InternalError => { - CommandOutput::internal_error(CommandView::ListingMutation(view)) - } - }) -} diff --git a/src/commands/local.rs b/src/commands/local.rs @@ -1,73 +0,0 @@ -use crate::cli::{LocalBackupArgs, LocalExportArgs}; -use crate::domain::runtime::{CommandDisposition, CommandOutput, CommandView}; -use crate::runtime::RuntimeError; -use crate::runtime::config::RuntimeConfig; - -pub fn init(config: &RuntimeConfig) -> Result<CommandOutput, RuntimeError> { - Ok(CommandOutput::success(CommandView::LocalInit( - crate::runtime::local::init(config)?, - ))) -} - -pub fn status(config: &RuntimeConfig) -> Result<CommandOutput, RuntimeError> { - let view = crate::runtime::local::status(config)?; - Ok(match view.disposition() { - CommandDisposition::Success => CommandOutput::success(CommandView::LocalStatus(view)), - CommandDisposition::Unconfigured => { - CommandOutput::unconfigured(CommandView::LocalStatus(view)) - } - CommandDisposition::ExternalUnavailable => { - CommandOutput::external_unavailable(CommandView::LocalStatus(view)) - } - CommandDisposition::Unsupported => { - CommandOutput::unsupported(CommandView::LocalStatus(view)) - } - CommandDisposition::InternalError => { - CommandOutput::internal_error(CommandView::LocalStatus(view)) - } - }) -} - -pub fn backup( - config: &RuntimeConfig, - args: &LocalBackupArgs, -) -> Result<CommandOutput, RuntimeError> { - let view = crate::runtime::local::backup(config, args.output.as_path())?; - Ok(match view.disposition() { - CommandDisposition::Success => CommandOutput::success(CommandView::LocalBackup(view)), - CommandDisposition::Unconfigured => { - CommandOutput::unconfigured(CommandView::LocalBackup(view)) - } - CommandDisposition::ExternalUnavailable => { - CommandOutput::external_unavailable(CommandView::LocalBackup(view)) - } - CommandDisposition::Unsupported => { - CommandOutput::unsupported(CommandView::LocalBackup(view)) - } - CommandDisposition::InternalError => { - CommandOutput::internal_error(CommandView::LocalBackup(view)) - } - }) -} - -pub fn export( - config: &RuntimeConfig, - args: &LocalExportArgs, -) -> Result<CommandOutput, RuntimeError> { - let view = crate::runtime::local::export(config, args.format, args.output.as_path())?; - Ok(match view.disposition() { - CommandDisposition::Success => CommandOutput::success(CommandView::LocalExport(view)), - CommandDisposition::Unconfigured => { - CommandOutput::unconfigured(CommandView::LocalExport(view)) - } - CommandDisposition::ExternalUnavailable => { - CommandOutput::external_unavailable(CommandView::LocalExport(view)) - } - CommandDisposition::Unsupported => { - CommandOutput::unsupported(CommandView::LocalExport(view)) - } - CommandDisposition::InternalError => { - CommandOutput::internal_error(CommandView::LocalExport(view)) - } - }) -} diff --git a/src/commands/market.rs b/src/commands/market.rs @@ -1,163 +0,0 @@ -use radroots_events::kinds::KIND_LISTING; -use radroots_events_codec::trade::RadrootsTradeListingAddress; - -use crate::cli::{FindArgs, RecordKeyArgs}; -use crate::domain::runtime::{ - CommandDisposition, CommandOutput, CommandView, FindView, ListingGetView, SyncActionView, -}; -use crate::runtime::RuntimeError; -use crate::runtime::config::RuntimeConfig; - -pub fn update(config: &RuntimeConfig) -> Result<CommandOutput, RuntimeError> { - let view = market_update_view(crate::runtime::sync::pull(config)?); - Ok(market_update_output(view)) -} - -pub fn search(config: &RuntimeConfig, args: &FindArgs) -> Result<CommandOutput, RuntimeError> { - let view = market_search_view(crate::runtime::find::search(config, args)?); - Ok(market_search_output(view)) -} - -pub fn view(config: &RuntimeConfig, args: &RecordKeyArgs) -> Result<CommandOutput, RuntimeError> { - let view = market_view_view(crate::runtime::listing::get(config, args)?); - Ok(market_view_output(view)) -} - -fn market_update_view(mut view: SyncActionView) -> SyncActionView { - view.actions = match view.state.as_str() { - "ready" => vec!["radroots market search tomatoes".to_owned()], - "unavailable" => vec![ - "radroots rpc status".to_owned(), - "radroots runtime status radrootsd".to_owned(), - "radroots sync status".to_owned(), - ], - "unconfigured" => { - let mut actions = Vec::new(); - if view.replica_db == "missing" { - actions.push("radroots local init".to_owned()); - } - if view.relay_count == 0 { - actions.push("radroots relay list --relay wss://relay.example.com".to_owned()); - } - if actions.is_empty() { - actions.extend(view.actions.clone()); - } - actions - } - _ => view.actions.clone(), - }; - view -} - -fn market_search_view(mut view: FindView) -> FindView { - view.actions = match view.state.as_str() { - "ready" => view - .results - .first() - .map(|result| { - let mut actions = vec![format!("radroots market view {}", result.product_key)]; - if listing_addr_can_back_order(result.listing_addr.as_deref()) { - actions.push(format!( - "radroots order create --listing {}", - result.product_key - )); - } - actions - }) - .unwrap_or_default(), - "empty" => vec![ - "radroots market update".to_owned(), - "radroots market search eggs".to_owned(), - ], - _ => view.actions.clone(), - }; - view -} - -fn market_view_view(mut view: ListingGetView) -> ListingGetView { - view.actions = match view.state.as_str() { - "ready" => { - let listing_key = view - .product_key - .as_deref() - .unwrap_or(view.lookup.as_str()) - .to_owned(); - if listing_addr_can_back_order(view.listing_addr.as_deref()) { - vec![format!("radroots order create --listing {listing_key}")] - } else { - Vec::new() - } - } - "missing" => vec![ - "radroots market search tomatoes".to_owned(), - "radroots market update".to_owned(), - ], - "unconfigured" => vec![ - "radroots local init".to_owned(), - "radroots market update".to_owned(), - ], - _ => view.actions.clone(), - }; - view -} - -fn market_update_output(view: SyncActionView) -> CommandOutput { - match view.disposition() { - CommandDisposition::Success => CommandOutput::success(CommandView::MarketUpdate(view)), - CommandDisposition::Unconfigured => { - CommandOutput::unconfigured(CommandView::MarketUpdate(view)) - } - CommandDisposition::ExternalUnavailable => { - CommandOutput::external_unavailable(CommandView::MarketUpdate(view)) - } - CommandDisposition::Unsupported => { - CommandOutput::unsupported(CommandView::MarketUpdate(view)) - } - CommandDisposition::InternalError => { - CommandOutput::internal_error(CommandView::MarketUpdate(view)) - } - } -} - -fn listing_addr_can_back_order(listing_addr: Option<&str>) -> bool { - let Some(listing_addr) = listing_addr else { - return false; - }; - RadrootsTradeListingAddress::parse(listing_addr).is_ok_and(|parsed| parsed.kind == KIND_LISTING) -} - -fn market_search_output(view: FindView) -> CommandOutput { - match view.disposition() { - CommandDisposition::Success => CommandOutput::success(CommandView::MarketSearch(view)), - CommandDisposition::Unconfigured => { - CommandOutput::unconfigured(CommandView::MarketSearch(view)) - } - CommandDisposition::ExternalUnavailable => { - CommandOutput::external_unavailable(CommandView::MarketSearch(view)) - } - CommandDisposition::Unsupported => { - CommandOutput::unsupported(CommandView::MarketSearch(view)) - } - CommandDisposition::InternalError => { - CommandOutput::internal_error(CommandView::MarketSearch(view)) - } - } -} - -fn market_view_output(view: ListingGetView) -> CommandOutput { - match view.disposition() { - CommandDisposition::Success => CommandOutput::success(CommandView::MarketView(view)), - CommandDisposition::Unconfigured => { - CommandOutput::unconfigured(CommandView::MarketView(view)) - } - CommandDisposition::ExternalUnavailable => { - CommandOutput::external_unavailable(CommandView::MarketView(view)) - } - CommandDisposition::Unsupported => { - CommandOutput::unsupported(CommandView::MarketView(view)) - } - CommandDisposition::InternalError => { - CommandOutput::internal_error(CommandView::MarketView(view)) - } - } -} diff --git a/src/commands/mod.rs b/src/commands/mod.rs @@ -1,185 +0,0 @@ -pub mod doctor; -pub mod farm; -pub mod find; -pub mod identity; -pub mod job; -pub mod listing; -pub mod local; -pub mod market; -pub mod myc; -pub mod net; -pub mod order; -pub mod relay; -pub mod rpc; -pub mod runtime; -pub mod sell; -pub mod signer; -pub mod sync; -pub mod workflow; - -use crate::cli::{ - AccountCommand, Command, ConfigCommand, FarmCommand, JobCommand, ListingCommand, LocalCommand, - MarketCommand, MycCommand, NetCommand, OrderCommand, RelayCommand, RpcCommand, RuntimeCommand, - RuntimeConfigCommand, SellCommand, SignerCommand, SignerSessionCommand, SyncCommand, -}; -use crate::domain::runtime::{CommandOutput, CommandView}; -use crate::runtime::RuntimeError; -use crate::runtime::config::RuntimeConfig; -use crate::runtime::logging::LoggingState; - -pub fn dispatch( - command: &Command, - config: &RuntimeConfig, - logging: &LoggingState, -) -> Result<CommandOutput, RuntimeError> { - match command { - Command::Account(account) => match &account.command { - AccountCommand::New => Ok(CommandOutput::success(CommandView::AccountNew( - identity::init(config)?, - ))), - AccountCommand::Import(args) => Ok(CommandOutput::success(CommandView::AccountImport( - identity::import(config, args)?, - ))), - AccountCommand::Whoami => identity::show(config), - AccountCommand::Ls => identity::list(config), - AccountCommand::Use(args) => Ok(CommandOutput::success(CommandView::AccountUse( - identity::use_account(config, args.selector.as_str())?, - ))), - AccountCommand::ClearDefault => Ok(CommandOutput::success( - CommandView::AccountClearDefault(identity::clear_default(config)?), - )), - AccountCommand::Remove(args) => Ok(CommandOutput::success(CommandView::AccountRemove( - identity::remove(config, args.selector.as_str())?, - ))), - }, - Command::Myc(myc) => match &myc.command { - MycCommand::Status => Ok(myc::status(config)), - }, - Command::Config(config_command) => match &config_command.command { - ConfigCommand::Show => Ok(CommandOutput::success(CommandView::ConfigShow( - runtime::show(config, logging)?, - ))), - }, - Command::Signer(signer) => match &signer.command { - SignerCommand::Status => Ok(signer::status(config)), - SignerCommand::Session(session) => match &session.command { - SignerSessionCommand::List => Ok(signer::session_list(config)), - SignerSessionCommand::Show { session_id } => { - Ok(signer::session_show(config, session_id.as_str())) - } - SignerSessionCommand::ConnectBunker { url } => { - Ok(signer::session_connect_bunker(config, url.as_str())) - } - SignerSessionCommand::ConnectNostrconnect { - url, - client_secret_key, - } => Ok(signer::session_connect_nostrconnect( - config, - url.as_str(), - client_secret_key.as_str(), - )), - SignerSessionCommand::PublicKey { session_id } => { - Ok(signer::session_public_key(config, session_id.as_str())) - } - SignerSessionCommand::Authorize { session_id } => { - Ok(signer::session_authorize(config, session_id.as_str())) - } - SignerSessionCommand::RequireAuth { - session_id, - auth_url, - } => Ok(signer::session_require_auth( - config, - session_id.as_str(), - auth_url.as_str(), - )), - SignerSessionCommand::Close { session_id } => { - Ok(signer::session_close(config, session_id.as_str())) - } - }, - }, - Command::Doctor => doctor::report(config, logging), - Command::Farm(farm_command) => match &farm_command.command { - FarmCommand::Init(args) => farm::init(config, args), - FarmCommand::Set(args) => farm::set(config, args), - FarmCommand::Publish(args) => farm::publish(config, args), - FarmCommand::Setup(args) => farm::setup(config, args), - FarmCommand::Status(args) => farm::status(config, args), - FarmCommand::Get(args) => farm::get(config, args), - }, - Command::Find(find_args) => find::search(config, find_args), - Command::Job(job) => match &job.command { - JobCommand::Ls => Ok(job::list(config)), - JobCommand::Get(args) => Ok(job::get(config, args.key.as_str())), - JobCommand::Watch(args) => job::watch(config, args), - }, - Command::Listing(listing) => match &listing.command { - ListingCommand::New(args) => listing::new(config, args), - ListingCommand::Validate(args) => listing::validate(config, args), - ListingCommand::Get(args) => listing::get(config, args), - ListingCommand::Publish(args) => listing::publish(config, args), - ListingCommand::Update(args) => listing::update(config, args), - ListingCommand::Archive(args) => listing::archive(config, args), - }, - Command::Local(local) => match &local.command { - LocalCommand::Init => local::init(config), - LocalCommand::Status => local::status(config), - LocalCommand::Export(args) => local::export(config, args), - LocalCommand::Backup(args) => local::backup(config, args), - }, - Command::Market(market) => match &market.command { - MarketCommand::Update => market::update(config), - MarketCommand::Search(args) => market::search(config, args), - MarketCommand::View(args) => market::view(config, args), - }, - Command::Net(net) => match &net.command { - NetCommand::Status => net::status(config), - }, - Command::Order(order) => match &order.command { - OrderCommand::New(args) => order::new(config, args), - OrderCommand::Get(args) => order::get(config, args), - OrderCommand::Ls => order::list(config), - OrderCommand::Submit(args) => order::submit(config, args), - OrderCommand::Watch(args) => order::watch(config, args), - OrderCommand::Cancel(args) => order::cancel(config, args), - OrderCommand::History => order::history(config), - }, - Command::Relay(relay) => match &relay.command { - RelayCommand::Ls => Ok(relay::list(config)), - }, - Command::Rpc(rpc) => match &rpc.command { - RpcCommand::Status => Ok(rpc::status(config)), - RpcCommand::Sessions => Ok(rpc::sessions(config)), - }, - Command::Sell(sell) => match &sell.command { - SellCommand::Add(args) => sell::add(config, args), - SellCommand::Show(args) => sell::show(config, args), - SellCommand::Check(args) => sell::check(config, args), - SellCommand::Publish(args) => sell::publish(config, args), - SellCommand::Update(args) => sell::update(config, args), - SellCommand::Pause(args) => sell::pause(config, args), - SellCommand::Reprice(args) => sell::reprice(config, args), - SellCommand::Restock(args) => sell::restock(config, args), - }, - Command::Setup(setup) => workflow::setup(config, setup), - Command::Runtime(runtime_command) => match &runtime_command.command { - RuntimeCommand::Install(args) => runtime::install(config, args), - RuntimeCommand::Uninstall(args) => runtime::uninstall(config, args), - RuntimeCommand::Status(args) => runtime::status(config, args), - RuntimeCommand::Start(args) => runtime::start(config, args), - RuntimeCommand::Stop(args) => runtime::stop(config, args), - RuntimeCommand::Restart(args) => runtime::restart(config, args), - RuntimeCommand::Logs(args) => runtime::logs(config, args), - RuntimeCommand::Config(runtime_config) => match &runtime_config.command { - RuntimeConfigCommand::Show(args) => runtime::config_show(config, logging, args), - RuntimeConfigCommand::Set(args) => runtime::config_set(config, args), - }, - }, - Command::Status => workflow::status(config), - Command::Sync(sync) => match &sync.command { - SyncCommand::Status => sync::status(config), - SyncCommand::Pull => sync::pull(config), - SyncCommand::Push => sync::push(config), - SyncCommand::Watch(args) => sync::watch(config, args), - }, - } -} diff --git a/src/commands/myc.rs b/src/commands/myc.rs @@ -1,19 +0,0 @@ -use crate::domain::runtime::{CommandDisposition, CommandOutput, CommandView, MycStatusView}; -use crate::runtime::config::RuntimeConfig; - -pub fn status(config: &RuntimeConfig) -> CommandOutput { - let view: MycStatusView = crate::runtime::myc::resolve_status(&config.myc); - match view.disposition() { - CommandDisposition::Success => CommandOutput::success(CommandView::MycStatus(view)), - CommandDisposition::Unconfigured => { - CommandOutput::unconfigured(CommandView::MycStatus(view)) - } - CommandDisposition::ExternalUnavailable => { - CommandOutput::external_unavailable(CommandView::MycStatus(view)) - } - CommandDisposition::Unsupported => CommandOutput::unsupported(CommandView::MycStatus(view)), - CommandDisposition::InternalError => { - CommandOutput::internal_error(CommandView::MycStatus(view)) - } - } -} diff --git a/src/commands/net.rs b/src/commands/net.rs @@ -1,21 +0,0 @@ -use crate::domain::runtime::{CommandDisposition, CommandOutput, CommandView}; -use crate::runtime::RuntimeError; -use crate::runtime::config::RuntimeConfig; -use crate::runtime::network; - -pub fn status(config: &RuntimeConfig) -> Result<CommandOutput, RuntimeError> { - let view = network::net_status(config)?; - Ok(match view.disposition() { - CommandDisposition::Success => CommandOutput::success(CommandView::NetStatus(view)), - CommandDisposition::Unconfigured => { - CommandOutput::unconfigured(CommandView::NetStatus(view)) - } - CommandDisposition::ExternalUnavailable => { - CommandOutput::external_unavailable(CommandView::NetStatus(view)) - } - CommandDisposition::Unsupported => CommandOutput::unsupported(CommandView::NetStatus(view)), - CommandDisposition::InternalError => { - CommandOutput::internal_error(CommandView::NetStatus(view)) - } - }) -} diff --git a/src/commands/order.rs b/src/commands/order.rs @@ -1,148 +0,0 @@ -use crate::cli::{OrderNewArgs, OrderSubmitArgs, OrderWatchArgs, RecordKeyArgs}; -use crate::domain::runtime::{ - CommandDisposition, CommandOutput, CommandView, OrderSubmitView, OrderSubmitWatchView, -}; -use crate::runtime::RuntimeError; -use crate::runtime::config::{ - CapabilityBindingTargetKind, OutputFormat, RuntimeConfig, WRITE_PLANE_TRADE_JSONRPC_CAPABILITY, -}; - -pub fn new(config: &RuntimeConfig, args: &OrderNewArgs) -> Result<CommandOutput, RuntimeError> { - let mut view = crate::runtime::order::scaffold(config, args)?; - rewrite_order_actions(&mut view.actions); - Ok(command_output( - view.disposition(), - CommandView::OrderNew(view), - )) -} - -pub fn get(config: &RuntimeConfig, args: &RecordKeyArgs) -> Result<CommandOutput, RuntimeError> { - let mut view = crate::runtime::order::get(config, args)?; - rewrite_order_actions(&mut view.actions); - Ok(command_output( - view.disposition(), - CommandView::OrderGet(view), - )) -} - -pub fn list(config: &RuntimeConfig) -> Result<CommandOutput, RuntimeError> { - let mut view = crate::runtime::order::list(config)?; - rewrite_order_actions(&mut view.actions); - Ok(command_output( - view.disposition(), - CommandView::OrderList(view), - )) -} - -pub fn submit( - config: &RuntimeConfig, - args: &OrderSubmitArgs, -) -> Result<CommandOutput, RuntimeError> { - let mut view = crate::runtime::order::submit(config, args)?; - rewrite_order_actions(&mut view.actions); - - if args.watch - && config.output.format == OutputFormat::Human - && should_watch_submitted_order(&view) - { - let watch_config = watch_runtime_config(config); - let mut watch = crate::runtime::order::watch( - &watch_config, - &OrderWatchArgs { - key: view.order_id.clone(), - frames: None, - interval_ms: 1_000, - }, - )?; - rewrite_order_actions(&mut watch.actions); - let combined = OrderSubmitWatchView { - submit: view, - watch, - }; - return Ok(command_output( - combined.disposition(), - CommandView::OrderSubmitWatch(combined), - )); - } - - Ok(command_output( - view.disposition(), - CommandView::OrderSubmit(view), - )) -} - -pub fn watch(config: &RuntimeConfig, args: &OrderWatchArgs) -> Result<CommandOutput, RuntimeError> { - let mut view = crate::runtime::order::watch(config, args)?; - rewrite_order_actions(&mut view.actions); - Ok(command_output( - view.disposition(), - CommandView::OrderWatch(view), - )) -} - -pub fn cancel(config: &RuntimeConfig, args: &RecordKeyArgs) -> Result<CommandOutput, RuntimeError> { - let mut view = crate::runtime::order::cancel(config, args)?; - rewrite_order_actions(&mut view.actions); - Ok(command_output( - view.disposition(), - CommandView::OrderCancel(view), - )) -} - -pub fn history(config: &RuntimeConfig) -> Result<CommandOutput, RuntimeError> { - let mut view = crate::runtime::order::history(config)?; - rewrite_order_actions(&mut view.actions); - Ok(command_output( - view.disposition(), - CommandView::OrderHistory(view), - )) -} - -fn should_watch_submitted_order(view: &OrderSubmitView) -> bool { - !matches!( - view.state.as_str(), - "dry_run" | "error" | "missing" | "unavailable" | "unconfigured" - ) && view - .job - .as_ref() - .is_some_and(|job| job.job_id.as_str() != "not_submitted") -} - -fn watch_runtime_config(config: &RuntimeConfig) -> RuntimeConfig { - let mut watch_config = config.clone(); - if let Some(binding) = config.capability_binding(WRITE_PLANE_TRADE_JSONRPC_CAPABILITY) { - if binding.target_kind == CapabilityBindingTargetKind::ExplicitEndpoint { - watch_config.rpc.url = binding.target.clone(); - } - } - watch_config -} - -fn rewrite_order_actions(actions: &mut Vec<String>) { - for action in actions { - *action = rewrite_order_action(action.as_str()); - } -} - -fn rewrite_order_action(action: &str) -> String { - if action == "radroots order new" { - return "radroots order create".to_owned(); - } - if action == "radroots order ls" { - return "radroots order list".to_owned(); - } - if let Some(key) = action.strip_prefix("radroots order get ") { - return format!("radroots order view {key}"); - } - action.to_owned() -} - -fn command_output(disposition: CommandDisposition, view: CommandView) -> CommandOutput { - match disposition { - CommandDisposition::Success => CommandOutput::success(view), - CommandDisposition::Unconfigured => CommandOutput::unconfigured(view), - CommandDisposition::ExternalUnavailable => CommandOutput::external_unavailable(view), - CommandDisposition::Unsupported => CommandOutput::unsupported(view), - CommandDisposition::InternalError => CommandOutput::internal_error(view), - } -} diff --git a/src/commands/relay.rs b/src/commands/relay.rs @@ -1,20 +0,0 @@ -use crate::domain::runtime::{CommandDisposition, CommandOutput, CommandView}; -use crate::runtime::config::RuntimeConfig; -use crate::runtime::network; - -pub fn list(config: &RuntimeConfig) -> CommandOutput { - let view = network::relay_list(config); - match view.disposition() { - CommandDisposition::Success => CommandOutput::success(CommandView::RelayList(view)), - CommandDisposition::Unconfigured => { - CommandOutput::unconfigured(CommandView::RelayList(view)) - } - CommandDisposition::ExternalUnavailable => { - CommandOutput::external_unavailable(CommandView::RelayList(view)) - } - CommandDisposition::Unsupported => CommandOutput::unsupported(CommandView::RelayList(view)), - CommandDisposition::InternalError => { - CommandOutput::internal_error(CommandView::RelayList(view)) - } - } -} diff --git a/src/commands/rpc.rs b/src/commands/rpc.rs @@ -1,10 +0,0 @@ -use crate::domain::runtime::CommandOutput; -use crate::runtime::config::RuntimeConfig; - -pub fn status(config: &RuntimeConfig) -> CommandOutput { - crate::runtime::daemon::status(config) -} - -pub fn sessions(config: &RuntimeConfig) -> CommandOutput { - crate::runtime::daemon::sessions(config) -} diff --git a/src/commands/runtime.rs b/src/commands/runtime.rs @@ -1,388 +0,0 @@ -use crate::cli::{RuntimeConfigSetArgs, RuntimeTargetArgs}; -use crate::domain::runtime::{ - AccountRuntimeView, AccountSecretRuntimeView, CapabilityBindingRuntimeView, CommandOutput, - CommandView, ConfigFilesRuntimeView, ConfigShowView, HyfProviderRuntimeView, HyfRuntimeView, - InteractionRuntimeView, LegacyPathRuntimeView, LocalRuntimeView, LoggingRuntimeView, - MigrationRuntimeView, MycRuntimeView, OutputRuntimeView, PathsRuntimeView, RelayRuntimeView, - ResolvedProviderRuntimeView, RpcRuntimeView, SignerRuntimeView, WorkflowRuntimeView, - WritePlaneRuntimeView, -}; -use crate::runtime::RuntimeError; -use crate::runtime::config::RuntimeConfig; -use crate::runtime::logging::LoggingState; -use crate::runtime::management::{ - RuntimeCommandAvailability, RuntimeConfigMutationRequest, RuntimeLifecycleAction, - inspect_action, inspect_config_set, inspect_config_show, inspect_logs, inspect_status, -}; -use crate::runtime::provider::{ - resolve_capability_providers, resolve_hyf_provider, resolve_workflow_provider, - resolve_write_plane_provider, -}; - -pub fn show( - config: &RuntimeConfig, - logging: &LoggingState, -) -> Result<ConfigShowView, RuntimeError> { - let secret_backend = crate::runtime::accounts::secret_backend_status(config); - let write_plane = resolve_write_plane_provider(config); - let workflow = resolve_workflow_provider(config); - let hyf_provider = resolve_hyf_provider(config); - let resolved_providers = resolve_capability_providers(config); - Ok(ConfigShowView { - source: "local runtime state".to_owned(), - output: OutputRuntimeView { - format: config.output.format.as_str().to_owned(), - verbosity: config.output.verbosity.as_str().to_owned(), - color: config.output.color, - dry_run: config.output.dry_run, - }, - interaction: InteractionRuntimeView { - input_enabled: config.interaction.input_enabled, - assume_yes: config.interaction.assume_yes, - stdin_tty: config.interaction.stdin_tty, - stdout_tty: config.interaction.stdout_tty, - prompts_allowed: config.interaction.prompts_allowed, - confirmations_allowed: config.interaction.confirmations_allowed, - }, - config_files: ConfigFilesRuntimeView { - user_present: config.paths.app_config_path.exists(), - workspace_present: config - .paths - .workspace_config_path - .as_ref() - .is_some_and(|path| path.exists()), - }, - paths: PathsRuntimeView { - profile: config.paths.profile.clone(), - profile_source: config.paths.profile_source.clone(), - allowed_profiles: config.paths.allowed_profiles.clone(), - root_source: config.paths.root_source.clone(), - repo_local_root: config - .paths - .repo_local_root - .as_ref() - .map(|path| path.display().to_string()), - repo_local_root_source: config.paths.repo_local_root_source.clone(), - subordinate_path_override_source: config.paths.subordinate_path_override_source.clone(), - app_namespace: config.paths.app_namespace.clone(), - shared_accounts_namespace: config.paths.shared_accounts_namespace.clone(), - shared_identities_namespace: config.paths.shared_identities_namespace.clone(), - app_config_path: config.paths.app_config_path.display().to_string(), - workspace_config_enabled: config.paths.workspace_config_path.is_some(), - workspace_config_path: config - .paths - .workspace_config_path - .as_ref() - .map(|path| path.display().to_string()), - app_data_root: config.paths.app_data_root.display().to_string(), - app_logs_root: config.paths.app_logs_root.display().to_string(), - shared_accounts_data_root: config.paths.shared_accounts_data_root.display().to_string(), - shared_accounts_secrets_root: config - .paths - .shared_accounts_secrets_root - .display() - .to_string(), - default_identity_path: config.paths.default_identity_path.display().to_string(), - }, - migration: migration_runtime_view(config), - logging: LoggingRuntimeView { - initialized: logging.initialized, - filter: config.logging.filter.clone(), - stdout: config.logging.stdout, - directory: config - .logging - .directory - .as_ref() - .map(|path| path.display().to_string()), - current_file: logging - .current_file - .as_ref() - .map(|path| path.display().to_string()), - }, - account: AccountRuntimeView { - selector: config.account.selector.clone(), - store_path: config.account.store_path.display().to_string(), - secrets_dir: config.account.secrets_dir.display().to_string(), - identity_path: config.identity.path.display().to_string(), - secret_backend: AccountSecretRuntimeView { - contract_default_backend: config.account_secret_contract.default_backend.clone(), - contract_default_fallback: config.account_secret_contract.default_fallback.clone(), - allowed_backends: config.account_secret_contract.allowed_backends.clone(), - host_vault_policy: config.account_secret_contract.host_vault_policy.clone(), - uses_protected_store: config.account_secret_contract.uses_protected_store, - configured_primary: secret_backend.configured_primary, - configured_fallback: secret_backend.configured_fallback, - state: secret_backend.state, - active_backend: secret_backend.active_backend, - used_fallback: secret_backend.used_fallback, - reason: secret_backend.reason, - }, - }, - signer: SignerRuntimeView { - mode: config.signer.backend.as_str().to_owned(), - }, - relay: RelayRuntimeView { - count: config.relay.urls.len(), - urls: config.relay.urls.clone(), - publish_policy: config.relay.publish_policy.as_str().to_owned(), - source: config.relay.source.as_str().to_owned(), - }, - local: LocalRuntimeView { - root: config.local.root.display().to_string(), - replica_db_path: config.local.replica_db_path.display().to_string(), - backups_dir: config.local.backups_dir.display().to_string(), - exports_dir: config.local.exports_dir.display().to_string(), - }, - myc: MycRuntimeView { - executable: config.myc.executable.display().to_string(), - status_timeout_ms: config.myc.status_timeout_ms, - }, - write_plane: WritePlaneRuntimeView { - provider_runtime_id: write_plane.provider_runtime_id, - binding_model: write_plane.binding_model, - state: write_plane.state, - provenance: write_plane.provenance, - source: write_plane.source, - target_kind: write_plane.target_kind, - target: write_plane.target, - detail: write_plane.detail, - bridge_auth_configured: write_plane.bridge_auth_configured, - }, - workflow: WorkflowRuntimeView { - provider_runtime_id: workflow.provider_runtime_id, - binding_model: workflow.binding_model, - state: workflow.state, - provenance: workflow.provenance, - source: workflow.source, - target_kind: workflow.target_kind, - target: workflow.target, - hyf_helper_state: workflow.hyf_helper_state, - hyf_helper_detail: workflow.hyf_helper_detail, - }, - hyf_provider: HyfProviderRuntimeView { - provider_runtime_id: hyf_provider.provider_runtime_id, - binding_model: hyf_provider.binding_model, - state: hyf_provider.state, - provenance: hyf_provider.provenance, - source: hyf_provider.source, - target_kind: hyf_provider.target_kind, - target: hyf_provider.target, - executable: hyf_provider.executable, - reason: hyf_provider.reason, - protocol_version: hyf_provider.protocol_version, - deterministic_available: hyf_provider.deterministic_available, - }, - hyf: HyfRuntimeView { - enabled: config.hyf.enabled, - executable: config.hyf.executable.display().to_string(), - }, - rpc: RpcRuntimeView { - url: config.rpc.url.clone(), - bridge_auth_configured: config.rpc.bridge_bearer_token.is_some(), - }, - resolved_providers: resolved_providers - .into_iter() - .map(|provider| ResolvedProviderRuntimeView { - capability_id: provider.capability_id, - provider_runtime_id: provider.provider_runtime_id, - binding_model: provider.binding_model, - state: provider.state, - provenance: provider.provenance, - source: provider.source, - target_kind: provider.target_kind, - target: provider.target, - }) - .collect(), - capability_bindings: config - .inspect_capability_bindings() - .into_iter() - .map(|binding| CapabilityBindingRuntimeView { - capability_id: binding.capability_id, - provider_runtime_id: binding.provider_runtime_id, - binding_model: binding.binding_model, - state: binding.state.as_str().to_owned(), - source: binding.source, - target_kind: binding.target_kind, - target: binding.target, - managed_account_ref: binding.managed_account_ref, - signer_session_ref: binding.signer_session_ref, - }) - .collect(), - }) -} - -pub fn status( - config: &RuntimeConfig, - args: &RuntimeTargetArgs, -) -> Result<CommandOutput, RuntimeError> { - let inspection = inspect_status(config, args.runtime.as_str(), args.instance.as_deref())?; - Ok(command_output( - inspection.availability, - CommandView::RuntimeStatus(inspection.view), - )) -} - -pub fn install( - config: &RuntimeConfig, - args: &RuntimeTargetArgs, -) -> Result<CommandOutput, RuntimeError> { - let inspection = inspect_action( - config, - args.runtime.as_str(), - args.instance.as_deref(), - RuntimeLifecycleAction::Install, - )?; - Ok(command_output( - inspection.availability, - CommandView::RuntimeAction(inspection.view), - )) -} - -pub fn uninstall( - config: &RuntimeConfig, - args: &RuntimeTargetArgs, -) -> Result<CommandOutput, RuntimeError> { - let inspection = inspect_action( - config, - args.runtime.as_str(), - args.instance.as_deref(), - RuntimeLifecycleAction::Uninstall, - )?; - Ok(command_output( - inspection.availability, - CommandView::RuntimeAction(inspection.view), - )) -} - -pub fn start( - config: &RuntimeConfig, - args: &RuntimeTargetArgs, -) -> Result<CommandOutput, RuntimeError> { - let inspection = inspect_action( - config, - args.runtime.as_str(), - args.instance.as_deref(), - RuntimeLifecycleAction::Start, - )?; - Ok(command_output( - inspection.availability, - CommandView::RuntimeAction(inspection.view), - )) -} - -pub fn stop( - config: &RuntimeConfig, - args: &RuntimeTargetArgs, -) -> Result<CommandOutput, RuntimeError> { - let inspection = inspect_action( - config, - args.runtime.as_str(), - args.instance.as_deref(), - RuntimeLifecycleAction::Stop, - )?; - Ok(command_output( - inspection.availability, - CommandView::RuntimeAction(inspection.view), - )) -} - -pub fn restart( - config: &RuntimeConfig, - args: &RuntimeTargetArgs, -) -> Result<CommandOutput, RuntimeError> { - let inspection = inspect_action( - config, - args.runtime.as_str(), - args.instance.as_deref(), - RuntimeLifecycleAction::Restart, - )?; - Ok(command_output( - inspection.availability, - CommandView::RuntimeAction(inspection.view), - )) -} - -pub fn logs( - config: &RuntimeConfig, - args: &RuntimeTargetArgs, -) -> Result<CommandOutput, RuntimeError> { - let inspection = inspect_logs(config, args.runtime.as_str(), args.instance.as_deref())?; - Ok(command_output( - inspection.availability, - CommandView::RuntimeLogs(inspection.view), - )) -} - -pub fn config_show( - config: &RuntimeConfig, - _logging: &LoggingState, - args: &RuntimeTargetArgs, -) -> Result<CommandOutput, RuntimeError> { - let inspection = inspect_config_show(config, args.runtime.as_str(), args.instance.as_deref())?; - Ok(command_output( - inspection.availability, - CommandView::RuntimeConfigShow(inspection.view), - )) -} - -pub fn config_set( - config: &RuntimeConfig, - args: &RuntimeConfigSetArgs, -) -> Result<CommandOutput, RuntimeError> { - let inspection = inspect_config_set( - config, - &RuntimeConfigMutationRequest { - runtime_id: args.target.runtime.clone(), - instance_id: args.target.instance.clone(), - key: args.key.clone(), - value: args.value.clone(), - }, - )?; - Ok(command_output( - inspection.availability, - CommandView::RuntimeAction(inspection.view), - )) -} - -fn migration_runtime_view(config: &RuntimeConfig) -> MigrationRuntimeView { - let report = &config.migration.report; - let detected_legacy_paths = report - .detected_legacy_paths - .iter() - .map(|path| LegacyPathRuntimeView { - id: path.id.clone(), - description: path.description.clone(), - path: path.path.display().to_string(), - destination: path - .destination - .as_ref() - .map(|destination| destination.display().to_string()), - import_hint: path.import_hint.clone(), - }) - .collect::<Vec<_>>(); - let actions = if detected_legacy_paths.is_empty() { - Vec::new() - } else { - vec![ - "inspect detected_legacy_paths before writing new local state".to_owned(), - "perform an explicit export/import or manual copy; startup did not move legacy data" - .to_owned(), - ] - }; - MigrationRuntimeView { - posture: report.posture.to_owned(), - state: report.state.to_owned(), - silent_startup_relocation: report.silent_startup_relocation, - compatibility_window: report.compatibility_window.to_owned(), - detected_legacy_paths, - actions, - } -} - -fn command_output(availability: RuntimeCommandAvailability, view: CommandView) -> CommandOutput { - match availability { - RuntimeCommandAvailability::Success => CommandOutput::success(view), - RuntimeCommandAvailability::Unconfigured => CommandOutput::unconfigured(view), - RuntimeCommandAvailability::Unsupported => CommandOutput::unsupported(view), - } -} diff --git a/src/commands/sell.rs b/src/commands/sell.rs @@ -1,150 +0,0 @@ -use crate::cli::{ - ListingFileArgs, ListingMutationArgs, SellAddArgs, SellRepriceArgs, SellRestockArgs, - SellShowArgs, -}; -use crate::domain::runtime::{ - CommandDisposition, CommandOutput, CommandView, SellAddView, SellCheckView, - SellDraftMutationView, SellMutationView, SellShowView, -}; -use crate::runtime::RuntimeError; -use crate::runtime::config::RuntimeConfig; - -pub fn add(config: &RuntimeConfig, args: &SellAddArgs) -> Result<CommandOutput, RuntimeError> { - let view = crate::runtime::listing::sell_add(config, args)?; - Ok(sell_add_output(view)) -} - -pub fn show(config: &RuntimeConfig, args: &SellShowArgs) -> Result<CommandOutput, RuntimeError> { - let view = crate::runtime::listing::sell_show(config, args)?; - Ok(sell_show_output(view)) -} - -pub fn check( - config: &RuntimeConfig, - args: &ListingFileArgs, -) -> Result<CommandOutput, RuntimeError> { - let view = crate::runtime::listing::sell_check(config, args)?; - Ok(sell_check_output(view)) -} - -pub fn publish( - config: &RuntimeConfig, - args: &ListingMutationArgs, -) -> Result<CommandOutput, RuntimeError> { - let view = crate::runtime::listing::sell_publish(config, args)?; - Ok(sell_mutation_output(view)) -} - -pub fn update( - config: &RuntimeConfig, - args: &ListingMutationArgs, -) -> Result<CommandOutput, RuntimeError> { - let view = crate::runtime::listing::sell_update(config, args)?; - Ok(sell_mutation_output(view)) -} - -pub fn pause( - config: &RuntimeConfig, - args: &ListingMutationArgs, -) -> Result<CommandOutput, RuntimeError> { - let view = crate::runtime::listing::sell_pause(config, args)?; - Ok(sell_mutation_output(view)) -} - -pub fn reprice( - config: &RuntimeConfig, - args: &SellRepriceArgs, -) -> Result<CommandOutput, RuntimeError> { - let view = crate::runtime::listing::sell_reprice(config, args)?; - Ok(sell_draft_mutation_output(view)) -} - -pub fn restock( - config: &RuntimeConfig, - args: &SellRestockArgs, -) -> Result<CommandOutput, RuntimeError> { - let view = crate::runtime::listing::sell_restock(config, args)?; - Ok(sell_draft_mutation_output(view)) -} - -fn sell_add_output(view: SellAddView) -> CommandOutput { - match view.disposition() { - CommandDisposition::Success => CommandOutput::success(CommandView::SellAdd(view)), - CommandDisposition::Unconfigured => CommandOutput::unconfigured(CommandView::SellAdd(view)), - CommandDisposition::ExternalUnavailable => { - CommandOutput::external_unavailable(CommandView::SellAdd(view)) - } - CommandDisposition::Unsupported => CommandOutput::unsupported(CommandView::SellAdd(view)), - CommandDisposition::InternalError => { - CommandOutput::internal_error(CommandView::SellAdd(view)) - } - } -} - -fn sell_show_output(view: SellShowView) -> CommandOutput { - match view.disposition() { - CommandDisposition::Success => CommandOutput::success(CommandView::SellShow(view)), - CommandDisposition::Unconfigured => { - CommandOutput::unconfigured(CommandView::SellShow(view)) - } - CommandDisposition::ExternalUnavailable => { - CommandOutput::external_unavailable(CommandView::SellShow(view)) - } - CommandDisposition::Unsupported => CommandOutput::unsupported(CommandView::SellShow(view)), - CommandDisposition::InternalError => { - CommandOutput::internal_error(CommandView::SellShow(view)) - } - } -} - -fn sell_check_output(view: SellCheckView) -> CommandOutput { - match view.disposition() { - CommandDisposition::Success => CommandOutput::success(CommandView::SellCheck(view)), - CommandDisposition::Unconfigured => { - CommandOutput::unconfigured(CommandView::SellCheck(view)) - } - CommandDisposition::ExternalUnavailable => { - CommandOutput::external_unavailable(CommandView::SellCheck(view)) - } - CommandDisposition::Unsupported => CommandOutput::unsupported(CommandView::SellCheck(view)), - CommandDisposition::InternalError => { - CommandOutput::internal_error(CommandView::SellCheck(view)) - } - } -} - -fn sell_mutation_output(view: SellMutationView) -> CommandOutput { - match view.disposition() { - CommandDisposition::Success => CommandOutput::success(CommandView::SellMutation(view)), - CommandDisposition::Unconfigured => { - CommandOutput::unconfigured(CommandView::SellMutation(view)) - } - CommandDisposition::ExternalUnavailable => { - CommandOutput::external_unavailable(CommandView::SellMutation(view)) - } - CommandDisposition::Unsupported => { - CommandOutput::unsupported(CommandView::SellMutation(view)) - } - CommandDisposition::InternalError => { - CommandOutput::internal_error(CommandView::SellMutation(view)) - } - } -} - -fn sell_draft_mutation_output(view: SellDraftMutationView) -> CommandOutput { - match view.disposition() { - CommandDisposition::Success => CommandOutput::success(CommandView::SellDraftMutation(view)), - CommandDisposition::Unconfigured => { - CommandOutput::unconfigured(CommandView::SellDraftMutation(view)) - } - CommandDisposition::ExternalUnavailable => { - CommandOutput::external_unavailable(CommandView::SellDraftMutation(view)) - } - CommandDisposition::Unsupported => { - CommandOutput::unsupported(CommandView::SellDraftMutation(view)) - } - CommandDisposition::InternalError => { - CommandOutput::internal_error(CommandView::SellDraftMutation(view)) - } - } -} diff --git a/src/commands/signer.rs b/src/commands/signer.rs @@ -1,163 +0,0 @@ -use crate::domain::runtime::{ - CommandDisposition, CommandOutput, CommandView, SignerSessionActionView, SignerStatusView, -}; -use crate::runtime::config::RuntimeConfig; -use crate::runtime::daemon::DaemonRpcError; -use crate::runtime::signer::resolve_signer_status; - -pub fn status(config: &RuntimeConfig) -> CommandOutput { - let view: SignerStatusView = resolve_signer_status(config); - match view.disposition() { - CommandDisposition::Success => CommandOutput::success(CommandView::SignerStatus(view)), - CommandDisposition::Unconfigured => { - CommandOutput::unconfigured(CommandView::SignerStatus(view)) - } - CommandDisposition::ExternalUnavailable => { - CommandOutput::external_unavailable(CommandView::SignerStatus(view)) - } - CommandDisposition::Unsupported => { - CommandOutput::unsupported(CommandView::SignerStatus(view)) - } - CommandDisposition::InternalError => { - CommandOutput::internal_error(CommandView::SignerStatus(view)) - } - } -} - -pub fn session_list(config: &RuntimeConfig) -> CommandOutput { - crate::runtime::daemon::signer_sessions(config) -} - -pub fn session_show(config: &RuntimeConfig, session_id: &str) -> CommandOutput { - session_action_output( - "show", - crate::runtime::daemon::signer_session_show(config, session_id), - ) -} - -pub fn session_connect_bunker(config: &RuntimeConfig, url: &str) -> CommandOutput { - session_action_output( - "connect_bunker", - crate::runtime::daemon::signer_session_connect_bunker(config, url), - ) -} - -pub fn session_connect_nostrconnect( - config: &RuntimeConfig, - url: &str, - client_secret_key: &str, -) -> CommandOutput { - session_action_output( - "connect_nostrconnect", - crate::runtime::daemon::signer_session_connect_nostrconnect(config, url, client_secret_key), - ) -} - -pub fn session_public_key(config: &RuntimeConfig, session_id: &str) -> CommandOutput { - session_action_output( - "public_key", - crate::runtime::daemon::signer_session_public_key(config, session_id), - ) -} - -pub fn session_authorize(config: &RuntimeConfig, session_id: &str) -> CommandOutput { - session_action_output( - "authorize", - crate::runtime::daemon::signer_session_authorize(config, session_id), - ) -} - -pub fn session_require_auth( - config: &RuntimeConfig, - session_id: &str, - auth_url: &str, -) -> CommandOutput { - session_action_output( - "require_auth", - crate::runtime::daemon::signer_session_require_auth(config, session_id, auth_url), - ) -} - -pub fn session_close(config: &RuntimeConfig, session_id: &str) -> CommandOutput { - session_action_output( - "close", - crate::runtime::daemon::signer_session_close(config, session_id), - ) -} - -fn session_action_output( - action: &str, - result: Result<SignerSessionActionView, DaemonRpcError>, -) -> CommandOutput { - match result { - Ok(view) => CommandOutput::success(CommandView::SignerSessionAction(view)), - Err(error) => { - let (disposition, view) = session_action_error_view(action, error); - match disposition { - CommandDisposition::Unconfigured => { - CommandOutput::unconfigured(CommandView::SignerSessionAction(view)) - } - CommandDisposition::ExternalUnavailable => { - CommandOutput::external_unavailable(CommandView::SignerSessionAction(view)) - } - CommandDisposition::Unsupported => { - CommandOutput::unsupported(CommandView::SignerSessionAction(view)) - } - CommandDisposition::InternalError => { - CommandOutput::internal_error(CommandView::SignerSessionAction(view)) - } - CommandDisposition::Success => { - CommandOutput::success(CommandView::SignerSessionAction(view)) - } - } - } - } -} - -fn session_action_error_view( - action: &str, - error: DaemonRpcError, -) -> (CommandDisposition, SignerSessionActionView) { - let (disposition, state, reason) = match error { - DaemonRpcError::Unconfigured(reason) - | DaemonRpcError::Unauthorized(reason) - | DaemonRpcError::MethodUnavailable(reason) => { - (CommandDisposition::Unconfigured, "unconfigured", reason) - } - DaemonRpcError::External(reason) => ( - CommandDisposition::ExternalUnavailable, - "unavailable", - reason, - ), - DaemonRpcError::InvalidResponse(reason) - | DaemonRpcError::Remote(reason) - | DaemonRpcError::UnknownJob(reason) => { - (CommandDisposition::InternalError, "error", reason) - } - }; - ( - disposition, - SignerSessionActionView { - action: action.to_owned(), - state: state.to_owned(), - source: "daemon signer session rpc · durable write plane".to_owned(), - session_id: None, - mode: None, - remote_signer_pubkey: None, - client_pubkey: None, - signer_pubkey: None, - user_pubkey: None, - relays: Vec::new(), - permissions: Vec::new(), - auth_required: None, - authorized: None, - auth_url: None, - expires_in_secs: None, - pubkey: None, - replayed: None, - required: None, - closed: None, - reason: Some(reason), - }, - ) -} diff --git a/src/commands/sync.rs b/src/commands/sync.rs @@ -1,46 +0,0 @@ -use crate::cli::SyncWatchArgs; -use crate::domain::runtime::{CommandDisposition, CommandOutput, CommandView}; -use crate::runtime::RuntimeError; -use crate::runtime::config::RuntimeConfig; - -pub fn status(config: &RuntimeConfig) -> Result<CommandOutput, RuntimeError> { - let view = crate::runtime::sync::status(config)?; - Ok(output_from_disposition( - view.disposition(), - CommandView::SyncStatus(view), - )) -} - -pub fn pull(config: &RuntimeConfig) -> Result<CommandOutput, RuntimeError> { - let view = crate::runtime::sync::pull(config)?; - Ok(output_from_disposition( - view.disposition(), - CommandView::SyncPull(view), - )) -} - -pub fn push(config: &RuntimeConfig) -> Result<CommandOutput, RuntimeError> { - let view = crate::runtime::sync::push(config)?; - Ok(output_from_disposition( - view.disposition(), - CommandView::SyncPush(view), - )) -} - -pub fn watch(config: &RuntimeConfig, args: &SyncWatchArgs) -> Result<CommandOutput, RuntimeError> { - let view = crate::runtime::sync::watch(config, args)?; - Ok(output_from_disposition( - view.disposition(), - CommandView::SyncWatch(view), - )) -} - -fn output_from_disposition(disposition: CommandDisposition, view: CommandView) -> CommandOutput { - match disposition { - CommandDisposition::Success => CommandOutput::success(view), - CommandDisposition::Unconfigured => CommandOutput::unconfigured(view), - CommandDisposition::ExternalUnavailable => CommandOutput::external_unavailable(view), - CommandDisposition::Unsupported => CommandOutput::unsupported(view), - CommandDisposition::InternalError => CommandOutput::internal_error(view), - } -} diff --git a/src/commands/workflow.rs b/src/commands/workflow.rs @@ -1,44 +0,0 @@ -use crate::cli::SetupArgs; -use crate::domain::runtime::{ - CommandDisposition, CommandOutput, CommandView, SetupView, StatusView, -}; -use crate::runtime::RuntimeError; -use crate::runtime::config::RuntimeConfig; - -pub fn setup(config: &RuntimeConfig, args: &SetupArgs) -> Result<CommandOutput, RuntimeError> { - let view = crate::runtime::workflow::setup(config, args.role)?; - Ok(setup_output(view)) -} - -pub fn status(config: &RuntimeConfig) -> Result<CommandOutput, RuntimeError> { - let view = crate::runtime::workflow::status(config)?; - Ok(status_output(view)) -} - -fn setup_output(view: SetupView) -> CommandOutput { - match view.disposition() { - CommandDisposition::Success => CommandOutput::success(CommandView::Setup(view)), - CommandDisposition::Unconfigured => CommandOutput::unconfigured(CommandView::Setup(view)), - CommandDisposition::ExternalUnavailable => { - CommandOutput::external_unavailable(CommandView::Setup(view)) - } - CommandDisposition::Unsupported => CommandOutput::unsupported(CommandView::Setup(view)), - CommandDisposition::InternalError => { - CommandOutput::internal_error(CommandView::Setup(view)) - } - } -} - -fn status_output(view: StatusView) -> CommandOutput { - match view.disposition() { - CommandDisposition::Success => CommandOutput::success(CommandView::Status(view)), - CommandDisposition::Unconfigured => CommandOutput::unconfigured(CommandView::Status(view)), - CommandDisposition::ExternalUnavailable => { - CommandOutput::external_unavailable(CommandView::Status(view)) - } - CommandDisposition::Unsupported => CommandOutput::unsupported(CommandView::Status(view)), - CommandDisposition::InternalError => { - CommandOutput::internal_error(CommandView::Status(view)) - } - } -} diff --git a/src/domain/runtime.rs b/src/domain/runtime.rs @@ -8,57 +8,6 @@ use radroots_events::profile::RadrootsProfile; use radroots_nostr_accounts::prelude::RadrootsNostrAccountRecord; use serde::Serialize; -#[derive(Debug, Clone)] -pub struct CommandOutput { - disposition: CommandDisposition, - view: CommandView, -} - -impl CommandOutput { - pub fn success(view: CommandView) -> Self { - Self { - disposition: CommandDisposition::Success, - view, - } - } - - pub fn unconfigured(view: CommandView) -> Self { - Self { - disposition: CommandDisposition::Unconfigured, - view, - } - } - - pub fn external_unavailable(view: CommandView) -> Self { - Self { - disposition: CommandDisposition::ExternalUnavailable, - view, - } - } - - pub fn unsupported(view: CommandView) -> Self { - Self { - disposition: CommandDisposition::Unsupported, - view, - } - } - - pub fn internal_error(view: CommandView) -> Self { - Self { - disposition: CommandDisposition::InternalError, - view, - } - } - - pub fn exit_code(&self) -> ExitCode { - self.disposition.exit_code() - } - - pub fn view(&self) -> &CommandView { - &self.view - } -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CommandDisposition { Success, @@ -80,69 +29,6 @@ impl CommandDisposition { } } -#[derive(Debug, Clone)] -pub enum CommandView { - AccountClearDefault(AccountClearDefaultView), - AccountImport(AccountImportView), - AccountList(AccountListView), - AccountNew(AccountNewView), - AccountRemove(AccountRemoveView), - AccountUse(AccountUseView), - AccountWhoami(AccountWhoamiView), - ConfigShow(ConfigShowView), - Doctor(DoctorView), - FarmGet(FarmGetView), - FarmPublish(FarmPublishView), - FarmSet(FarmSetView), - FarmSetup(FarmSetupView), - FarmStatus(FarmStatusView), - Find(FindView), - JobGet(JobGetView), - JobList(JobListView), - JobWatch(JobWatchView), - ListingGet(ListingGetView), - ListingMutation(ListingMutationView), - ListingNew(ListingNewView), - ListingValidate(ListingValidateView), - LocalBackup(LocalBackupView), - LocalExport(LocalExportView), - LocalInit(LocalInitView), - LocalStatus(LocalStatusView), - MarketSearch(FindView), - MarketUpdate(SyncActionView), - MarketView(ListingGetView), - MycStatus(MycStatusView), - NetStatus(NetStatusView), - OrderCancel(OrderCancelView), - OrderGet(OrderGetView), - OrderHistory(OrderHistoryView), - OrderList(OrderListView), - OrderNew(OrderNewView), - OrderSubmit(OrderSubmitView), - OrderSubmitWatch(OrderSubmitWatchView), - OrderWatch(OrderWatchView), - RpcSessions(RpcSessionsView), - RpcStatus(RpcStatusView), - RelayList(RelayListView), - RuntimeAction(RuntimeActionView), - RuntimeConfigShow(RuntimeManagedConfigView), - RuntimeLogs(RuntimeLogsView), - RuntimeStatus(RuntimeStatusView), - SellAdd(SellAddView), - SellCheck(SellCheckView), - SellDraftMutation(SellDraftMutationView), - SellMutation(SellMutationView), - SellShow(SellShowView), - Setup(SetupView), - SignerSessionAction(SignerSessionActionView), - SignerStatus(SignerStatusView), - Status(StatusView), - SyncPull(SyncActionView), - SyncPush(SyncActionView), - SyncStatus(SyncStatusView), - SyncWatch(SyncWatchView), -} - #[derive(Debug, Clone, Serialize)] pub struct ConfigShowView { pub source: String, diff --git a/src/main.rs b/src/main.rs @@ -1,6 +1,5 @@ #![forbid(unsafe_code)] -mod cli; mod domain; mod operation_adapter; mod operation_basket; @@ -13,6 +12,7 @@ mod operation_registry; mod operation_runtime; mod output_contract; mod runtime; +mod runtime_args; mod target_cli; use std::io::Write; @@ -20,7 +20,6 @@ use std::process::ExitCode; use clap::Parser; -use crate::cli::{CliArgs, Command, ConfigArgs, ConfigCommand, OutputFormatArg}; use crate::operation_adapter::{ OperationAdapter, OperationAdapterError, OperationNetworkMode, OperationOutputFormat, OperationRequest, OperationRequestPayload, OperationResultPayload, OperationService, @@ -36,6 +35,7 @@ use crate::operation_runtime::RuntimeOperationService; use crate::output_contract::OutputEnvelope; use crate::runtime::config::{RuntimeConfig, SignerBackend}; use crate::runtime::logging::initialize_logging; +use crate::runtime_args::{RuntimeInvocationArgs, RuntimeOutputFormatArg}; use crate::target_cli::{TargetCliArgs, TargetOutputFormat}; fn main() -> ExitCode { @@ -52,7 +52,7 @@ fn run() -> Result<ExitCode, runtime::RuntimeError> { debug_assert!(operation_registry::registry_linkage_is_valid()); debug_assert!(operation_adapter::adapter_registry_linkage_is_valid()); let args = TargetCliArgs::parse(); - let config = RuntimeConfig::from_system(&config_args_from_target(&args)?)?; + let config = RuntimeConfig::from_system(&runtime_args_from_target(&args))?; let logging = initialize_logging(&config.logging)?; let request = TargetOperationRequest::from_target_args(&args).map_err(operation_config_error)?; @@ -64,12 +64,12 @@ fn run() -> Result<ExitCode, runtime::RuntimeError> { Ok(envelope_exit_code(&envelope)) } -fn config_args_from_target(args: &TargetCliArgs) -> Result<CliArgs, runtime::RuntimeError> { - Ok(CliArgs { +fn runtime_args_from_target(args: &TargetCliArgs) -> RuntimeInvocationArgs { + RuntimeInvocationArgs { output_format: Some(match args.format { - TargetOutputFormat::Human => OutputFormatArg::Human, - TargetOutputFormat::Json => OutputFormatArg::Json, - TargetOutputFormat::Ndjson => OutputFormatArg::Ndjson, + TargetOutputFormat::Human => RuntimeOutputFormatArg::Human, + TargetOutputFormat::Json => RuntimeOutputFormatArg::Json, + TargetOutputFormat::Ndjson => RuntimeOutputFormatArg::Ndjson, }), json: false, ndjson: false, @@ -94,10 +94,7 @@ fn config_args_from_target(args: &TargetCliArgs) -> Result<CliArgs, runtime::Run hyf_enabled: false, no_hyf_enabled: false, hyf_executable: None, - command: Command::Config(ConfigArgs { - command: ConfigCommand::Show, - }), - }) + } } fn execute_request( diff --git a/src/operation_basket.rs b/src/operation_basket.rs @@ -1,5 +1,3 @@ -#![allow(dead_code)] - use std::fs; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicU64, Ordering}; @@ -8,7 +6,6 @@ use std::time::{SystemTime, UNIX_EPOCH}; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; -use crate::cli::OrderNewArgs; use crate::domain::runtime::OrderNewView; use crate::operation_adapter::{ BasketCreateRequest, BasketCreateResult, BasketGetRequest, BasketGetResult, @@ -20,6 +17,7 @@ use crate::operation_adapter::{ }; use crate::runtime::RuntimeError; use crate::runtime::config::RuntimeConfig; +use crate::runtime_args::OrderDraftCreateArgs; const BASKET_KIND: &str = "basket_v1"; const BASKET_SOURCE: &str = "local baskets - local first"; @@ -373,7 +371,7 @@ impl OperationService<BasketQuoteCreateRequest> for BasketOperationService<'_> { let order = map_runtime(crate::runtime::order::scaffold( self.config, - &OrderNewArgs { + &OrderDraftCreateArgs { listing: item.listing.clone(), listing_addr: item.listing_addr.clone(), bin_id: Some(item.bin_id.clone()), diff --git a/src/operation_core.rs b/src/operation_core.rs @@ -1,11 +1,8 @@ -#![allow(dead_code)] - use std::path::PathBuf; use serde::Serialize; use serde_json::{Value, json}; -use crate::cli::LocalExportFormatArg; use crate::operation_adapter::{ AccountCreateRequest, AccountCreateResult, AccountGetRequest, AccountGetResult, AccountImportRequest, AccountImportResult, AccountListRequest, AccountListResult, @@ -27,6 +24,7 @@ use crate::runtime::accounts::{ }; use crate::runtime::config::RuntimeConfig; use crate::runtime::logging::LoggingState; +use crate::runtime_args::LocalExportFormatArg; pub struct CoreOperationService<'a> { config: &'a RuntimeConfig, diff --git a/src/operation_farm.rs b/src/operation_farm.rs @@ -1,11 +1,6 @@ -#![allow(dead_code)] - use serde::Serialize; use serde_json::{Value, json}; -use crate::cli::{ - FarmFieldArg, FarmInitArgs, FarmPublishArgs, FarmScopeArg, FarmScopedArgs, FarmSetArgs, -}; use crate::operation_adapter::{ FarmCreateRequest, FarmCreateResult, FarmFulfillmentUpdateRequest, FarmFulfillmentUpdateResult, FarmGetRequest, FarmGetResult, FarmLocationUpdateRequest, FarmLocationUpdateResult, @@ -16,6 +11,9 @@ use crate::operation_adapter::{ }; use crate::runtime::RuntimeError; use crate::runtime::config::RuntimeConfig; +use crate::runtime_args::{ + FarmCreateArgs, FarmFieldArg, FarmPublishArgs, FarmScopeArg, FarmScopedArgs, FarmUpdateArgs, +}; pub struct FarmOperationService<'a> { config: &'a RuntimeConfig, @@ -34,7 +32,7 @@ impl OperationService<FarmCreateRequest> for FarmOperationService<'_> { &self, request: OperationRequest<FarmCreateRequest>, ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { - let args = FarmInitArgs { + let args = FarmCreateArgs { scope: scope_input(&request)?, farm_d_tag: string_input(&request, "farm_d_tag"), name: string_input(&request, "name"), @@ -172,7 +170,7 @@ where R: OperationResultData, { let value = required_string(request, "value")?; - let args = FarmSetArgs { + let args = FarmUpdateArgs { scope: scope_input(request)?, field, value: vec![value.clone()], diff --git a/src/operation_listing.rs b/src/operation_listing.rs @@ -1,11 +1,8 @@ -#![allow(dead_code)] - use std::path::PathBuf; use serde::Serialize; use serde_json::{Value, json}; -use crate::cli::{ListingFileArgs, ListingMutationArgs, ListingNewArgs, RecordKeyArgs}; use crate::domain::runtime::{CommandDisposition, ListingMutationView}; use crate::operation_adapter::{ ListingArchiveRequest, ListingArchiveResult, ListingCreateRequest, ListingCreateResult, @@ -17,6 +14,9 @@ use crate::operation_adapter::{ }; use crate::runtime::RuntimeError; use crate::runtime::config::RuntimeConfig; +use crate::runtime_args::{ + ListingCreateArgs, ListingFileArgs, ListingMutationArgs, RecordLookupArgs, +}; pub struct ListingOperationService<'a> { config: &'a RuntimeConfig, @@ -35,7 +35,7 @@ impl OperationService<ListingCreateRequest> for ListingOperationService<'_> { &self, request: OperationRequest<ListingCreateRequest>, ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { - let args = ListingNewArgs { + let args = ListingCreateArgs { output: optional_path(&request, "output"), key: string_input(&request, "key"), title: string_input(&request, "title"), @@ -72,7 +72,7 @@ impl OperationService<ListingGetRequest> for ListingOperationService<'_> { &self, request: OperationRequest<ListingGetRequest>, ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { - let args = RecordKeyArgs { + let args = RecordLookupArgs { key: required_string(&request, "key")?, }; let view = map_runtime(crate::runtime::listing::get(self.config, &args))?; diff --git a/src/operation_market.rs b/src/operation_market.rs @@ -1,11 +1,8 @@ -#![allow(dead_code)] - use radroots_events::kinds::KIND_LISTING; use radroots_events_codec::trade::RadrootsTradeListingAddress; use serde::Serialize; use serde_json::{Value, json}; -use crate::cli::{FindArgs, RecordKeyArgs}; use crate::domain::runtime::{FindView, ListingGetView, SyncActionView}; use crate::operation_adapter::{ MarketListingGetRequest, MarketListingGetResult, MarketProductSearchRequest, @@ -15,6 +12,7 @@ use crate::operation_adapter::{ }; use crate::runtime::RuntimeError; use crate::runtime::config::RuntimeConfig; +use crate::runtime_args::{FindQueryArgs, RecordLookupArgs}; pub struct MarketOperationService<'a> { config: &'a RuntimeConfig, @@ -53,7 +51,7 @@ impl OperationService<MarketProductSearchRequest> for MarketOperationService<'_> &self, request: OperationRequest<MarketProductSearchRequest>, ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { - let args = FindArgs { + let args = FindQueryArgs { query: required_query_terms(&request)?, }; let view = market_product_search_view(map_runtime(crate::runtime::find::search( @@ -71,7 +69,7 @@ impl OperationService<MarketListingGetRequest> for MarketOperationService<'_> { &self, request: OperationRequest<MarketListingGetRequest>, ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { - let args = RecordKeyArgs { + let args = RecordLookupArgs { key: required_lookup(&request)?, }; let view = market_listing_get_view(map_runtime(crate::runtime::listing::get( diff --git a/src/operation_order.rs b/src/operation_order.rs @@ -1,9 +1,6 @@ -#![allow(dead_code)] - use serde::Serialize; use serde_json::Value; -use crate::cli::{OrderSubmitArgs, OrderWatchArgs, RecordKeyArgs}; use crate::domain::runtime::{CommandDisposition, OrderSubmitView}; use crate::operation_adapter::{ OperationAdapterError, OperationRequest, OperationRequestData, OperationRequestPayload, @@ -13,6 +10,7 @@ use crate::operation_adapter::{ }; use crate::runtime::RuntimeError; use crate::runtime::config::RuntimeConfig; +use crate::runtime_args::{OrderSubmitArgs, OrderWatchArgs, RecordLookupArgs}; pub struct OrderOperationService<'a> { config: &'a RuntimeConfig, @@ -40,7 +38,6 @@ impl OperationService<OrderSubmitRequest> for OrderOperationService<'_> { let key = required_order_key(&request)?; let args = OrderSubmitArgs { key, - watch: bool_input(&request, "watch").unwrap_or(false), idempotency_key: request .context .idempotency_key @@ -68,7 +65,7 @@ impl OperationService<OrderGetRequest> for OrderOperationService<'_> { &self, request: OperationRequest<OrderGetRequest>, ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { - let args = RecordKeyArgs { + let args = RecordLookupArgs { key: required_order_key(&request)?, }; let view = map_runtime(crate::runtime::order::get(self.config, &args))?; @@ -191,13 +188,6 @@ where .map(str::to_owned) } -fn bool_input<P>(request: &OperationRequest<P>, key: &str) -> Option<bool> -where - P: OperationRequestPayload + OperationRequestData, -{ - request.payload.input().get(key).and_then(Value::as_bool) -} - fn usize_input<P>(request: &OperationRequest<P>, key: &str) -> Option<usize> where P: OperationRequestPayload + OperationRequestData, diff --git a/src/operation_runtime.rs b/src/operation_runtime.rs @@ -1,9 +1,6 @@ -#![allow(dead_code)] - use serde::Serialize; use serde_json::{Value, json}; -use crate::cli::SyncWatchArgs; use crate::operation_adapter::{ JobGetRequest, JobGetResult, JobListRequest, JobListResult, JobWatchRequest, JobWatchResult, OperationAdapterError, OperationRequest, OperationRequestData, OperationRequestPayload, @@ -21,6 +18,7 @@ use crate::runtime::daemon::{self, DaemonRpcError}; use crate::runtime::management::{ RuntimeLifecycleAction, inspect_action, inspect_config_show, inspect_logs, inspect_status, }; +use crate::runtime_args::SyncWatchArgs; const DEFAULT_RUNTIME_ID: &str = "radrootsd"; diff --git a/src/render/mod.rs b/src/render/mod.rs @@ -1,4876 +0,0 @@ -use std::io::{self, Write}; - -use crate::domain::runtime::{ - AccountClearDefaultView, AccountImportView, AccountListView, AccountRemoveView, - AccountSummaryView, CommandOutput, CommandView, DoctorCheckView, DoctorView, - FarmConfigSummaryView, FarmGetView, FarmPublishComponentView, FarmPublishView, FarmSetView, - FarmSetupView, FarmStatusView, FindView, JobGetView, JobListView, JobWatchView, ListingGetView, - ListingMutationView, ListingNewView, ListingValidateView, LocalBackupView, LocalExportView, - LocalInitView, LocalStatusView, NetStatusView, OrderCancelView, OrderDraftItemView, - OrderGetView, OrderHistoryView, OrderJobView, OrderListView, OrderNewView, OrderSubmitView, - OrderSubmitWatchView, OrderWatchView, OrderWorkflowView, RelayListView, RpcSessionsView, - RpcStatusView, RuntimeActionView, RuntimeLogsView, RuntimeManagedConfigView, RuntimeStatusView, - SellAddView, SellCheckView, SellDraftMutationView, SellMutationView, SellShowView, SetupView, - SignerWriteKindReadinessView, StatusView, SyncActionView, SyncStatusView, SyncWatchView, -}; -use crate::runtime::RuntimeError; -use crate::runtime::config::{OutputConfig, OutputFormat, Verbosity}; - -const THIN_RULE: &str = "────────────────────────────────────────────────────"; - -pub fn render_output(output: &CommandOutput, config: &OutputConfig) -> Result<(), RuntimeError> { - match config.format { - OutputFormat::Human => render_human(output, config), - OutputFormat::Json => render_json(output), - OutputFormat::Ndjson => render_ndjson(output), - } -} - -fn render_human(output: &CommandOutput, config: &OutputConfig) -> Result<(), RuntimeError> { - let mut stdout = io::stdout().lock(); - render_human_with_config_to(&mut stdout, output, config) -} - -#[cfg(test)] -fn render_human_to(stdout: &mut dyn Write, output: &CommandOutput) -> Result<(), RuntimeError> { - render_human_with_config_to(stdout, output, &default_human_output_config()) -} - -fn render_human_with_config_to( - stdout: &mut dyn Write, - output: &CommandOutput, - config: &OutputConfig, -) -> Result<(), RuntimeError> { - if config.verbosity == Verbosity::Quiet { - if let Some(quiet) = render_quiet_output(output) { - writeln!(stdout, "{quiet}")?; - return Ok(()); - } - } - - let mut buffer = Vec::new(); - render_human_view_to(&mut buffer, output)?; - let rendered = String::from_utf8(buffer).map_err(|error| { - RuntimeError::Config(format!("human render output was not utf8: {error}")) - })?; - let finalized = finalize_human_output(output, rendered, config)?; - write!(stdout, "{finalized}")?; - Ok(()) -} - -#[cfg(test)] -fn default_human_output_config() -> OutputConfig { - OutputConfig { - format: OutputFormat::Human, - verbosity: Verbosity::Normal, - color: true, - dry_run: false, - } -} - -fn render_human_view_to( - stdout: &mut dyn Write, - output: &CommandOutput, -) -> Result<(), RuntimeError> { - match output.view() { - CommandView::AccountClearDefault(view) => render_account_clear_default(stdout, view)?, - CommandView::AccountImport(view) => render_account_import(stdout, view)?, - CommandView::AccountList(view) => render_account_list(stdout, view)?, - CommandView::AccountNew(view) => render_account_new(stdout, view)?, - CommandView::AccountRemove(view) => render_account_remove(stdout, view)?, - CommandView::AccountUse(view) => render_account_use(stdout, view)?, - CommandView::AccountWhoami(view) => render_account_whoami(stdout, view)?, - CommandView::MycStatus(view) => { - render_myc_status(stdout, view, true)?; - } - CommandView::NetStatus(view) => { - render_net_status(stdout, view)?; - } - CommandView::OrderCancel(view) => { - render_order_cancel(stdout, view)?; - } - CommandView::OrderGet(view) => { - render_order_get(stdout, view)?; - } - CommandView::OrderHistory(view) => { - render_order_history(stdout, view)?; - } - CommandView::OrderList(view) => { - render_order_list(stdout, view)?; - } - CommandView::OrderNew(view) => { - render_order_new(stdout, view)?; - } - CommandView::OrderSubmit(view) => { - render_order_submit(stdout, view)?; - } - CommandView::OrderSubmitWatch(view) => { - render_order_submit_watch(stdout, view)?; - } - CommandView::OrderWatch(view) => { - render_order_watch(stdout, view)?; - } - CommandView::RpcSessions(view) => { - render_rpc_sessions(stdout, view)?; - } - CommandView::RpcStatus(view) => { - render_rpc_status(stdout, view)?; - } - CommandView::SignerSessionAction(view) => { - render_signer_session_action(stdout, view)?; - } - CommandView::ConfigShow(view) => { - render_config_show(stdout, view)?; - } - CommandView::Doctor(view) => { - render_doctor(stdout, view)?; - } - CommandView::FarmGet(view) => { - render_farm_get(stdout, view)?; - } - CommandView::FarmPublish(view) => { - render_farm_publish(stdout, view)?; - } - CommandView::FarmSet(view) => { - render_farm_set(stdout, view)?; - } - CommandView::FarmSetup(view) => { - render_farm_setup(stdout, view)?; - } - CommandView::FarmStatus(view) => { - render_farm_status(stdout, view)?; - } - CommandView::Find(view) => { - render_find(stdout, view)?; - } - CommandView::JobGet(view) => { - render_job_get(stdout, view)?; - } - CommandView::JobList(view) => { - render_job_list(stdout, view)?; - } - CommandView::JobWatch(view) => { - render_job_watch(stdout, view)?; - } - CommandView::ListingGet(view) => { - render_listing_get(stdout, view)?; - } - CommandView::ListingMutation(view) => { - render_listing_mutation(stdout, view)?; - } - CommandView::ListingNew(view) => { - render_listing_new(stdout, view)?; - } - CommandView::ListingValidate(view) => { - render_listing_validate(stdout, view)?; - } - CommandView::LocalBackup(view) => { - render_local_backup(stdout, view)?; - } - CommandView::LocalExport(view) => { - render_local_export(stdout, view)?; - } - CommandView::LocalInit(view) => { - render_local_init(stdout, view)?; - } - CommandView::LocalStatus(view) => { - render_local_status(stdout, view)?; - } - CommandView::MarketSearch(view) => { - render_market_search(stdout, view)?; - } - CommandView::MarketUpdate(view) => { - render_market_update(stdout, view)?; - } - CommandView::MarketView(view) => { - render_market_view(stdout, view)?; - } - CommandView::RelayList(view) => { - render_relay_list(stdout, view)?; - } - CommandView::RuntimeAction(view) => { - render_runtime_action(stdout, view)?; - } - CommandView::RuntimeConfigShow(view) => { - render_runtime_config_show(stdout, view)?; - } - CommandView::RuntimeLogs(view) => { - render_runtime_logs(stdout, view)?; - } - CommandView::RuntimeStatus(view) => { - render_runtime_status(stdout, view)?; - } - CommandView::SellAdd(view) => { - render_sell_add(stdout, view)?; - } - CommandView::SellCheck(view) => { - render_sell_check(stdout, view)?; - } - CommandView::SellDraftMutation(view) => { - render_sell_draft_mutation(stdout, view)?; - } - CommandView::SellMutation(view) => { - render_sell_mutation(stdout, view)?; - } - CommandView::SellShow(view) => { - render_sell_show(stdout, view)?; - } - CommandView::Setup(view) => { - render_setup(stdout, view)?; - } - CommandView::SignerStatus(view) => { - write_context( - stdout, - match view.state.as_str() { - "ready" => "signer · active", - "unconfigured" => "signer · unconfigured", - "degraded" => "signer · degraded", - "unavailable" => "signer · unavailable", - _ => "signer · error", - }, - )?; - let mut signer_rows = vec![ - ("mode", view.mode.as_str()), - ("status", view.state.as_str()), - ]; - if let Some(account_id) = &view.signer_account_id { - signer_rows.push(("signer account id", account_id.as_str())); - } - render_pairs(stdout, "signer", signer_rows.as_slice())?; - if !view.write_kinds.is_empty() { - writeln!(stdout)?; - render_signer_write_kinds(stdout, &view.write_kinds)?; - } - writeln!(stdout)?; - render_account_resolution(stdout, &view.account_resolution)?; - if let Some(reason) = &view.reason { - writeln!(stdout, "reason: {reason}")?; - } - writeln!(stdout, "source: {}", view.source)?; - writeln!(stdout)?; - render_signer_binding(stdout, &view.binding)?; - if let Some(local) = &view.local { - writeln!(stdout)?; - render_local_signer(stdout, "local account", local)?; - } - if let Some(myc) = &view.myc { - writeln!(stdout)?; - render_myc_status(stdout, myc, false)?; - } - } - CommandView::Status(view) => { - render_status_summary(stdout, view)?; - } - CommandView::SyncPull(view) => { - render_sync_action(stdout, view)?; - } - CommandView::SyncPush(view) => { - render_sync_action(stdout, view)?; - } - CommandView::SyncStatus(view) => { - render_sync_status(stdout, view)?; - } - CommandView::SyncWatch(view) => { - render_sync_watch(stdout, view)?; - } - } - Ok(()) -} - -fn render_json(output: &CommandOutput) -> Result<(), RuntimeError> { - let mut stdout = io::stdout().lock(); - render_json_to(&mut stdout, output) -} - -fn render_json_to(stdout: &mut dyn Write, output: &CommandOutput) -> Result<(), RuntimeError> { - match output.view() { - CommandView::AccountClearDefault(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::AccountImport(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::AccountList(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::AccountNew(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::AccountRemove(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::AccountUse(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::AccountWhoami(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::MycStatus(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::NetStatus(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::OrderCancel(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::OrderGet(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::OrderHistory(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::OrderList(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::OrderNew(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::OrderSubmit(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::OrderSubmitWatch(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::OrderWatch(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::RpcSessions(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::RpcStatus(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::ConfigShow(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::Doctor(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::FarmGet(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::FarmPublish(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::FarmSet(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::FarmSetup(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::FarmStatus(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::Find(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::JobGet(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::JobList(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::JobWatch(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::ListingGet(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::ListingMutation(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::ListingNew(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::ListingValidate(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::LocalBackup(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::LocalExport(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::LocalInit(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::LocalStatus(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::MarketSearch(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::MarketUpdate(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::MarketView(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::RelayList(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::RuntimeAction(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::RuntimeConfigShow(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::RuntimeLogs(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::RuntimeStatus(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::SellAdd(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::SellCheck(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::SellDraftMutation(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::SellMutation(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::SellShow(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::Setup(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::SignerSessionAction(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::SignerStatus(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::Status(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::SyncPull(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::SyncPush(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::SyncStatus(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - CommandView::SyncWatch(view) => { - serde_json::to_writer_pretty(&mut *stdout, view)?; - writeln!(stdout)?; - } - } - Ok(()) -} - -fn render_ndjson(output: &CommandOutput) -> Result<(), RuntimeError> { - let mut stdout = io::stdout().lock(); - render_ndjson_to(&mut stdout, output) -} - -fn render_ndjson_to(stdout: &mut dyn Write, output: &CommandOutput) -> Result<(), RuntimeError> { - match output.view() { - CommandView::AccountList(view) => { - for account in &view.accounts { - serde_json::to_writer(&mut *stdout, account)?; - writeln!(stdout)?; - } - Ok(()) - } - CommandView::RelayList(view) => { - for relay in &view.relays { - serde_json::to_writer(&mut *stdout, relay)?; - writeln!(stdout)?; - } - Ok(()) - } - CommandView::Find(view) => { - for result in &view.results { - serde_json::to_writer(&mut *stdout, result)?; - writeln!(stdout)?; - } - Ok(()) - } - CommandView::MarketSearch(view) => { - for result in &view.results { - serde_json::to_writer(&mut *stdout, result)?; - writeln!(stdout)?; - } - Ok(()) - } - CommandView::JobList(view) => { - for job in &view.jobs { - serde_json::to_writer(&mut *stdout, job)?; - writeln!(stdout)?; - } - Ok(()) - } - CommandView::JobWatch(view) => { - for frame in &view.frames { - serde_json::to_writer(&mut *stdout, frame)?; - writeln!(stdout)?; - } - Ok(()) - } - CommandView::OrderHistory(view) => { - for order in &view.orders { - serde_json::to_writer(&mut *stdout, order)?; - writeln!(stdout)?; - } - Ok(()) - } - CommandView::OrderList(view) => { - for order in &view.orders { - serde_json::to_writer(&mut *stdout, order)?; - writeln!(stdout)?; - } - Ok(()) - } - CommandView::OrderWatch(view) => { - for frame in &view.frames { - serde_json::to_writer(&mut *stdout, frame)?; - writeln!(stdout)?; - } - Ok(()) - } - CommandView::RpcSessions(view) => { - for session in &view.sessions { - serde_json::to_writer(&mut *stdout, session)?; - writeln!(stdout)?; - } - Ok(()) - } - CommandView::SyncWatch(view) => { - for frame in &view.frames { - serde_json::to_writer(&mut *stdout, frame)?; - writeln!(stdout)?; - } - Ok(()) - } - _ => Err(RuntimeError::Config(format!( - "`{}` does not support --ndjson", - human_command_name(output.view()) - ))), - } -} - -fn yes_no(value: bool) -> &'static str { - if value { "yes" } else { "no" } -} - -fn render_quiet_output(output: &CommandOutput) -> Option<String> { - match output.view() { - CommandView::AccountClearDefault(view) => Some(match &view.cleared_account { - Some(account) => format!("Default account cleared: {}", account.id), - None => "No default account configured".to_owned(), - }), - CommandView::AccountImport(view) => Some(format!("Account imported: {}", view.account.id)), - CommandView::AccountNew(view) => Some(format!( - "{}: {}", - match view.state.as_str() { - "migrated" => "Account migrated", - _ => "Account created", - }, - view.account.id - )), - CommandView::AccountRemove(view) => { - Some(format!("Account removed: {}", view.removed_account.id)) - } - CommandView::Find(view) | CommandView::MarketSearch(view) => match view.state.as_str() { - "ready" if !view.results.is_empty() => Some( - view.results - .iter() - .map(|result| result.product_key.clone()) - .collect::<Vec<_>>() - .join("\n"), - ), - "empty" => Some("No listings found".to_owned()), - _ => None, - }, - CommandView::OrderSubmit(view) => match view.state.as_str() { - "accepted" | "submitted" | "already_submitted" | "deduplicated" => { - Some(format!("Order submitted: {}", view.order_id)) - } - _ => None, - }, - _ => None, - } -} - -fn finalize_human_output( - output: &CommandOutput, - rendered: String, - config: &OutputConfig, -) -> Result<String, RuntimeError> { - let mut cleaned_lines = Vec::new(); - let mut fallback_details = Vec::<(&'static str, String)>::new(); - - for line in rendered.lines() { - let trimmed = line.trim_end(); - if trimmed == THIN_RULE { - continue; - } - if let Some(value) = trimmed.trim_start().strip_prefix("workflow source: ") { - fallback_details.push(("Workflow source", value.to_owned())); - continue; - } - if let Some(value) = trimmed.trim_start().strip_prefix("source: ") { - fallback_details.push(("Source", value.to_owned())); - continue; - } - if let Some(value) = trimmed.trim_start().strip_prefix("provenance: ") { - fallback_details.push(("Provenance", value.to_owned())); - continue; - } - if let Some(value) = trimmed.strip_prefix("reason: ") { - cleaned_lines.push(value.to_owned()); - continue; - } - if trimmed == "actions" { - cleaned_lines.push("Next".to_owned()); - continue; - } - cleaned_lines.push(trimmed.to_owned()); - } - - let cleaned_lines = collapse_blank_lines(cleaned_lines); - let mut finalized = cleaned_lines.join("\n"); - if !finalized.is_empty() && !finalized.ends_with('\n') { - finalized.push('\n'); - } - - if matches!(config.verbosity, Verbosity::Verbose | Verbosity::Trace) { - let mut details = verbose_details(output); - for fallback in fallback_details { - if !details.iter().any(|(label, _)| *label == fallback.0) { - details.push(fallback); - } - } - if !details.is_empty() { - if !finalized.is_empty() { - finalized.push('\n'); - } - finalized.push_str("Details\n"); - finalized.push_str(render_field_rows_string(details.as_slice()).as_str()); - } - } - - if config.verbosity == Verbosity::Trace { - let mut trace_buffer = Vec::new(); - render_json_to(&mut trace_buffer, output)?; - let trace_json = String::from_utf8(trace_buffer).map_err(|error| { - RuntimeError::Config(format!("trace render output was not utf8: {error}")) - })?; - if !finalized.is_empty() { - finalized.push('\n'); - } - finalized.push_str("Trace\n"); - finalized.push_str( - render_field_rows_string(&[("Command", human_command_name(output.view()).to_owned())]) - .as_str(), - ); - for line in trace_json.trim_end().lines() { - finalized.push_str(" "); - finalized.push_str(line); - finalized.push('\n'); - } - } - - Ok(finalized) -} - -fn collapse_blank_lines(lines: Vec<String>) -> Vec<String> { - let mut collapsed = Vec::new(); - let mut previous_blank = true; - for line in lines { - let blank = line.trim().is_empty(); - if blank { - if previous_blank { - continue; - } - collapsed.push(String::new()); - } else { - collapsed.push(line); - } - previous_blank = blank; - } - while collapsed.last().is_some_and(|line| line.trim().is_empty()) { - collapsed.pop(); - } - collapsed -} - -fn render_field_rows_string(rows: &[(&str, String)]) -> String { - let label_width = rows - .iter() - .map(|(label, _)| label.len()) - .max() - .unwrap_or_default(); - let mut rendered = String::new(); - for (label, value) in rows { - rendered.push_str( - format!( - " {label:label_width$} {value}\n", - label_width = label_width - ) - .as_str(), - ); - } - rendered -} - -fn verbose_details(output: &CommandOutput) -> Vec<(&'static str, String)> { - match output.view() { - CommandView::AccountClearDefault(view) => vec![ - ("Source", view.source.clone()), - ( - "Remaining accounts", - view.remaining_account_count.to_string(), - ), - ], - CommandView::AccountImport(view) => vec![("Source", view.source.clone())], - CommandView::AccountList(view) => vec![("Source", view.source.clone())], - CommandView::AccountNew(view) => vec![("Source", view.source.clone())], - CommandView::AccountRemove(view) => vec![ - ("Source", view.source.clone()), - ( - "Remaining accounts", - view.remaining_account_count.to_string(), - ), - ], - CommandView::AccountUse(view) => vec![("Source", view.source.clone())], - CommandView::AccountWhoami(view) => vec![("Source", view.source.clone())], - CommandView::Doctor(view) => vec![("Source", view.source.clone())], - CommandView::Find(view) | CommandView::MarketSearch(view) => vec![ - ("Source", view.source.clone()), - ("Freshness", view.freshness.display.clone()), - ("Relay count", view.relay_count.to_string()), - ], - CommandView::ListingGet(view) | CommandView::MarketView(view) => vec![ - ("Source", view.source.clone()), - ("Freshness", view.provenance.freshness.clone()), - ("Relay count", view.provenance.relay_count.to_string()), - ], - CommandView::OrderSubmit(view) => { - let mut rows = vec![("Source", view.source.clone())]; - push_row( - &mut rows, - "Signer mode", - view.signer_mode.as_deref().map(str::to_owned), - ); - push_row( - &mut rows, - "Requested session", - view.requested_signer_session_id - .as_deref() - .map(str::to_owned), - ); - push_row( - &mut rows, - "Idempotency key", - view.idempotency_key.as_deref().map(str::to_owned), - ); - rows - } - CommandView::OrderSubmitWatch(view) => { - let mut rows = vec![("Source", view.submit.source.clone())]; - push_row( - &mut rows, - "Signer mode", - view.submit.signer_mode.as_deref().map(str::to_owned), - ); - push_row( - &mut rows, - "Requested session", - view.submit - .requested_signer_session_id - .as_deref() - .map(str::to_owned), - ); - rows - } - CommandView::RelayList(view) => vec![ - ("Source", view.source.clone()), - ("Relay count", view.count.to_string()), - ("Publish policy", view.publish_policy.clone()), - ], - _ => Vec::new(), - } -} - -fn present_absent(value: bool) -> &'static str { - if value { "present" } else { "absent" } -} - -fn render_account_list(stdout: &mut dyn Write, view: &AccountListView) -> Result<(), RuntimeError> { - if view.accounts.is_empty() { - writeln!(stdout, "No accounts yet")?; - if !view.actions.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Next", &view.actions)?; - } - return Ok(()); - } - - writeln!( - stdout, - "{} account{}", - view.count, - if view.count == 1 { "" } else { "s" } - )?; - writeln!(stdout)?; - for (index, account) in view.accounts.iter().enumerate() { - writeln!( - stdout, - "{}", - account - .display_name - .as_deref() - .filter(|name| !name.trim().is_empty()) - .unwrap_or(account.id.as_str()) - )?; - let rows = vec![ - ("Account", account.id.clone()), - ("Signer", humanize_machine_label(account.signer.as_str())), - ( - "Default", - if account.is_default { - "Yes".to_owned() - } else { - "No".to_owned() - }, - ), - ]; - render_field_rows(stdout, rows.as_slice())?; - if index + 1 < view.accounts.len() { - writeln!(stdout)?; - } - } - if !view.actions.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Next", &view.actions)?; - } - Ok(()) -} - -fn render_account_import( - stdout: &mut dyn Write, - view: &AccountImportView, -) -> Result<(), RuntimeError> { - writeln!(stdout, "Watch-only account imported")?; - writeln!(stdout)?; - render_account_section(stdout, &view.account)?; - writeln!(stdout)?; - writeln!(stdout, "Identity")?; - render_field_rows( - stdout, - &[("npub", view.public_identity.public_key_npub.clone())], - )?; - if !view.actions.is_empty() { - render_item_section(stdout, "Next", &view.actions)?; - } - Ok(()) -} - -fn render_account_new( - stdout: &mut dyn Write, - view: &crate::domain::runtime::AccountNewView, -) -> Result<(), RuntimeError> { - writeln!( - stdout, - "{}", - match view.state.as_str() { - "migrated" => "Account migrated", - _ => "Account created", - } - )?; - writeln!(stdout)?; - render_account_section(stdout, &view.account)?; - writeln!(stdout)?; - writeln!(stdout, "Identity")?; - render_field_rows( - stdout, - &[("npub", view.public_identity.public_key_npub.clone())], - )?; - if !view.actions.is_empty() { - render_item_section(stdout, "Next", &view.actions)?; - } - Ok(()) -} - -fn render_account_use( - stdout: &mut dyn Write, - view: &crate::domain::runtime::AccountUseView, -) -> Result<(), RuntimeError> { - writeln!(stdout, "Default account selected")?; - writeln!(stdout)?; - render_account_section(stdout, &view.account) -} - -fn render_account_clear_default( - stdout: &mut dyn Write, - view: &AccountClearDefaultView, -) -> Result<(), RuntimeError> { - writeln!( - stdout, - "{}", - match view.state.as_str() { - "cleared" => "Default account cleared", - _ => "No default account configured", - } - )?; - if let Some(account) = &view.cleared_account { - writeln!(stdout)?; - render_account_section(stdout, account)?; - } - writeln!(stdout)?; - render_field_rows( - stdout, - &[( - "Remaining accounts", - view.remaining_account_count.to_string(), - )], - )?; - if !view.actions.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Next", &view.actions)?; - } - Ok(()) -} - -fn render_account_remove( - stdout: &mut dyn Write, - view: &AccountRemoveView, -) -> Result<(), RuntimeError> { - writeln!( - stdout, - "{}", - if view.default_cleared { - "Default account removed" - } else { - "Account removed" - } - )?; - writeln!(stdout)?; - render_account_section(stdout, &view.removed_account)?; - writeln!(stdout)?; - render_field_rows( - stdout, - &[( - "Remaining accounts", - view.remaining_account_count.to_string(), - )], - )?; - if !view.actions.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Next", &view.actions)?; - } - Ok(()) -} - -fn render_account_whoami( - stdout: &mut dyn Write, - view: &crate::domain::runtime::AccountWhoamiView, -) -> Result<(), RuntimeError> { - match view.state.as_str() { - "ready" => { - writeln!(stdout, "Resolved account")?; - writeln!(stdout)?; - if let Some(account) = &view.account_resolution.resolved_account { - render_account_section(stdout, account)?; - } - writeln!(stdout)?; - render_account_resolution(stdout, &view.account_resolution)?; - if let Some(identity) = &view.public_identity { - writeln!(stdout)?; - writeln!(stdout, "Identity")?; - render_field_rows(stdout, &[("npub", identity.public_key_npub.clone())])?; - } - } - _ => { - writeln!(stdout, "Not ready yet")?; - if let Some(reason) = &view.reason { - writeln!(stdout)?; - writeln!(stdout, "{reason}")?; - } - writeln!(stdout)?; - render_account_resolution(stdout, &view.account_resolution)?; - writeln!(stdout)?; - render_item_section(stdout, "Missing", &["Resolved account".to_owned()])?; - if !view.actions.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Next", &view.actions)?; - } - } - } - Ok(()) -} - -fn render_account_section( - stdout: &mut dyn Write, - account: &AccountSummaryView, -) -> Result<(), RuntimeError> { - writeln!(stdout, "Account")?; - let mut rows = Vec::<(&str, String)>::new(); - push_row(&mut rows, "Name", account.display_name.clone()); - rows.push(("Account", account.id.clone())); - rows.push(("Signer", humanize_machine_label(account.signer.as_str()))); - rows.push(( - "Default", - if account.is_default { - "Yes".to_owned() - } else { - "No".to_owned() - }, - )); - render_field_rows(stdout, rows.as_slice()) -} - -fn render_account_resolution( - stdout: &mut dyn Write, - resolution: &crate::domain::runtime::AccountResolutionView, -) -> Result<(), RuntimeError> { - writeln!(stdout, "Account resolution")?; - let mut rows = vec![("Source", humanize_machine_label(resolution.source.as_str()))]; - if let Some(account) = &resolution.resolved_account { - rows.push(("Resolved account", account.id.clone())); - } - if let Some(account) = &resolution.default_account { - rows.push(("Default account", account.id.clone())); - } - render_field_rows(stdout, rows.as_slice()) -} - -fn render_config_show( - stdout: &mut dyn Write, - view: &crate::domain::runtime::ConfigShowView, -) -> Result<(), RuntimeError> { - write_context(stdout, "config · effective")?; - render_pairs( - stdout, - "output", - &[ - ("format", view.output.format.as_str()), - ("verbosity", view.output.verbosity.as_str()), - ("color", yes_no(view.output.color)), - ("dry run", yes_no(view.output.dry_run)), - ], - )?; - render_pairs( - stdout, - "interaction", - &[ - ("input enabled", yes_no(view.interaction.input_enabled)), - ("assume yes", yes_no(view.interaction.assume_yes)), - ("stdin tty", yes_no(view.interaction.stdin_tty)), - ("stdout tty", yes_no(view.interaction.stdout_tty)), - ("prompts allowed", yes_no(view.interaction.prompts_allowed)), - ( - "confirmations allowed", - yes_no(view.interaction.confirmations_allowed), - ), - ], - )?; - let user_config = format!( - "{} · {}", - present_absent(view.config_files.user_present), - view.paths.app_config_path - ); - let workspace_config = format!( - "{} · {}", - present_absent(view.config_files.workspace_present), - view.paths - .workspace_config_path - .as_deref() - .unwrap_or("disabled for interactive_user") - ); - let allowed_profiles = view.paths.allowed_profiles.join(", "); - render_pairs( - stdout, - "runtime roots", - &[ - ("profile", view.paths.profile.as_str()), - ("allowed profiles", allowed_profiles.as_str()), - ("app namespace", view.paths.app_namespace.as_str()), - ( - "shared accounts namespace", - view.paths.shared_accounts_namespace.as_str(), - ), - ( - "shared identities namespace", - view.paths.shared_identities_namespace.as_str(), - ), - ("app config", user_config.as_str()), - ("workspace config", workspace_config.as_str()), - ("app data root", view.paths.app_data_root.as_str()), - ("app logs root", view.paths.app_logs_root.as_str()), - ( - "shared accounts data", - view.paths.shared_accounts_data_root.as_str(), - ), - ( - "shared accounts secrets", - view.paths.shared_accounts_secrets_root.as_str(), - ), - ( - "default identity path", - view.paths.default_identity_path.as_str(), - ), - ], - )?; - - let mut logging_rows = vec![ - ("filter", view.logging.filter.as_str()), - ("stdout", yes_no(view.logging.stdout)), - ]; - if let Some(directory) = &view.logging.directory { - logging_rows.push(("directory", directory.as_str())); - } - if let Some(current_file) = &view.logging.current_file { - logging_rows.push(("file", current_file.as_str())); - } - render_pairs(stdout, "logging", logging_rows.as_slice())?; - - let mut account_rows = vec![ - ("store path", view.account.store_path.as_str()), - ("secrets dir", view.account.secrets_dir.as_str()), - ( - "contract default secret backend", - view.account - .secret_backend - .contract_default_backend - .as_str(), - ), - ( - "configured secret backend", - view.account.secret_backend.configured_primary.as_str(), - ), - ("identity path", view.account.identity_path.as_str()), - ]; - if let Some(fallback) = &view.account.secret_backend.contract_default_fallback { - account_rows.push(("contract default fallback", fallback.as_str())); - } - if let Some(fallback) = &view.account.secret_backend.configured_fallback { - account_rows.push(("configured secret fallback", fallback.as_str())); - } - let allowed_backends = view.account.secret_backend.allowed_backends.join(", "); - account_rows.push(("allowed secret backends", allowed_backends.as_str())); - if let Some(policy) = &view.account.secret_backend.host_vault_policy { - account_rows.push(("host vault policy", policy.as_str())); - } - account_rows.push(( - "uses protected store", - yes_no(view.account.secret_backend.uses_protected_store), - )); - account_rows.push(("secret status", view.account.secret_backend.state.as_str())); - if let Some(active_backend) = &view.account.secret_backend.active_backend { - account_rows.push(("active secret backend", active_backend.as_str())); - } - account_rows.push(( - "used secret fallback", - yes_no(view.account.secret_backend.used_fallback), - )); - if let Some(selector) = &view.account.selector { - account_rows.insert(0, ("selector", selector.as_str())); - } - render_pairs(stdout, "account", account_rows.as_slice())?; - if let Some(reason) = &view.account.secret_backend.reason { - writeln!(stdout, "account secret backend reason: {reason}")?; - } - render_pairs(stdout, "signer", &[("mode", view.signer.mode.as_str())])?; - let relay_count = view.relay.count.to_string(); - render_pairs( - stdout, - "relay", - &[ - ("count", relay_count.as_str()), - ("publish policy", view.relay.publish_policy.as_str()), - ("source", view.relay.source.as_str()), - ], - )?; - render_pairs( - stdout, - "local", - &[ - ("root", view.local.root.as_str()), - ("replica db", view.local.replica_db_path.as_str()), - ("backups dir", view.local.backups_dir.as_str()), - ("exports dir", view.local.exports_dir.as_str()), - ], - )?; - let myc_status_timeout_ms = view.myc.status_timeout_ms.to_string(); - render_pairs( - stdout, - "myc", - &[ - ("executable", view.myc.executable.as_str()), - ("status timeout ms", myc_status_timeout_ms.as_str()), - ], - )?; - let write_plane_target = format_runtime_target( - view.write_plane.target_kind.as_deref(), - view.write_plane.target.as_deref(), - ); - render_pairs( - stdout, - "write plane", - &[ - ("provider", view.write_plane.provider_runtime_id.as_str()), - ("binding model", view.write_plane.binding_model.as_str()), - ("state", view.write_plane.state.as_str()), - ("provenance", view.write_plane.provenance.as_str()), - ("source", view.write_plane.source.as_str()), - ("target", write_plane_target.as_str()), - ("detail", view.write_plane.detail.as_str()), - ( - "bridge auth configured", - yes_no(view.write_plane.bridge_auth_configured), - ), - ], - )?; - let workflow_target = format_runtime_target( - view.workflow.target_kind.as_deref(), - view.workflow.target.as_deref(), - ); - render_pairs( - stdout, - "workflow", - &[ - ("provider", view.workflow.provider_runtime_id.as_str()), - ("binding model", view.workflow.binding_model.as_str()), - ("state", view.workflow.state.as_str()), - ("provenance", view.workflow.provenance.as_str()), - ("source", view.workflow.source.as_str()), - ("target", workflow_target.as_str()), - ("hyf helper", view.workflow.hyf_helper_state.as_str()), - ( - "hyf helper detail", - view.workflow.hyf_helper_detail.as_str(), - ), - ], - )?; - render_pairs( - stdout, - "hyf", - &[ - ("enabled", yes_no(view.hyf.enabled)), - ("executable", view.hyf.executable.as_str()), - ], - )?; - let hyf_provider_target = format_runtime_target( - view.hyf_provider.target_kind.as_deref(), - view.hyf_provider.target.as_deref(), - ); - let mut hyf_provider_rows = vec![ - ("provider", view.hyf_provider.provider_runtime_id.as_str()), - ("binding model", view.hyf_provider.binding_model.as_str()), - ("state", view.hyf_provider.state.as_str()), - ("provenance", view.hyf_provider.provenance.as_str()), - ("source", view.hyf_provider.source.as_str()), - ("target", hyf_provider_target.as_str()), - ("executable", view.hyf_provider.executable.as_str()), - ]; - if let Some(reason) = &view.hyf_provider.reason { - hyf_provider_rows.push(("reason", reason.as_str())); - } - render_pairs(stdout, "hyf provider", hyf_provider_rows.as_slice())?; - render_pairs( - stdout, - "rpc", - &[ - ("url", view.rpc.url.as_str()), - ( - "bridge auth configured", - yes_no(view.rpc.bridge_auth_configured), - ), - ], - )?; - writeln!(stdout)?; - writeln!(stdout, "capability bindings")?; - let table = Table { - headers: &["capability", "provider", "state", "target"], - rows: view - .capability_bindings - .iter() - .map(|binding| { - vec![ - binding.capability_id.clone(), - binding.provider_runtime_id.clone(), - binding.state.clone(), - format_capability_binding_target(binding), - ] - }) - .collect(), - }; - render_table(stdout, &table)?; - writeln!(stdout)?; - writeln!(stdout, "resolved providers")?; - let resolved_table = Table { - headers: &["capability", "provider", "state", "provenance", "target"], - rows: view - .resolved_providers - .iter() - .map(|provider| { - vec![ - provider.capability_id.clone(), - provider.provider_runtime_id.clone(), - provider.state.clone(), - provider.provenance.clone(), - format_runtime_target( - provider.target_kind.as_deref(), - provider.target.as_deref(), - ), - ] - }) - .collect(), - }; - render_table(stdout, &resolved_table)?; - writeln!(stdout, "source: {}", view.source)?; - Ok(()) -} - -fn render_runtime_action( - stdout: &mut dyn Write, - view: &RuntimeActionView, -) -> Result<(), RuntimeError> { - write_context( - stdout, - format!( - "runtime · {} · {}", - view.runtime_id, - view.action.replace('_', " ") - ) - .as_str(), - )?; - render_pairs( - stdout, - "runtime", - &[ - ("runtime", view.runtime_id.as_str()), - ("instance", view.instance_id.as_str()), - ("instance source", view.instance_source.as_str()), - ("group", view.runtime_group.as_str()), - ("state", view.state.as_str()), - ("mutates bindings", yes_no(view.mutates_bindings)), - ], - )?; - writeln!(stdout, "detail: {}", view.detail)?; - if let Some(next_step) = &view.next_step { - writeln!(stdout, "next step: {next_step}")?; - } - writeln!(stdout, "source: {}", view.source)?; - Ok(()) -} - -fn render_runtime_config_show( - stdout: &mut dyn Write, - view: &RuntimeManagedConfigView, -) -> Result<(), RuntimeError> { - write_context( - stdout, - format!("runtime · {} · config", view.runtime_id).as_str(), - )?; - let mut rows = vec![ - ("runtime", view.runtime_id.as_str()), - ("instance", view.instance_id.as_str()), - ("instance source", view.instance_source.as_str()), - ("group", view.runtime_group.as_str()), - ("state", view.state.as_str()), - ("config present", yes_no(view.config_present)), - ]; - if let Some(config_format) = &view.config_format { - rows.push(("config format", config_format.as_str())); - } - if let Some(config_path) = &view.config_path { - rows.push(("config path", config_path.as_str())); - } - if let Some(requires_bootstrap_secret) = view.requires_bootstrap_secret { - rows.push(( - "requires bootstrap secret", - yes_no(requires_bootstrap_secret), - )); - } - if let Some(requires_config_bootstrap) = view.requires_config_bootstrap { - rows.push(( - "requires config bootstrap", - yes_no(requires_config_bootstrap), - )); - } - if let Some(requires_signer_provider) = view.requires_signer_provider { - rows.push(("requires signer provider", yes_no(requires_signer_provider))); - } - render_pairs(stdout, "runtime config", rows.as_slice())?; - writeln!(stdout, "detail: {}", view.detail)?; - writeln!(stdout, "source: {}", view.source)?; - Ok(()) -} - -fn render_runtime_logs(stdout: &mut dyn Write, view: &RuntimeLogsView) -> Result<(), RuntimeError> { - write_context( - stdout, - format!("runtime · {} · logs", view.runtime_id).as_str(), - )?; - let mut rows = vec![ - ("runtime", view.runtime_id.as_str()), - ("instance", view.instance_id.as_str()), - ("instance source", view.instance_source.as_str()), - ("group", view.runtime_group.as_str()), - ("state", view.state.as_str()), - ("stdout present", yes_no(view.stdout_log_present)), - ("stderr present", yes_no(view.stderr_log_present)), - ]; - if let Some(stdout_log_path) = &view.stdout_log_path { - rows.push(("stdout log", stdout_log_path.as_str())); - } - if let Some(stderr_log_path) = &view.stderr_log_path { - rows.push(("stderr log", stderr_log_path.as_str())); - } - render_pairs(stdout, "runtime logs", rows.as_slice())?; - writeln!(stdout, "detail: {}", view.detail)?; - writeln!(stdout, "source: {}", view.source)?; - Ok(()) -} - -fn render_runtime_status( - stdout: &mut dyn Write, - view: &RuntimeStatusView, -) -> Result<(), RuntimeError> { - write_context( - stdout, - format!("runtime · {} · status", view.runtime_id).as_str(), - )?; - let mut rows = vec![ - ("runtime", view.runtime_id.as_str()), - ("instance", view.instance_id.as_str()), - ("instance source", view.instance_source.as_str()), - ("group", view.runtime_group.as_str()), - ("posture", view.management_posture.as_str()), - ("state", view.state.as_str()), - ("install state", view.install_state.as_str()), - ("health state", view.health_state.as_str()), - ("health source", view.health_source.as_str()), - ("registry", view.registry_path.as_str()), - ]; - if let Some(mode) = &view.management_mode { - rows.push(("management mode", mode.as_str())); - } - if let Some(service_manager_integration) = view.service_manager_integration { - rows.push(( - "service manager integration", - yes_no(service_manager_integration), - )); - } - if let Some(uses_absolute_binary_paths) = view.uses_absolute_binary_paths { - rows.push(( - "uses absolute binary paths", - yes_no(uses_absolute_binary_paths), - )); - } - if let Some(preferred_cli_binding) = view.preferred_cli_binding { - rows.push(("preferred cli binding", yes_no(preferred_cli_binding))); - } - render_pairs(stdout, "runtime status", rows.as_slice())?; - writeln!(stdout, "detail: {}", view.detail)?; - if let Some(instance_paths) = &view.instance_paths { - render_pairs( - stdout, - "instance paths", - &[ - ("install dir", instance_paths.install_dir.as_str()), - ("state dir", instance_paths.state_dir.as_str()), - ("logs dir", instance_paths.logs_dir.as_str()), - ("run dir", instance_paths.run_dir.as_str()), - ("secrets dir", instance_paths.secrets_dir.as_str()), - ("pid file", instance_paths.pid_file_path.as_str()), - ("stdout log", instance_paths.stdout_log_path.as_str()), - ("stderr log", instance_paths.stderr_log_path.as_str()), - ("metadata", instance_paths.metadata_path.as_str()), - ], - )?; - } - if let Some(record) = &view.instance_record { - let mut record_rows = vec![ - ("management mode", record.management_mode.as_str()), - ("install state", record.install_state.as_str()), - ("binary path", record.binary_path.as_str()), - ("config path", record.config_path.as_str()), - ("logs path", record.logs_path.as_str()), - ("run path", record.run_path.as_str()), - ("installed version", record.installed_version.as_str()), - ]; - if let Some(health_endpoint) = &record.health_endpoint { - record_rows.push(("health endpoint", health_endpoint.as_str())); - } - if let Some(secret_material_ref) = &record.secret_material_ref { - record_rows.push(("secret material ref", secret_material_ref.as_str())); - } - if let Some(last_started_at) = &record.last_started_at { - record_rows.push(("last started at", last_started_at.as_str())); - } - if let Some(last_stopped_at) = &record.last_stopped_at { - record_rows.push(("last stopped at", last_stopped_at.as_str())); - } - if let Some(notes) = &record.notes { - record_rows.push(("notes", notes.as_str())); - } - render_pairs(stdout, "instance record", record_rows.as_slice())?; - } - if !view.lifecycle_actions.is_empty() { - writeln!( - stdout, - "lifecycle actions: {}", - view.lifecycle_actions.join(", ") - )?; - } - writeln!(stdout, "source: {}", view.source)?; - Ok(()) -} - -fn format_capability_binding_target( - binding: &crate::domain::runtime::CapabilityBindingRuntimeView, -) -> String { - let mut rendered = - format_runtime_target(binding.target_kind.as_deref(), binding.target.as_deref()); - if rendered.is_empty() { - return rendered; - } - if let Some(account_ref) = &binding.managed_account_ref { - rendered.push_str(format!(" · account {account_ref}").as_str()); - } - if let Some(session_ref) = &binding.signer_session_ref { - rendered.push_str(format!(" · session {session_ref}").as_str()); - } - rendered -} - -fn format_runtime_target(target_kind: Option<&str>, target: Option<&str>) -> String { - let Some(target) = target else { - return String::new(); - }; - - match target_kind { - Some(kind) => format!("{kind} {target}"), - None => target.to_owned(), - } -} - -fn render_doctor(stdout: &mut dyn Write, view: &DoctorView) -> Result<(), RuntimeError> { - writeln!(stdout, "Readiness check")?; - let ready = view - .checks - .iter() - .filter(|check| matches!(check.status.as_str(), "ok" | "ready" | "healthy")) - .map(doctor_item) - .collect::<Vec<_>>(); - let needs_attention = view - .checks - .iter() - .filter(|check| !matches!(check.status.as_str(), "ok" | "ready" | "healthy")) - .map(doctor_item) - .collect::<Vec<_>>(); - - if !ready.is_empty() || !needs_attention.is_empty() || !view.actions.is_empty() { - writeln!(stdout)?; - } - let mut wrote_section = false; - if !ready.is_empty() { - render_item_section(stdout, "Ready", &ready)?; - wrote_section = true; - } - if !needs_attention.is_empty() { - if wrote_section { - writeln!(stdout)?; - } - render_item_section(stdout, "Needs attention", &needs_attention)?; - wrote_section = true; - } - if !view.actions.is_empty() { - if wrote_section { - writeln!(stdout)?; - } - render_item_section(stdout, "Next", &view.actions)?; - } - writeln!(stdout)?; - render_account_resolution(stdout, &view.account_resolution)?; - Ok(()) -} - -fn render_find(stdout: &mut dyn Write, view: &FindView) -> Result<(), RuntimeError> { - render_market_search(stdout, view) -} - -fn render_market_search(stdout: &mut dyn Write, view: &FindView) -> Result<(), RuntimeError> { - match view.state.as_str() { - "unconfigured" => { - writeln!(stdout, "Not ready yet")?; - writeln!(stdout)?; - render_item_section(stdout, "Missing", &["Local market data".to_owned()])?; - if !view.actions.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Next", &view.actions)?; - } - } - "empty" => { - writeln!(stdout, "No listings found")?; - if !view.actions.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Next", &view.actions)?; - } - } - _ => { - writeln!(stdout, "{}", market_search_headline(view))?; - writeln!(stdout)?; - for (index, result) in view.results.iter().enumerate() { - render_market_search_card(stdout, result)?; - if index + 1 < view.results.len() { - writeln!(stdout)?; - } - } - if let Some(hyf) = &view.hyf { - if hyf.rewritten_query.trim() != view.query.trim() { - writeln!(stdout)?; - render_item_section(stdout, "Also searched for", &[view.query.clone()])?; - } - } - if !view.actions.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Next", &view.actions)?; - } - } - } - Ok(()) -} - -fn render_market_search_card( - stdout: &mut dyn Write, - result: &crate::domain::runtime::FindResultView, -) -> Result<(), RuntimeError> { - writeln!(stdout, "{}", result.title)?; - let mut rows = vec![("Key", result.product_key.clone())]; - push_row( - &mut rows, - "Listing address", - result - .listing_addr - .as_deref() - .and_then(non_empty_str) - .map(str::to_owned), - ); - push_row( - &mut rows, - "Place", - result - .location_primary - .as_deref() - .and_then(non_empty_str) - .map(str::to_owned), - ); - push_row(&mut rows, "Offer", quantity_offer_text(&result.available)); - rows.push(( - "Price", - format_price( - result.price.amount, - &result.price.currency, - result.price.per_amount, - &result.price.per_unit, - ), - )); - rows.push(( - "Stock", - format_available( - result - .available - .available_amount - .unwrap_or(result.available.total_amount), - result - .available - .label - .as_deref() - .unwrap_or(result.available.total_unit.as_str()), - ), - )); - render_field_rows(stdout, rows.as_slice()) -} - -fn market_search_headline(view: &FindView) -> String { - let query = view - .hyf - .as_ref() - .map(|hyf| hyf.rewritten_query.as_str()) - .unwrap_or(view.query.as_str()); - format!( - "{} listing{} for {}", - view.count, - if view.count == 1 { "" } else { "s" }, - query - ) -} - -fn render_job_list(stdout: &mut dyn Write, view: &JobListView) -> Result<(), RuntimeError> { - let context = match view.state.as_str() { - "ready" => format!( - "activity · {} job{}", - view.count, - if view.count == 1 { "" } else { "s" } - ), - "empty" => "activity · no retained jobs".to_owned(), - "unconfigured" => "activity · jobs unconfigured".to_owned(), - "unavailable" => "activity · jobs unavailable".to_owned(), - _ => "activity · jobs error".to_owned(), - }; - write_context(stdout, context.as_str())?; - if view.jobs.is_empty() { - if let Some(reason) = &view.reason { - writeln!(stdout, "{reason}")?; - writeln!(stdout)?; - } - } else { - let table = Table { - headers: &["job", "type", "state", "signer", "session", "updated"], - rows: view - .jobs - .iter() - .map(|job| { - let updated_at = job.completed_at_unix.unwrap_or(job.requested_at_unix); - vec![ - job.id.clone(), - job.command.clone(), - job.state.clone(), - job.signer.clone(), - job.signer_session_id.clone().unwrap_or_default(), - crate::runtime::job::format_timestamp(updated_at), - ] - }) - .collect(), - }; - render_table(stdout, &table)?; - writeln!(stdout)?; - } - writeln!(stdout, "rpc url: {}", view.rpc_url)?; - writeln!(stdout, "source: {}", view.source)?; - render_actions(stdout, &view.actions)?; - Ok(()) -} - -fn render_job_get(stdout: &mut dyn Write, view: &JobGetView) -> Result<(), RuntimeError> { - write_context(stdout, format!("activity · {}", view.lookup).as_str())?; - if let Some(job) = &view.job { - render_owned_pairs( - stdout, - "job", - &[ - ("id", job.id.clone()), - ("type", job.command.clone()), - ("state", job.state.clone()), - ("signer mode", job.signer.clone()), - ( - "signer session", - job.signer_session_id - .clone() - .unwrap_or_else(|| "-".to_owned()), - ), - ( - "requested", - crate::runtime::job::format_timestamp(job.requested_at_unix), - ), - ( - "completed", - job.completed_at_unix - .map(crate::runtime::job::format_timestamp) - .unwrap_or_else(|| "pending".to_owned()), - ), - ("terminal", yes_no(job.terminal).to_owned()), - ( - "recovered after restart", - yes_no(job.recovered_after_restart).to_owned(), - ), - ("delivery policy", job.delivery_policy.clone()), - ("relay outcome", job.relay_outcome_summary.clone()), - ], - )?; - if !job.attempt_summaries.is_empty() { - writeln!(stdout, "attempts")?; - for attempt in &job.attempt_summaries { - writeln!(stdout, " {attempt}")?; - } - writeln!(stdout)?; - } - } else if let Some(reason) = &view.reason { - writeln!(stdout, "{reason}")?; - writeln!(stdout)?; - } - writeln!(stdout, "rpc url: {}", view.rpc_url)?; - writeln!(stdout, "source: {}", view.source)?; - render_actions(stdout, &view.actions)?; - Ok(()) -} - -fn render_job_watch(stdout: &mut dyn Write, view: &JobWatchView) -> Result<(), RuntimeError> { - match view.state.as_str() { - "unconfigured" => { - writeln!(stdout, "Not ready yet")?; - if let Some(reason) = &view.reason { - writeln!(stdout)?; - writeln!(stdout, "{reason}")?; - } - if !view.actions.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Next", &view.actions)?; - } - } - "unavailable" => { - writeln!(stdout, "Unavailable right now")?; - if let Some(reason) = &view.reason { - writeln!(stdout)?; - writeln!(stdout, "{reason}")?; - } - if !view.actions.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Next", &view.actions)?; - } - } - "error" => { - writeln!(stdout, "Could not complete the command")?; - if let Some(reason) = &view.reason { - writeln!(stdout)?; - writeln!(stdout, "{reason}")?; - } - if !view.actions.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Next", &view.actions)?; - } - } - _ => { - writeln!(stdout, "Watching job {}", view.job_id)?; - if view.frames.is_empty() { - if let Some(reason) = &view.reason { - writeln!(stdout)?; - writeln!(stdout, "{reason}")?; - } - } else { - for frame in &view.frames { - writeln!(stdout)?; - writeln!( - stdout, - "{}", - crate::runtime::job::format_clock(frame.observed_at_unix) - )?; - let mut rows = vec![ - ("State", humanize_machine_label(frame.state.as_str())), - ("Summary", frame.summary.clone()), - ("Signer", humanize_machine_label(frame.signer.as_str())), - ]; - push_row(&mut rows, "Session", frame.signer_session_id.clone()); - if frame.terminal { - rows.push(("Terminal", "Yes".to_owned())); - } - render_field_rows(stdout, rows.as_slice())?; - } - } - if !view.actions.is_empty() { - render_item_section(stdout, "Next", &view.actions)?; - } - } - } - Ok(()) -} - -fn render_order_new(stdout: &mut dyn Write, view: &OrderNewView) -> Result<(), RuntimeError> { - write_context(stdout, "order · draft created")?; - let mut rows = vec![ - ("order id", view.order_id.as_str()), - ("file", view.file.as_str()), - ("ready for submit", yes_no(view.ready_for_submit)), - ]; - if let Some(listing_lookup) = &view.listing_lookup { - rows.push(("listing", listing_lookup.as_str())); - } - if let Some(listing_addr) = &view.listing_addr { - rows.push(("listing addr", listing_addr.as_str())); - } - if let Some(account_id) = &view.buyer_account_id { - rows.push(("buyer account", account_id.as_str())); - } - if let Some(buyer_pubkey) = &view.buyer_pubkey { - rows.push(("buyer pubkey", buyer_pubkey.as_str())); - } - if let Some(seller_pubkey) = &view.seller_pubkey { - rows.push(("seller pubkey", seller_pubkey.as_str())); - } - render_pairs(stdout, "draft", rows.as_slice())?; - render_order_items(stdout, &view.items)?; - render_order_issues(stdout, &view.issues)?; - writeln!(stdout, "source: {}", view.source)?; - render_actions(stdout, &view.actions)?; - Ok(()) -} - -fn render_order_get(stdout: &mut dyn Write, view: &OrderGetView) -> Result<(), RuntimeError> { - let context = match view.state.as_str() { - "missing" => format!("order · {} missing", view.lookup), - "submitted" => format!("order · {} submitted", view.lookup), - "ready" => format!("order · {} ready", view.lookup), - "draft" => format!("order · {} draft", view.lookup), - "error" => format!("order · {} error", view.lookup), - _ => format!("order · {}", view.lookup), - }; - write_context(stdout, context.as_str())?; - - if view.state == "missing" || view.state == "error" { - if let Some(reason) = &view.reason { - writeln!(stdout, "{reason}")?; - writeln!(stdout)?; - } - if let Some(file) = &view.file { - writeln!(stdout, "file: {file}")?; - } - writeln!(stdout, "source: {}", view.source)?; - render_actions(stdout, &view.actions)?; - return Ok(()); - } - - let mut rows = Vec::<(&str, &str)>::new(); - if let Some(order_id) = &view.order_id { - rows.push(("order id", order_id.as_str())); - } - if let Some(file) = &view.file { - rows.push(("file", file.as_str())); - } - rows.push(("ready for submit", yes_no(view.ready_for_submit))); - if let Some(listing_lookup) = &view.listing_lookup { - rows.push(("listing", listing_lookup.as_str())); - } - if let Some(listing_addr) = &view.listing_addr { - rows.push(("listing addr", listing_addr.as_str())); - } - if let Some(account_id) = &view.buyer_account_id { - rows.push(("buyer account", account_id.as_str())); - } - if let Some(buyer_pubkey) = &view.buyer_pubkey { - rows.push(("buyer pubkey", buyer_pubkey.as_str())); - } - if let Some(seller_pubkey) = &view.seller_pubkey { - rows.push(("seller pubkey", seller_pubkey.as_str())); - } - render_pairs(stdout, "order", rows.as_slice())?; - if let Some(updated_at_unix) = view.updated_at_unix { - writeln!( - stdout, - "updated: {}", - crate::runtime::job::format_timestamp(updated_at_unix) - )?; - } - render_order_items(stdout, &view.items)?; - if let Some(job) = &view.job { - render_order_job(stdout, job)?; - } - if let Some(workflow) = &view.workflow { - render_order_workflow(stdout, workflow)?; - } - render_order_issues(stdout, &view.issues)?; - if let Some(reason) = &view.reason { - writeln!(stdout, "reason: {reason}")?; - } - writeln!(stdout, "source: {}", view.source)?; - render_actions(stdout, &view.actions)?; - Ok(()) -} - -fn render_order_list(stdout: &mut dyn Write, view: &OrderListView) -> Result<(), RuntimeError> { - let context = match view.state.as_str() { - "empty" => "orders · no local drafts".to_owned(), - "degraded" => format!("orders · {} local drafts with issues", view.count), - _ => format!( - "orders · {} local draft{}", - view.count, - if view.count == 1 { "" } else { "s" } - ), - }; - write_context(stdout, context.as_str())?; - if view.orders.is_empty() { - writeln!(stdout, "no order drafts found")?; - writeln!(stdout)?; - } else { - let table = Table { - headers: &["order", "listing", "state", "ready", "job", "updated"], - rows: view - .orders - .iter() - .map(|order| { - vec![ - order.id.clone(), - order - .listing_lookup - .clone() - .or_else(|| order.listing_addr.clone()) - .unwrap_or_default(), - order.state.clone(), - yes_no(order.ready_for_submit).to_owned(), - order - .job - .as_ref() - .map(|job| job.state.clone()) - .unwrap_or_default(), - crate::runtime::job::format_timestamp(order.updated_at_unix), - ] - }) - .collect(), - }; - render_table(stdout, &table)?; - writeln!(stdout)?; - } - writeln!(stdout, "source: {}", view.source)?; - render_actions(stdout, &view.actions)?; - Ok(()) -} - -fn render_order_submit(stdout: &mut dyn Write, view: &OrderSubmitView) -> Result<(), RuntimeError> { - match view.state.as_str() { - "dry_run" => { - writeln!(stdout, "Dry run only")?; - writeln!(stdout)?; - writeln!(stdout, "Order would be submitted.")?; - writeln!(stdout)?; - render_order_submit_section(stdout, view)?; - writeln!(stdout, "Nothing was written.")?; - } - "missing" => { - writeln!(stdout, "Not found")?; - if let Some(reason) = &view.reason { - writeln!(stdout)?; - writeln!(stdout, "{reason}")?; - } - if !view.actions.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Next", &view.actions)?; - } - } - "unconfigured" => { - writeln!(stdout, "Not ready yet")?; - if let Some(reason) = &view.reason { - writeln!(stdout)?; - writeln!(stdout, "{reason}")?; - } - if !view.issues.is_empty() { - writeln!(stdout)?; - writeln!(stdout, "Needs attention")?; - let rows = view - .issues - .iter() - .map(|issue| (issue.field.as_str(), issue.message.clone())) - .collect::<Vec<_>>(); - render_field_rows(stdout, rows.as_slice())?; - } - if !view.actions.is_empty() { - render_item_section(stdout, "Next", &view.actions)?; - } - } - "unavailable" => { - writeln!(stdout, "Unavailable right now")?; - if let Some(reason) = &view.reason { - writeln!(stdout)?; - writeln!(stdout, "{reason}")?; - } - if !view.actions.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Next", &view.actions)?; - } - } - "error" => { - writeln!(stdout, "Could not complete the command")?; - if let Some(reason) = &view.reason { - writeln!(stdout)?; - writeln!(stdout, "{reason}")?; - } - if !view.actions.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Next", &view.actions)?; - } - } - _ => { - writeln!( - stdout, - "{}", - match view.state.as_str() { - "already_submitted" => "Order already submitted", - "deduplicated" => "Order already in progress", - _ => "Order submitted", - } - )?; - writeln!(stdout)?; - render_order_submit_section(stdout, view)?; - if let Some(job) = &view.job { - writeln!(stdout)?; - writeln!(stdout, "Job")?; - let mut rows = vec![ - ("Job", job.job_id.clone()), - ("State", humanize_machine_label(job.state.as_str())), - ]; - push_row(&mut rows, "Event", job.event_id.clone()); - render_field_rows(stdout, rows.as_slice())?; - } - if !view.actions.is_empty() { - render_item_section(stdout, "Next", &view.actions)?; - } - } - } - Ok(()) -} - -fn render_order_submit_section( - stdout: &mut dyn Write, - view: &OrderSubmitView, -) -> Result<(), RuntimeError> { - writeln!(stdout, "Order")?; - let mut rows = vec![("ID", view.order_id.clone())]; - push_row( - &mut rows, - "Listing", - first_present([view.listing_lookup.as_deref(), view.listing_addr.as_deref()]), - ); - push_row( - &mut rows, - "Buyer", - first_present([ - view.buyer_account_id.as_deref(), - view.buyer_pubkey.as_deref(), - ]), - ); - if !matches!(view.state.as_str(), "dry_run" | "missing" | "unconfigured") { - rows.push(("State", humanize_machine_label(view.state.as_str()))); - } - render_field_rows(stdout, rows.as_slice()) -} - -fn render_order_submit_watch( - stdout: &mut dyn Write, - view: &OrderSubmitWatchView, -) -> Result<(), RuntimeError> { - writeln!(stdout, "{}", order_submit_watch_headline(&view.submit))?; - writeln!(stdout)?; - - writeln!(stdout, "Order")?; - let mut order_rows = vec![ - ("ID", view.submit.order_id.clone()), - ("State", humanize_machine_label(view.submit.state.as_str())), - ]; - push_row( - &mut order_rows, - "Listing", - first_present([ - view.submit.listing_lookup.as_deref(), - view.submit.listing_addr.as_deref(), - ]), - ); - push_row( - &mut order_rows, - "Buyer", - first_present([ - view.submit.buyer_account_id.as_deref(), - view.submit.buyer_pubkey.as_deref(), - ]), - ); - render_field_rows(stdout, order_rows.as_slice())?; - - if let Some(job) = &view.submit.job { - writeln!(stdout, "Job")?; - let mut job_rows = vec![ - ("Job", job.job_id.clone()), - ("State", humanize_machine_label(job.state.as_str())), - ]; - push_row(&mut job_rows, "Event", job.event_id.clone()); - render_field_rows(stdout, job_rows.as_slice())?; - } - - writeln!(stdout, "Watching order {}", view.watch.order_id)?; - - if view.watch.frames.is_empty() { - if let Some(reason) = &view.watch.reason { - writeln!(stdout)?; - writeln!(stdout, "{reason}")?; - } - } else { - for frame in &view.watch.frames { - writeln!(stdout)?; - writeln!( - stdout, - "{}", - crate::runtime::job::format_clock(frame.observed_at_unix) - )?; - let rows = vec![ - ("State", humanize_machine_label(frame.state.as_str())), - ("Summary", frame.summary.clone()), - ]; - render_field_rows(stdout, rows.as_slice())?; - } - } - - if !view.watch.actions.is_empty() { - render_item_section(stdout, "Next", &view.watch.actions)?; - } - Ok(()) -} - -fn render_order_watch(stdout: &mut dyn Write, view: &OrderWatchView) -> Result<(), RuntimeError> { - match view.state.as_str() { - "missing" => { - writeln!(stdout, "Not found")?; - if let Some(reason) = &view.reason { - writeln!(stdout)?; - writeln!(stdout, "{reason}")?; - } - if !view.actions.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Next", &view.actions)?; - } - } - "not_submitted" | "unconfigured" => { - writeln!(stdout, "Not ready yet")?; - if let Some(reason) = &view.reason { - writeln!(stdout)?; - writeln!(stdout, "{reason}")?; - } - if !view.actions.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Next", &view.actions)?; - } - } - "unavailable" => { - writeln!(stdout, "Unavailable right now")?; - if let Some(reason) = &view.reason { - writeln!(stdout)?; - writeln!(stdout, "{reason}")?; - } - if !view.actions.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Next", &view.actions)?; - } - } - "error" => { - writeln!(stdout, "Could not complete the command")?; - if let Some(reason) = &view.reason { - writeln!(stdout)?; - writeln!(stdout, "{reason}")?; - } - if !view.actions.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Next", &view.actions)?; - } - } - _ => { - writeln!(stdout, "Watching order {}", view.order_id)?; - if view.frames.is_empty() { - if let Some(reason) = &view.reason { - writeln!(stdout)?; - writeln!(stdout, "{reason}")?; - } - } else { - for frame in &view.frames { - writeln!(stdout)?; - writeln!( - stdout, - "{}", - crate::runtime::job::format_clock(frame.observed_at_unix) - )?; - let mut rows = vec![ - ("State", humanize_machine_label(frame.state.as_str())), - ("Summary", frame.summary.clone()), - ]; - push_row( - &mut rows, - "Signer", - Some(humanize_machine_label(frame.signer_mode.as_str())), - ); - push_row(&mut rows, "Session", frame.signer_session_id.clone()); - if frame.terminal { - rows.push(("Terminal", "Yes".to_owned())); - } - render_field_rows(stdout, rows.as_slice())?; - } - } - if !view.actions.is_empty() { - render_item_section(stdout, "Next", &view.actions)?; - } - } - } - Ok(()) -} - -fn render_order_history( - stdout: &mut dyn Write, - view: &OrderHistoryView, -) -> Result<(), RuntimeError> { - let context = match view.state.as_str() { - "empty" => "order history · no submitted orders".to_owned(), - _ => format!( - "order history · {} submitted order{}", - view.count, - if view.count == 1 { "" } else { "s" } - ), - }; - write_context(stdout, context.as_str())?; - if view.orders.is_empty() { - if let Some(reason) = &view.reason { - writeln!(stdout, "{reason}")?; - writeln!(stdout)?; - } - } else { - let table = Table { - headers: &["order", "listing", "state", "job", "submitted", "updated"], - rows: view - .orders - .iter() - .map(|order| { - vec![ - order.id.clone(), - order - .listing_lookup - .clone() - .or_else(|| order.listing_addr.clone()) - .unwrap_or_default(), - order.state.clone(), - order - .job - .as_ref() - .map(|job| job.job_id.clone()) - .unwrap_or_default(), - order - .submitted_at_unix - .map(crate::runtime::job::format_timestamp) - .unwrap_or_default(), - crate::runtime::job::format_timestamp(order.updated_at_unix), - ] - }) - .collect(), - }; - render_table(stdout, &table)?; - writeln!(stdout)?; - if let Some(reason) = &view.reason { - writeln!(stdout, "note: {reason}")?; - } - } - writeln!(stdout, "source: {}", view.source)?; - render_actions(stdout, &view.actions)?; - Ok(()) -} - -fn render_order_cancel(stdout: &mut dyn Write, view: &OrderCancelView) -> Result<(), RuntimeError> { - let context = match view.state.as_str() { - "missing" => format!("order · {} missing", view.lookup), - "not_submitted" => format!("order · {} not submitted", view.lookup), - "unconfigured" => format!("order · {} cancel unavailable", view.lookup), - "unavailable" => format!("order · {} cancel unavailable", view.lookup), - "error" => format!("order · {} cancel error", view.lookup), - _ => format!("order · {} cancel", view.lookup), - }; - write_context(stdout, context.as_str())?; - let mut rows = vec![("lookup", view.lookup.as_str())]; - if let Some(order_id) = &view.order_id { - rows.push(("order id", order_id.as_str())); - } - render_pairs(stdout, "order", rows.as_slice())?; - if let Some(job) = &view.job { - render_order_job(stdout, job)?; - } - if let Some(reason) = &view.reason { - writeln!(stdout, "reason: {reason}")?; - } - writeln!(stdout, "source: {}", view.source)?; - render_actions(stdout, &view.actions)?; - Ok(()) -} - -fn render_order_items( - stdout: &mut dyn Write, - items: &[OrderDraftItemView], -) -> Result<(), RuntimeError> { - if items.is_empty() { - writeln!(stdout, "items: no line items yet")?; - writeln!(stdout)?; - return Ok(()); - } - - let table = Table { - headers: &["bin", "qty"], - rows: items - .iter() - .map(|item| vec![item.bin_id.clone(), item.bin_count.to_string()]) - .collect(), - }; - render_table(stdout, &table)?; - writeln!(stdout)?; - Ok(()) -} - -fn render_order_job(stdout: &mut dyn Write, job: &OrderJobView) -> Result<(), RuntimeError> { - let mut rows = vec![ - ("job id", job.job_id.as_str()), - ("state", job.state.as_str()), - ]; - if let Some(command) = &job.command { - rows.push(("command", command.as_str())); - } - if let Some(signer_mode) = &job.signer_mode { - rows.push(("signer mode", signer_mode.as_str())); - } - if let Some(signer_session_id) = &job.signer_session_id { - rows.push(("signer session", signer_session_id.as_str())); - } - if let Some(requested_signer_session_id) = &job.requested_signer_session_id { - rows.push(( - "requested signer session", - requested_signer_session_id.as_str(), - )); - } - if let Some(event_id) = &job.event_id { - rows.push(("event id", event_id.as_str())); - } - if let Some(event_addr) = &job.event_addr { - rows.push(("event addr", event_addr.as_str())); - } - render_pairs(stdout, "job", rows.as_slice())?; - if let Some(reason) = &job.reason { - writeln!(stdout, "job reason: {reason}")?; - } - Ok(()) -} - -fn render_order_workflow( - stdout: &mut dyn Write, - workflow: &OrderWorkflowView, -) -> Result<(), RuntimeError> { - let mut rows = vec![ - ("state", workflow.state.as_str()), - ("order id", workflow.order_id.as_str()), - ]; - if let Some(listing_addr) = &workflow.listing_addr { - rows.push(("listing addr", listing_addr.as_str())); - } - if let Some(validated_listing_event_id) = &workflow.validated_listing_event_id { - rows.push(( - "validated listing event", - validated_listing_event_id.as_str(), - )); - } - if let Some(root_event_id) = &workflow.root_event_id { - rows.push(("root event id", root_event_id.as_str())); - } - if let Some(last_event_id) = &workflow.last_event_id { - rows.push(("last event id", last_event_id.as_str())); - } - render_pairs(stdout, "workflow", rows.as_slice())?; - if let Some(reason) = &workflow.reason { - writeln!(stdout, "workflow reason: {reason}")?; - } - writeln!(stdout, "workflow source: {}", workflow.source)?; - Ok(()) -} - -fn render_order_issues( - stdout: &mut dyn Write, - issues: &[crate::domain::runtime::OrderIssueView], -) -> Result<(), RuntimeError> { - if issues.is_empty() { - return Ok(()); - } - - writeln!(stdout, "issues")?; - for issue in issues { - writeln!(stdout, " {} {}", issue.field, issue.message)?; - } - writeln!(stdout)?; - Ok(()) -} - -fn render_listing_new(stdout: &mut dyn Write, view: &ListingNewView) -> Result<(), RuntimeError> { - write_context(stdout, "listing · draft created")?; - let mut rows = vec![ - ("file", view.file.as_str()), - ("listing id", view.listing_id.as_str()), - ]; - if let Some(account_id) = &view.selected_account_id { - rows.push(("account id", account_id.as_str())); - } - if let Some(seller_pubkey) = &view.seller_pubkey { - rows.push(("seller", seller_pubkey.as_str())); - } - if let Some(farm_d_tag) = &view.farm_d_tag { - rows.push(("farm d_tag", farm_d_tag.as_str())); - } - if let Some(delivery_method) = &view.delivery_method { - rows.push(("delivery", delivery_method.as_str())); - } - if let Some(location_primary) = &view.location_primary { - rows.push(("location", location_primary.as_str())); - } - render_pairs(stdout, "draft", rows.as_slice())?; - if let Some(reason) = &view.reason { - writeln!(stdout, "reason: {reason}")?; - } - writeln!(stdout, "source: {}", view.source)?; - render_actions(stdout, &view.actions)?; - Ok(()) -} - -fn render_listing_validate( - stdout: &mut dyn Write, - view: &ListingValidateView, -) -> Result<(), RuntimeError> { - write_context( - stdout, - match view.state.as_str() { - "valid" => "listing · valid", - _ => "listing · invalid", - }, - )?; - let status = if view.valid { - "ready to publish" - } else { - "needs edits" - }; - let mut rows = vec![("file", view.file.as_str()), ("status", status)]; - if let Some(listing_id) = &view.listing_id { - rows.push(("listing id", listing_id.as_str())); - } - if let Some(seller_pubkey) = &view.seller_pubkey { - rows.push(("seller", seller_pubkey.as_str())); - } - if let Some(farm_d_tag) = &view.farm_d_tag { - rows.push(("farm d_tag", farm_d_tag.as_str())); - } - render_pairs(stdout, "validation", rows.as_slice())?; - if !view.issues.is_empty() { - writeln!(stdout, "issues")?; - for issue in &view.issues { - match issue.line { - Some(line) => writeln!( - stdout, - " {field} {message} (line {line})", - field = issue.field, - message = issue.message - )?, - None => writeln!( - stdout, - " {field} {message}", - field = issue.field, - message = issue.message - )?, - } - } - writeln!(stdout)?; - } - writeln!(stdout, "source: {}", view.source)?; - render_actions(stdout, &view.actions)?; - Ok(()) -} - -fn render_listing_get(stdout: &mut dyn Write, view: &ListingGetView) -> Result<(), RuntimeError> { - render_market_view(stdout, view) -} - -fn render_market_view(stdout: &mut dyn Write, view: &ListingGetView) -> Result<(), RuntimeError> { - match view.state.as_str() { - "unconfigured" => { - writeln!(stdout, "Not ready yet")?; - if let Some(reason) = &view.reason { - writeln!(stdout)?; - writeln!(stdout, "{reason}")?; - } - if !view.actions.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Next", &view.actions)?; - } - } - "missing" => { - writeln!(stdout, "Not found")?; - if let Some(reason) = &view.reason { - writeln!(stdout)?; - writeln!(stdout, "{reason}")?; - } - if !view.actions.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Next", &view.actions)?; - } - } - _ => { - let headline = view.title.as_deref().unwrap_or("Listing"); - writeln!(stdout, "{headline}")?; - writeln!(stdout)?; - let mut rows = Vec::<(&str, String)>::new(); - push_row( - &mut rows, - "Key", - view.product_key - .as_deref() - .and_then(non_empty_str) - .map(str::to_owned), - ); - push_row( - &mut rows, - "Listing address", - view.listing_addr - .as_deref() - .and_then(non_empty_str) - .map(str::to_owned), - ); - push_row( - &mut rows, - "Category", - view.category - .as_deref() - .and_then(non_empty_str) - .map(str::to_owned), - ); - push_row( - &mut rows, - "Place", - view.location_primary - .as_deref() - .and_then(non_empty_str) - .map(str::to_owned), - ); - if let Some(available) = &view.available { - push_row(&mut rows, "Offer", quantity_offer_text(available)); - rows.push(( - "Stock", - format_available( - available.available_amount.unwrap_or(available.total_amount), - available - .label - .as_deref() - .unwrap_or(available.total_unit.as_str()), - ), - )); - } - if let Some(price) = &view.price { - rows.push(( - "Price", - format_price( - price.amount, - &price.currency, - price.per_amount, - &price.per_unit, - ), - )); - } - render_owned_pairs(stdout, "Listing", rows.as_slice())?; - let mut wrote_about = false; - if let Some(description) = &view.description { - render_item_section(stdout, "About", &[description.clone()])?; - wrote_about = true; - } - if !view.actions.is_empty() { - if wrote_about { - writeln!(stdout)?; - } - render_item_section(stdout, "Next", &view.actions)?; - } - } - } - Ok(()) -} - -fn render_sell_add(stdout: &mut dyn Write, view: &SellAddView) -> Result<(), RuntimeError> { - writeln!(stdout, "Listing draft saved")?; - writeln!(stdout)?; - writeln!(stdout, "The draft is local until you publish it.")?; - writeln!(stdout)?; - - let mut draft_rows = vec![("File", view.file.clone())]; - push_row(&mut draft_rows, "Listing", view.product_key.clone()); - push_row(&mut draft_rows, "Title", view.title.clone()); - push_row(&mut draft_rows, "Offer", view.offer.clone()); - push_row(&mut draft_rows, "Price", view.price.clone()); - push_row(&mut draft_rows, "Stock", view.stock.clone()); - render_owned_pairs(stdout, "Draft", draft_rows.as_slice())?; - - let mut default_rows = Vec::<(&str, String)>::new(); - push_row(&mut default_rows, "Farm", view.farm_name.clone()); - push_row( - &mut default_rows, - "Delivery", - view.delivery_method - .as_deref() - .map(humanize_delivery_method), - ); - push_row(&mut default_rows, "Place", view.location_primary.clone()); - if !default_rows.is_empty() { - render_owned_pairs(stdout, "Defaults", default_rows.as_slice())?; - } - - if let Some(reason) = &view.reason { - writeln!(stdout, "{reason}")?; - writeln!(stdout)?; - } - if !view.actions.is_empty() { - render_item_section(stdout, "Next", &view.actions)?; - } - Ok(()) -} - -fn render_sell_show(stdout: &mut dyn Write, view: &SellShowView) -> Result<(), RuntimeError> { - writeln!(stdout, "Listing draft")?; - writeln!(stdout)?; - - let mut draft_rows = vec![("File", view.file.clone())]; - push_row(&mut draft_rows, "Listing", view.product_key.clone()); - push_row(&mut draft_rows, "Title", view.title.clone()); - push_row(&mut draft_rows, "Category", view.category.clone()); - push_row(&mut draft_rows, "Offer", view.offer.clone()); - push_row(&mut draft_rows, "Price", view.price.clone()); - push_row(&mut draft_rows, "Stock", view.stock.clone()); - push_row( - &mut draft_rows, - "Delivery", - view.delivery_method - .as_deref() - .map(humanize_delivery_method), - ); - push_row(&mut draft_rows, "Place", view.location_primary.clone()); - render_owned_pairs(stdout, "Draft", draft_rows.as_slice())?; - - if let Some(reason) = &view.reason { - writeln!(stdout, "{reason}")?; - writeln!(stdout)?; - } - if !view.actions.is_empty() { - render_item_section(stdout, "Next", &view.actions)?; - } - Ok(()) -} - -fn render_sell_check(stdout: &mut dyn Write, view: &SellCheckView) -> Result<(), RuntimeError> { - if view.valid { - writeln!(stdout, "Draft looks ready")?; - writeln!(stdout)?; - let mut draft_rows = vec![("File", view.file.clone())]; - push_row(&mut draft_rows, "Listing", view.product_key.clone()); - push_row(&mut draft_rows, "Seller", view.seller_pubkey.clone()); - push_row(&mut draft_rows, "Farm", view.farm_ref.clone()); - render_owned_pairs(stdout, "Draft", draft_rows.as_slice())?; - } else { - writeln!(stdout, "Draft needs changes")?; - writeln!(stdout)?; - let rows = view - .issues - .iter() - .map(|issue| (issue.field.as_str(), issue.message.clone())) - .collect::<Vec<_>>(); - render_field_rows(stdout, rows.as_slice())?; - } - - if !view.actions.is_empty() { - render_item_section(stdout, "Next", &view.actions)?; - } - Ok(()) -} - -fn render_sell_mutation( - stdout: &mut dyn Write, - view: &SellMutationView, -) -> Result<(), RuntimeError> { - match view.state.as_str() { - "dry_run" => { - writeln!(stdout, "Dry run only")?; - writeln!(stdout)?; - writeln!( - stdout, - "Listing would be {}.", - match view.operation.as_str() { - "publish" => "published", - "update" => "updated", - "pause" => "paused", - _ => "changed", - } - )?; - writeln!(stdout)?; - let mut rows = vec![("File", view.file.clone())]; - push_row(&mut rows, "Listing", view.product_key.clone()); - rows.push(("Address", view.listing_addr.clone())); - if view.operation == "publish" { - push_row( - &mut rows, - "Publish mode", - view.publish_mode.as_deref().map(|mode| { - if mode == "runtime_bridge" { - "Runtime bridge".to_owned() - } else { - mode.to_owned() - } - }), - ); - } - render_owned_pairs(stdout, "Listing", rows.as_slice())?; - writeln!(stdout, "Nothing was written.")?; - } - "unconfigured" => { - writeln!(stdout, "Not ready yet")?; - if let Some(reason) = &view.reason { - writeln!(stdout)?; - writeln!(stdout, "{reason}")?; - } - if !view.actions.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Next", &view.actions)?; - } - } - "unavailable" => { - writeln!(stdout, "Unavailable right now")?; - if let Some(reason) = &view.reason { - writeln!(stdout)?; - writeln!(stdout, "{reason}")?; - } - if !view.actions.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Next", &view.actions)?; - } - } - "error" => { - writeln!(stdout, "Something went wrong")?; - if let Some(reason) = &view.reason { - writeln!(stdout)?; - writeln!(stdout, "{reason}")?; - } - if !view.actions.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Next", &view.actions)?; - } - } - _ => { - writeln!( - stdout, - "{}", - match view.operation.as_str() { - "publish" => "Listing published", - "update" => "Listing updated", - "pause" => "Listing paused", - _ => "Listing updated", - } - )?; - writeln!(stdout)?; - let mut listing_rows = vec![("File", view.file.clone())]; - push_row(&mut listing_rows, "Listing", view.product_key.clone()); - listing_rows.push(("Address", view.listing_addr.clone())); - if view.operation == "publish" { - push_row( - &mut listing_rows, - "Publish mode", - view.publish_mode.as_deref().map(|mode| { - if mode == "runtime_bridge" { - "Runtime bridge".to_owned() - } else { - mode.to_owned() - } - }), - ); - } - render_owned_pairs(stdout, "Listing", listing_rows.as_slice())?; - - let mut job_rows = Vec::<(&str, String)>::new(); - push_row(&mut job_rows, "State", view.job_status.clone()); - push_row(&mut job_rows, "Job", view.job_id.clone()); - push_row(&mut job_rows, "Event", view.event_id.clone()); - if !job_rows.is_empty() { - render_owned_pairs(stdout, "Job", job_rows.as_slice())?; - } - - if !view.actions.is_empty() { - render_item_section(stdout, "Next", &view.actions)?; - } - } - } - Ok(()) -} - -fn render_sell_draft_mutation( - stdout: &mut dyn Write, - view: &SellDraftMutationView, -) -> Result<(), RuntimeError> { - writeln!(stdout, "Draft updated")?; - writeln!(stdout)?; - render_owned_pairs( - stdout, - "Changed", - &[(view.changed_label.as_str(), view.changed_value.clone())], - )?; - let mut draft_rows = vec![("File", view.file.clone())]; - push_row(&mut draft_rows, "Listing", view.product_key.clone()); - render_owned_pairs(stdout, "Draft", draft_rows.as_slice())?; - if !view.actions.is_empty() { - render_item_section(stdout, "Next", &view.actions)?; - } - Ok(()) -} - -fn render_listing_mutation( - stdout: &mut dyn Write, - view: &ListingMutationView, -) -> Result<(), RuntimeError> { - let context = match view.state.as_str() { - "dry_run" => format!("listing · {} dry run", view.operation), - "deduplicated" => format!("listing · {} deduplicated", view.operation), - "published" => format!("listing · {} completed", view.operation), - "failed" | "unavailable" => format!("listing · {} unavailable", view.operation), - "unconfigured" => format!("listing · {} unconfigured", view.operation), - "error" => format!("listing · {} error", view.operation), - other => format!("listing · {} {other}", view.operation), - }; - write_context(stdout, context.as_str())?; - - let mut rows = vec![ - ("file", view.file.as_str()), - ("listing id", view.listing_id.as_str()), - ("event addr", view.listing_addr.as_str()), - ]; - if let Some(job_id) = &view.job_id { - rows.push(("job id", job_id.as_str())); - } - if let Some(job_status) = &view.job_status { - rows.push(("status", job_status.as_str())); - } - if let Some(event_id) = &view.event_id { - rows.push(("event id", event_id.as_str())); - } - if let Some(signer_mode) = &view.signer_mode { - rows.push(("signer mode", signer_mode.as_str())); - } - if let Some(signer_session_id) = &view.signer_session_id { - rows.push(("signer session", signer_session_id.as_str())); - } - if let Some(requested_signer_session_id) = &view.requested_signer_session_id { - rows.push(( - "requested signer session", - requested_signer_session_id.as_str(), - )); - } - render_pairs(stdout, "listing", rows.as_slice())?; - if let Some(reason) = &view.reason { - writeln!(stdout, "reason: {reason}")?; - } - writeln!(stdout, "source: {}", view.source)?; - - if let Some(job) = &view.job { - writeln!(stdout)?; - writeln!(stdout, "job preview")?; - let job_json = serde_json::to_string_pretty(job)?; - for line in job_json.lines() { - writeln!(stdout, " {line}")?; - } - } - if let Some(event) = &view.event { - writeln!(stdout)?; - writeln!(stdout, "event preview")?; - let event_json = serde_json::to_string_pretty(event)?; - for line in event_json.lines() { - writeln!(stdout, " {line}")?; - } - } - render_actions(stdout, &view.actions)?; - Ok(()) -} - -fn render_relay_list(stdout: &mut dyn Write, view: &RelayListView) -> Result<(), RuntimeError> { - if view.relays.is_empty() { - writeln!(stdout, "Not ready yet")?; - if let Some(reason) = &view.reason { - writeln!(stdout)?; - writeln!(stdout, "{reason}")?; - } - writeln!(stdout)?; - render_item_section(stdout, "Missing", &["Relay configuration".to_owned()])?; - if !view.actions.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Next", &view.actions)?; - } - return Ok(()); - } - - writeln!( - stdout, - "{} relay{}", - view.count, - if view.count == 1 { "" } else { "s" } - )?; - writeln!(stdout)?; - for (index, relay) in view.relays.iter().enumerate() { - writeln!(stdout, "{}", relay.url)?; - let rows = vec![ - ( - "Read", - if relay.read { - "Yes".to_owned() - } else { - "No".to_owned() - }, - ), - ( - "Write", - if relay.write { - "Yes".to_owned() - } else { - "No".to_owned() - }, - ), - ]; - render_field_rows(stdout, rows.as_slice())?; - if index + 1 < view.relays.len() { - writeln!(stdout)?; - } - } - if !view.actions.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Next", &view.actions)?; - } - Ok(()) -} - -fn render_net_status(stdout: &mut dyn Write, view: &NetStatusView) -> Result<(), RuntimeError> { - write_context( - stdout, - match view.state.as_str() { - "configured" => "network · configured", - _ => "network · unconfigured", - }, - )?; - let relay_count = view.relay_count.to_string(); - let rows = vec![ - ("status", view.state.as_str()), - ("session", view.session.as_str()), - ("relays configured", relay_count.as_str()), - ("publish policy", view.publish_policy.as_str()), - ("signer mode", view.signer_mode.as_str()), - ]; - render_pairs(stdout, "network", rows.as_slice())?; - writeln!(stdout)?; - render_account_resolution(stdout, &view.account_resolution)?; - if let Some(reason) = &view.reason { - writeln!(stdout, "reason: {reason}")?; - } - writeln!(stdout, "source: {}", view.source)?; - render_actions(stdout, &view.actions)?; - Ok(()) -} - -fn render_rpc_status(stdout: &mut dyn Write, view: &RpcStatusView) -> Result<(), RuntimeError> { - write_context(stdout, format!("rpc · {}", view.state).as_str())?; - let mut rows = vec![("url", view.url.as_str()), ("status", view.state.as_str())]; - if let Some(auth_mode) = &view.auth_mode { - rows.push(("auth mode", auth_mode.as_str())); - } - if let Some(signer_mode) = &view.signer_mode { - rows.push(("signer mode", signer_mode.as_str())); - } - if let Some(default_signer_mode) = &view.default_signer_mode { - rows.push(("default signer", default_signer_mode.as_str())); - } - render_pairs(stdout, "rpc", rows.as_slice())?; - - let mut bridge_rows = Vec::<(&str, String)>::new(); - if let Some(enabled) = view.bridge_enabled { - bridge_rows.push(("bridge enabled", yes_no(enabled).to_owned())); - } - if let Some(ready) = view.bridge_ready { - bridge_rows.push(("bridge ready", yes_no(ready).to_owned())); - } - if let Some(relay_count) = view.relay_count { - bridge_rows.push(("relay count", relay_count.to_string())); - } - if let Some(retained_jobs) = view.retained_jobs { - bridge_rows.push(("retained jobs", retained_jobs.to_string())); - } - if let Some(job_status_retention) = view.job_status_retention { - bridge_rows.push(("job retention", job_status_retention.to_string())); - } - if !bridge_rows.is_empty() { - render_owned_pairs(stdout, "bridge", bridge_rows.as_slice())?; - } - if let Some(reason) = &view.reason { - writeln!(stdout, "reason: {reason}")?; - } - writeln!(stdout, "source: {}", view.source)?; - render_actions(stdout, &view.actions)?; - Ok(()) -} - -fn render_rpc_sessions(stdout: &mut dyn Write, view: &RpcSessionsView) -> Result<(), RuntimeError> { - let context = match view.state.as_str() { - "ready" => format!( - "rpc · {} session{}", - view.count, - if view.count == 1 { "" } else { "s" } - ), - "empty" => "rpc · no public sessions".to_owned(), - "unconfigured" => "rpc · sessions unconfigured".to_owned(), - "unavailable" => "rpc · sessions unavailable".to_owned(), - _ => "rpc · sessions error".to_owned(), - }; - write_context(stdout, context.as_str())?; - if view.sessions.is_empty() { - if let Some(reason) = &view.reason { - writeln!(stdout, "{reason}")?; - writeln!(stdout)?; - } - } else { - let table = Table { - headers: &[ - "session", - "role", - "user", - "auth", - "authorized", - "relays", - "expires", - ], - rows: view - .sessions - .iter() - .map(|session| { - vec![ - session.session_id.clone(), - session.role.clone(), - session - .user_pubkey - .clone() - .unwrap_or_else(|| "n/a".to_owned()), - yes_no(session.auth_required).to_owned(), - yes_no(session.authorized).to_owned(), - session.relay_count.to_string(), - session - .expires_in_secs - .map(|secs| format!("{secs}s")) - .unwrap_or_else(|| "n/a".to_owned()), - ] - }) - .collect(), - }; - render_table(stdout, &table)?; - writeln!(stdout)?; - } - writeln!(stdout, "rpc url: {}", view.url)?; - writeln!(stdout, "source: {}", view.source)?; - render_actions(stdout, &view.actions)?; - Ok(()) -} - -fn render_signer_session_action( - stdout: &mut dyn Write, - view: &crate::domain::runtime::SignerSessionActionView, -) -> Result<(), RuntimeError> { - write_context( - stdout, - format!("signer session · {} · {}", view.action, view.state).as_str(), - )?; - let relays = view.relays.join(", "); - let permissions = view.permissions.join(", "); - let auth_required = view.auth_required.map(yes_no).unwrap_or("n/a"); - let authorized = view.authorized.map(yes_no).unwrap_or("n/a"); - let replayed = view.replayed.map(yes_no).unwrap_or("n/a"); - let required = view.required.map(yes_no).unwrap_or("n/a"); - let closed = view.closed.map(yes_no).unwrap_or("n/a"); - let expires = view - .expires_in_secs - .map(|secs| format!("{secs}s")) - .unwrap_or_else(|| "n/a".to_owned()); - render_pairs( - stdout, - "session", - &[ - ("id", view.session_id.as_deref().unwrap_or("n/a")), - ("mode", view.mode.as_deref().unwrap_or("n/a")), - ( - "remote signer", - view.remote_signer_pubkey.as_deref().unwrap_or("n/a"), - ), - ("client", view.client_pubkey.as_deref().unwrap_or("n/a")), - ( - "signer pubkey", - view.signer_pubkey.as_deref().unwrap_or("n/a"), - ), - ("user pubkey", view.user_pubkey.as_deref().unwrap_or("n/a")), - ("pubkey", view.pubkey.as_deref().unwrap_or("n/a")), - ("auth required", auth_required), - ("authorized", authorized), - ("auth url", view.auth_url.as_deref().unwrap_or("n/a")), - ("expires", expires.as_str()), - ("replayed", replayed), - ("required", required), - ("closed", closed), - ( - "relays", - if relays.is_empty() { - "n/a" - } else { - relays.as_str() - }, - ), - ( - "permissions", - if permissions.is_empty() { - "n/a" - } else { - permissions.as_str() - }, - ), - ], - )?; - if let Some(reason) = &view.reason { - writeln!(stdout, "reason: {reason}")?; - } - writeln!(stdout, "source: {}", view.source)?; - Ok(()) -} - -fn render_sync_status(stdout: &mut dyn Write, view: &SyncStatusView) -> Result<(), RuntimeError> { - write_context( - stdout, - match view.state.as_str() { - "ready" => "activity · sync status", - _ => "activity · sync unconfigured", - }, - )?; - let relay_count = view.relay_count.to_string(); - let expected = view.queue.expected_count.to_string(); - let pending = view.queue.pending_count.to_string(); - render_pairs( - stdout, - "sync", - &[ - ("status", view.state.as_str()), - ("freshness", view.freshness.display.as_str()), - ("pending", pending.as_str()), - ("expected", expected.as_str()), - ("relays", relay_count.as_str()), - ("publish policy", view.publish_policy.as_str()), - ("replica db", view.replica_db.as_str()), - ("local root", view.local_root.as_str()), - ], - )?; - if let Some(reason) = &view.reason { - writeln!(stdout, "reason: {reason}")?; - } - writeln!(stdout, "source: {}", view.source)?; - render_actions(stdout, &view.actions)?; - Ok(()) -} - -fn render_sync_action(stdout: &mut dyn Write, view: &SyncActionView) -> Result<(), RuntimeError> { - write_context( - stdout, - format!("activity · sync {} {}", view.direction, view.state).as_str(), - )?; - let relay_count = view.relay_count.to_string(); - let expected = view.queue.expected_count.to_string(); - let pending = view.queue.pending_count.to_string(); - render_pairs( - stdout, - "sync", - &[ - ("direction", view.direction.as_str()), - ("status", view.state.as_str()), - ("freshness", view.freshness.display.as_str()), - ("pending", pending.as_str()), - ("expected", expected.as_str()), - ("relays", relay_count.as_str()), - ("publish policy", view.publish_policy.as_str()), - ("replica db", view.replica_db.as_str()), - ("local root", view.local_root.as_str()), - ], - )?; - if let Some(reason) = &view.reason { - writeln!(stdout, "reason: {reason}")?; - } - writeln!(stdout, "source: {}", view.source)?; - render_actions(stdout, &view.actions)?; - Ok(()) -} - -fn render_market_update(stdout: &mut dyn Write, view: &SyncActionView) -> Result<(), RuntimeError> { - match view.state.as_str() { - "unconfigured" => { - writeln!(stdout, "Not ready yet")?; - let mut missing = Vec::new(); - if view.replica_db == "missing" { - missing.push("Local market data".to_owned()); - } - if view.relay_count == 0 { - missing.push("Relay configuration".to_owned()); - } - if !missing.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Missing", &missing)?; - } - if !view.actions.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Next", &view.actions)?; - } - } - "unavailable" => { - writeln!(stdout, "Unavailable right now")?; - if let Some(reason) = &view.reason { - writeln!(stdout)?; - writeln!(stdout, "{reason}")?; - } - if !view.actions.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Next", &view.actions)?; - } - } - _ => { - writeln!(stdout, "Market data updated")?; - writeln!(stdout)?; - render_owned_pairs( - stdout, - "Local data", - &[ - ("State", view.state.clone()), - ("Updated", view.freshness.display.clone()), - ("Relays", view.relay_count.to_string()), - ], - )?; - if !view.actions.is_empty() { - render_item_section(stdout, "Next", &view.actions)?; - } - } - } - Ok(()) -} - -fn render_sync_watch(stdout: &mut dyn Write, view: &SyncWatchView) -> Result<(), RuntimeError> { - match view.state.as_str() { - "unconfigured" => { - writeln!(stdout, "Not ready yet")?; - if let Some(reason) = &view.reason { - writeln!(stdout)?; - writeln!(stdout, "{reason}")?; - } - if !view.actions.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Next", &view.actions)?; - } - } - "unavailable" => { - writeln!(stdout, "Unavailable right now")?; - if let Some(reason) = &view.reason { - writeln!(stdout)?; - writeln!(stdout, "{reason}")?; - } - if !view.actions.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Next", &view.actions)?; - } - } - "error" => { - writeln!(stdout, "Could not complete the command")?; - if let Some(reason) = &view.reason { - writeln!(stdout)?; - writeln!(stdout, "{reason}")?; - } - if !view.actions.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Next", &view.actions)?; - } - } - _ => { - writeln!(stdout, "Watching market sync")?; - if view.frames.is_empty() { - if let Some(reason) = &view.reason { - writeln!(stdout)?; - writeln!(stdout, "{reason}")?; - } - } else { - for frame in &view.frames { - writeln!(stdout)?; - writeln!( - stdout, - "{}", - crate::runtime::job::format_clock(frame.observed_at) - )?; - let rows = vec![ - ("State", humanize_machine_label(frame.state.as_str())), - ("Relays", frame.relay_count.to_string()), - ("Updated", frame.freshness.display.clone()), - ("Queue", format!("{} pending", frame.queue.pending_count)), - ]; - render_field_rows(stdout, rows.as_slice())?; - } - } - if !view.actions.is_empty() { - render_item_section(stdout, "Next", &view.actions)?; - } - } - } - Ok(()) -} - -fn render_farm_setup(stdout: &mut dyn Write, view: &FarmSetupView) -> Result<(), RuntimeError> { - match view.state.as_str() { - "unconfigured" => { - writeln!(stdout, "Not ready yet")?; - writeln!(stdout)?; - render_item_section(stdout, "Missing", &["Resolved account".to_owned()])?; - if !view.actions.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Next", &view.actions)?; - } - Ok(()) - } - _ => { - writeln!(stdout, "Farm draft saved")?; - if let Some(reason) = &view.reason { - writeln!(stdout)?; - writeln!(stdout, "{reason}")?; - } - if let Some(config) = &view.config { - writeln!(stdout)?; - render_farm_summary(stdout, config)?; - } - if !view.actions.is_empty() { - render_item_section(stdout, "Next", &view.actions)?; - } - Ok(()) - } - } -} - -fn render_farm_set(stdout: &mut dyn Write, view: &FarmSetView) -> Result<(), RuntimeError> { - match view.state.as_str() { - "unconfigured" => { - writeln!(stdout, "Farm draft not found")?; - if !view.actions.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Next", &view.actions)?; - } - Ok(()) - } - _ => { - writeln!(stdout, "Farm updated")?; - writeln!(stdout)?; - render_owned_pairs( - stdout, - "Changed", - &[("Field", view.field.clone()), ("Value", view.value.clone())], - )?; - if let Some(config) = &view.config { - render_farm_summary(stdout, config)?; - } - if !view.actions.is_empty() { - render_item_section(stdout, "Next", &view.actions)?; - } - Ok(()) - } - } -} - -fn render_farm_status(stdout: &mut dyn Write, view: &FarmStatusView) -> Result<(), RuntimeError> { - match view.state.as_str() { - "ready" => { - writeln!(stdout, "Farm ready to publish")?; - if let Some(config) = &view.config { - writeln!(stdout)?; - render_farm_summary(stdout, config)?; - } - if !view.actions.is_empty() { - render_item_section(stdout, "Next", &view.actions)?; - } - Ok(()) - } - _ => { - writeln!(stdout, "Farm not ready yet")?; - if !view.missing.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Missing", &view.missing)?; - } - if !view.actions.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Next", &view.actions)?; - } - Ok(()) - } - } -} - -fn render_farm_get(stdout: &mut dyn Write, view: &FarmGetView) -> Result<(), RuntimeError> { - if let Some(document) = &view.document { - writeln!(stdout, "Farm draft")?; - writeln!(stdout)?; - render_farm_document(stdout, document)?; - } else { - writeln!(stdout, "Farm draft not found")?; - if !view.actions.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Next", &view.actions)?; - } - } - Ok(()) -} - -fn render_farm_publish(stdout: &mut dyn Write, view: &FarmPublishView) -> Result<(), RuntimeError> { - if view.state == "unconfigured" { - writeln!(stdout, "Not ready yet")?; - if !view.missing.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Missing", &view.missing)?; - } - if !view.actions.is_empty() { - writeln!(stdout)?; - render_item_section(stdout, "Next", &view.actions)?; - } - return Ok(()); - } - - write_context(stdout, format!("farm publish · {}", view.state).as_str())?; - render_owned_pairs( - stdout, - "farm", - &[ - ("scope", view.scope.clone()), - ("path", view.path.clone()), - ("account id", view.selected_account_id.clone()), - ("account pubkey", view.selected_account_pubkey.clone()), - ("farm d_tag", view.farm_d_tag.clone()), - ("dry run", yes_no(view.dry_run).to_owned()), - ], - )?; - render_farm_publish_component(stdout, "profile", &view.profile)?; - render_farm_publish_component(stdout, "farm record", &view.farm)?; - if let Some(reason) = &view.reason { - writeln!(stdout, "reason: {reason}")?; - } - writeln!(stdout, "source: {}", view.source)?; - render_actions(stdout, &view.actions)?; - Ok(()) -} - -fn render_farm_document( - stdout: &mut dyn Write, - document: &crate::domain::runtime::FarmConfigDocumentView, -) -> Result<(), RuntimeError> { - let mut rows = Vec::new(); - push_row( - &mut rows, - "Name", - first_present([ - Some(document.profile.name.as_str()), - Some(document.farm.name.as_str()), - ]), - ); - push_row( - &mut rows, - "Display name", - document.profile.display_name.as_deref().map(str::to_owned), - ); - push_row( - &mut rows, - "About", - first_present([ - document.profile.about.as_deref(), - document.farm.about.as_deref(), - ]), - ); - push_row( - &mut rows, - "Website", - first_present([ - document.profile.website.as_deref(), - document.farm.website.as_deref(), - ]), - ); - push_row( - &mut rows, - "Place", - first_present([ - Some(document.listing_defaults.location.primary.as_str()), - document - .farm - .location - .as_ref() - .and_then(|location| location.primary.as_deref()), - ]), - ); - push_row( - &mut rows, - "City", - first_present([ - document.listing_defaults.location.city.as_deref(), - document - .farm - .location - .as_ref() - .and_then(|location| location.city.as_deref()), - ]), - ); - push_row( - &mut rows, - "Region", - first_present([ - document.listing_defaults.location.region.as_deref(), - document - .farm - .location - .as_ref() - .and_then(|location| location.region.as_deref()), - ]), - ); - push_row( - &mut rows, - "Country", - first_present([ - document.listing_defaults.location.country.as_deref(), - document - .farm - .location - .as_ref() - .and_then(|location| location.country.as_deref()), - ]), - ); - push_row( - &mut rows, - "Delivery", - non_empty_str(document.listing_defaults.delivery_method.as_str()) - .map(humanize_delivery_method), - ); - rows.push(("Scope", document.selection.scope.clone())); - rows.push(("Farm tag", document.selection.farm_d_tag.clone())); - render_owned_pairs(stdout, "Farm", rows.as_slice()) -} - -fn render_farm_publish_component( - stdout: &mut dyn Write, - label: &str, - component: &FarmPublishComponentView, -) -> Result<(), RuntimeError> { - let mut rows = vec![ - ("state", component.state.clone()), - ("rpc method", component.rpc_method.clone()), - ("event kind", component.event_kind.to_string()), - ]; - if let Some(job_id) = &component.job_id { - rows.push(("job id", job_id.clone())); - } - if let Some(job_status) = &component.job_status { - rows.push(("job status", job_status.clone())); - } - if let Some(event_id) = &component.event_id { - rows.push(("event id", event_id.clone())); - } - if let Some(event_addr) = &component.event_addr { - rows.push(("event addr", event_addr.clone())); - } - if let Some(reason) = &component.reason { - rows.push(("reason", reason.clone())); - } - render_owned_pairs(stdout, label, rows.as_slice()) -} - -fn render_farm_summary( - stdout: &mut dyn Write, - config: &FarmConfigSummaryView, -) -> Result<(), RuntimeError> { - let mut rows = Vec::new(); - push_row( - &mut rows, - "Name", - non_empty_str(config.name.as_str()).map(str::to_owned), - ); - rows.push(("Scope", config.scope.clone())); - push_row( - &mut rows, - "Place", - config - .location_primary - .as_deref() - .and_then(non_empty_str) - .map(str::to_owned), - ); - push_row( - &mut rows, - "Delivery", - non_empty_str(config.delivery_method.as_str()).map(humanize_delivery_method), - ); - render_owned_pairs(stdout, "Farm", rows.as_slice()) -} - -fn render_local_init(stdout: &mut dyn Write, view: &LocalInitView) -> Result<(), RuntimeError> { - write_context(stdout, format!("local · {}", view.state).as_str())?; - render_pairs( - stdout, - "local", - &[ - ("replica db", view.replica_db.as_str()), - ("path", view.path.as_str()), - ("local root", view.local_root.as_str()), - ("replica db version", view.replica_db_version.as_str()), - ("backup format version", view.backup_format_version.as_str()), - ], - )?; - writeln!(stdout, "source: {}", view.source)?; - Ok(()) -} - -fn render_local_status(stdout: &mut dyn Write, view: &LocalStatusView) -> Result<(), RuntimeError> { - write_context( - stdout, - match view.state.as_str() { - "ready" => "local · status", - _ => "local · unconfigured", - }, - )?; - let mut rows = vec![ - ("replica db", view.replica_db.as_str()), - ("path", view.path.as_str()), - ("local root", view.local_root.as_str()), - ]; - if view.state == "ready" { - rows.push(("replica db version", view.replica_db_version.as_str())); - rows.push(("backup format version", view.backup_format_version.as_str())); - rows.push(("schema hash", view.schema_hash.as_str())); - } - render_pairs(stdout, "local", rows.as_slice())?; - if view.state == "ready" { - let sync_expected = view.sync.expected_count.to_string(); - let sync_pending = view.sync.pending_count.to_string(); - render_pairs( - stdout, - "sync", - &[ - ("expected", sync_expected.as_str()), - ("pending", sync_pending.as_str()), - ], - )?; - let farms = view.counts.farms.to_string(); - let listings = view.counts.listings.to_string(); - let profiles = view.counts.profiles.to_string(); - let relays = view.counts.relays.to_string(); - let event_states = view.counts.event_states.to_string(); - render_pairs( - stdout, - "counts", - &[ - ("farms", farms.as_str()), - ("listings", listings.as_str()), - ("profiles", profiles.as_str()), - ("relays", relays.as_str()), - ("event states", event_states.as_str()), - ], - )?; - } - if let Some(reason) = &view.reason { - writeln!(stdout, "reason: {reason}")?; - } - writeln!(stdout, "source: {}", view.source)?; - render_actions(stdout, &view.actions)?; - Ok(()) -} - -fn render_setup(stdout: &mut dyn Write, view: &SetupView) -> Result<(), RuntimeError> { - render_checklist_summary( - stdout, - match view.state.as_str() { - "unconfigured" => "Not ready yet", - _ => "Setup saved", - }, - &view.ready, - &view.needs_attention, - &view.next, - )?; - writeln!(stdout)?; - render_account_resolution(stdout, &view.account_resolution) -} - -fn render_status_summary(stdout: &mut dyn Write, view: &StatusView) -> Result<(), RuntimeError> { - render_checklist_summary( - stdout, - match view.state.as_str() { - "unconfigured" => "Not ready yet", - _ => "Status", - }, - &view.ready, - &view.needs_attention, - &view.next, - )?; - writeln!(stdout)?; - render_account_resolution(stdout, &view.account_resolution) -} - -fn render_checklist_summary( - stdout: &mut dyn Write, - headline: &str, - ready: &[String], - needs_attention: &[String], - next: &[String], -) -> Result<(), RuntimeError> { - writeln!(stdout, "{headline}")?; - - let mut wrote_section = false; - if !ready.is_empty() || !needs_attention.is_empty() || !next.is_empty() { - writeln!(stdout)?; - } - - if !ready.is_empty() { - render_item_section(stdout, "Ready", ready)?; - wrote_section = true; - } - - if !needs_attention.is_empty() { - if wrote_section { - writeln!(stdout)?; - } - render_item_section(stdout, "Needs attention", needs_attention)?; - wrote_section = true; - } - - if !next.is_empty() { - if wrote_section { - writeln!(stdout)?; - } - render_item_section(stdout, "Next", next)?; - } - - Ok(()) -} - -fn render_item_section( - stdout: &mut dyn Write, - title: &str, - items: &[String], -) -> Result<(), RuntimeError> { - writeln!(stdout, "{title}")?; - for item in items { - writeln!(stdout, " {item}")?; - } - Ok(()) -} - -fn push_row(rows: &mut Vec<(&'static str, String)>, label: &'static str, value: Option<String>) { - if let Some(value) = value.filter(|value| !value.trim().is_empty()) { - rows.push((label, value)); - } -} - -fn first_present<const N: usize>(values: [Option<&str>; N]) -> Option<String> { - values - .into_iter() - .flatten() - .find_map(|value| non_empty_str(value).map(str::to_owned)) -} - -fn non_empty_str(value: &str) -> Option<&str> { - let trimmed = value.trim(); - if trimmed.is_empty() { - None - } else { - Some(trimmed) - } -} - -fn humanize_machine_label(value: &str) -> String { - value - .split('_') - .filter(|segment| !segment.is_empty()) - .map(capitalize_ascii_word) - .collect::<Vec<_>>() - .join(" ") -} - -fn order_submit_watch_headline(view: &OrderSubmitView) -> &'static str { - match view.state.as_str() { - "already_submitted" => "Order already submitted", - "deduplicated" => "Order already in progress", - "dry_run" => "Dry run only", - "error" => "Order submit failed", - "missing" => "Order draft not found", - "unavailable" => "Order submit unavailable", - "unconfigured" => "Not ready yet", - _ => "Order submitted", - } -} - -fn humanize_delivery_method(value: &str) -> String { - value - .split('_') - .filter(|segment| !segment.is_empty()) - .map(capitalize_ascii_word) - .collect::<Vec<_>>() - .join(" ") -} - -fn capitalize_ascii_word(word: &str) -> String { - let mut chars = word.chars(); - let Some(first) = chars.next() else { - return String::new(); - }; - let mut rendered = String::new(); - rendered.push(first.to_ascii_uppercase()); - rendered.push_str(chars.as_str()); - rendered -} - -fn render_local_backup(stdout: &mut dyn Write, view: &LocalBackupView) -> Result<(), RuntimeError> { - write_context(stdout, format!("local · {}", view.state).as_str())?; - let size_bytes = view.size_bytes.to_string(); - let mut rows = vec![("file", view.file.as_str())]; - if view.state != "unconfigured" { - rows.push(("size bytes", size_bytes.as_str())); - rows.push(("backup format version", view.backup_format_version.as_str())); - rows.push(("replica db version", view.replica_db_version.as_str())); - } - render_pairs(stdout, "backup", rows.as_slice())?; - if let Some(reason) = &view.reason { - writeln!(stdout, "reason: {reason}")?; - } - writeln!(stdout, "source: {}", view.source)?; - render_actions(stdout, &view.actions)?; - Ok(()) -} - -fn render_local_export(stdout: &mut dyn Write, view: &LocalExportView) -> Result<(), RuntimeError> { - write_context(stdout, format!("local · {}", view.state).as_str())?; - let records = view.records.to_string(); - let mut rows = vec![ - ("format", view.format.as_str()), - ("file", view.file.as_str()), - ]; - if view.state != "unconfigured" { - rows.push(("records", records.as_str())); - rows.push(("export version", view.export_version.as_str())); - rows.push(("schema hash", view.schema_hash.as_str())); - } - render_pairs(stdout, "export", rows.as_slice())?; - if let Some(reason) = &view.reason { - writeln!(stdout, "reason: {reason}")?; - } - writeln!(stdout, "source: {}", view.source)?; - render_actions(stdout, &view.actions)?; - Ok(()) -} - -fn doctor_item(check: &DoctorCheckView) -> String { - let name = humanize_machine_label(check.name.as_str()); - match non_empty_str(check.detail.as_str()) { - Some(detail) => format!("{name}: {detail}"), - None => name, - } -} - -fn write_context(stdout: &mut dyn Write, line: &str) -> Result<(), RuntimeError> { - writeln!(stdout, "{line}")?; - writeln!(stdout)?; - Ok(()) -} - -fn render_actions(stdout: &mut dyn Write, actions: &[String]) -> Result<(), RuntimeError> { - if actions.is_empty() { - return Ok(()); - } - writeln!(stdout)?; - writeln!(stdout, "Next")?; - for action in actions { - writeln!(stdout, " {action}")?; - } - Ok(()) -} - -fn render_pairs( - stdout: &mut dyn Write, - heading: &str, - rows: &[(&str, &str)], -) -> Result<(), RuntimeError> { - writeln!(stdout, "{heading}")?; - let label_width = rows - .iter() - .map(|(label, _)| label.len()) - .max() - .unwrap_or_default(); - for (label, value) in rows { - writeln!(stdout, " {label:label_width$} {value}")?; - } - writeln!(stdout)?; - Ok(()) -} - -fn render_field_rows(stdout: &mut dyn Write, rows: &[(&str, String)]) -> Result<(), RuntimeError> { - let label_width = rows - .iter() - .map(|(label, _)| label.len()) - .max() - .unwrap_or_default(); - for (label, value) in rows { - writeln!(stdout, " {label:label_width$} {value}")?; - } - writeln!(stdout)?; - Ok(()) -} - -fn render_owned_pairs( - stdout: &mut dyn Write, - heading: &str, - rows: &[(&str, String)], -) -> Result<(), RuntimeError> { - let borrowed = rows - .iter() - .map(|(label, value)| (*label, value.as_str())) - .collect::<Vec<_>>(); - render_pairs(stdout, heading, borrowed.as_slice()) -} - -fn render_local_signer( - stdout: &mut dyn Write, - heading: &str, - local: &crate::domain::runtime::LocalSignerStatusView, -) -> Result<(), RuntimeError> { - writeln!(stdout, "{heading}")?; - writeln!(stdout, " account id: {}", local.account_id)?; - writeln!( - stdout, - " public key hex: {}", - local.public_identity.public_key_hex - )?; - writeln!( - stdout, - " public key npub: {}", - local.public_identity.public_key_npub - )?; - writeln!(stdout, " availability: {}", local.availability)?; - writeln!(stdout, " secret backed: {}", yes_no(local.secret_backed))?; - writeln!(stdout, " backend: {}", local.backend)?; - writeln!(stdout, " used fallback: {}", yes_no(local.used_fallback))?; - Ok(()) -} - -fn render_myc_status( - stdout: &mut dyn Write, - view: &crate::domain::runtime::MycStatusView, - standalone: bool, -) -> Result<(), RuntimeError> { - if standalone { - write_context(stdout, format!("myc · {}", view.state).as_str())?; - } - let mut rows = vec![ - ("executable", view.executable.as_str()), - ("status", view.state.as_str()), - ("ready", yes_no(view.ready)), - ]; - if let Some(service_status) = &view.service_status { - rows.push(("service status", service_status.as_str())); - } - let remote_session_count = view.remote_session_count.to_string(); - rows.push(("remote session count", remote_session_count.as_str())); - render_pairs(stdout, "myc", rows.as_slice())?; - if let Some(reason) = &view.reason { - writeln!(stdout, "reason: {reason}")?; - } - if !view.reasons.is_empty() { - writeln!(stdout, "reasons: {}", view.reasons.join(" | "))?; - } - writeln!(stdout, "source: {}", view.source)?; - if let Some(local_signer) = &view.local_signer { - writeln!(stdout)?; - render_local_signer(stdout, "myc local signer", local_signer)?; - } - for session in &view.remote_sessions { - writeln!(stdout)?; - render_myc_remote_session(stdout, session)?; - } - if let Some(custody) = &view.custody { - writeln!(stdout)?; - render_myc_custody_identity(stdout, "myc custody signer", &custody.signer)?; - render_myc_custody_identity(stdout, "myc custody user", &custody.user)?; - if let Some(discovery_app) = &custody.discovery_app { - render_myc_custody_identity(stdout, "myc custody discovery app", discovery_app)?; - } - } - Ok(()) -} - -fn render_signer_binding( - stdout: &mut dyn Write, - binding: &crate::domain::runtime::SignerBindingStatusView, -) -> Result<(), RuntimeError> { - writeln!(stdout, "signer binding")?; - writeln!(stdout, " capability: {}", binding.capability_id)?; - writeln!(stdout, " provider: {}", binding.provider_runtime_id)?; - writeln!(stdout, " model: {}", binding.binding_model)?; - writeln!(stdout, " status: {}", binding.state)?; - writeln!(stdout, " source: {}", binding.source)?; - if let Some(target_kind) = &binding.target_kind { - writeln!(stdout, " target kind: {target_kind}")?; - } - if let Some(target) = &binding.target { - writeln!(stdout, " target: {target}")?; - } - if let Some(account_ref) = &binding.managed_account_ref { - writeln!(stdout, " managed account ref: {account_ref}")?; - } - if let Some(session_ref) = &binding.signer_session_ref { - writeln!(stdout, " signer session ref: {session_ref}")?; - } - if let Some(session_id) = &binding.resolved_signer_session_id { - writeln!(stdout, " resolved signer session id: {session_id}")?; - } - if let Some(count) = binding.matched_session_count { - writeln!(stdout, " matched session count: {count}")?; - } - if let Some(reason) = &binding.reason { - writeln!(stdout, " reason: {reason}")?; - } - Ok(()) -} - -fn render_myc_remote_session( - stdout: &mut dyn Write, - session: &crate::domain::runtime::MycRemoteSessionView, -) -> Result<(), RuntimeError> { - writeln!(stdout, "myc remote session {}", session.connection_id)?; - writeln!(stdout, " signer id: {}", session.signer_identity.id)?; - writeln!( - stdout, - " signer npub: {}", - session.signer_identity.public_key_npub - )?; - writeln!(stdout, " user id: {}", session.user_identity.id)?; - writeln!( - stdout, - " user npub: {}", - session.user_identity.public_key_npub - )?; - writeln!(stdout, " relay count: {}", session.relay_count)?; - if !session.permissions.is_empty() { - writeln!(stdout, " permissions: {}", session.permissions.join(", "))?; - } - Ok(()) -} - -fn render_signer_write_kinds( - stdout: &mut dyn Write, - write_kinds: &[SignerWriteKindReadinessView], -) -> Result<(), RuntimeError> { - let table = Table { - headers: &["command", "kind", "ready", "permission"], - rows: write_kinds - .iter() - .map(|kind| { - vec![ - kind.command.clone(), - kind.event_kind.to_string(), - yes_no(kind.ready).to_owned(), - kind.permission.clone(), - ] - }) - .collect(), - }; - render_table(stdout, &table)?; - let reasons = write_kinds - .iter() - .filter_map(|kind| { - kind.reason - .as_ref() - .map(|reason| (kind.command.as_str(), reason)) - }) - .collect::<Vec<_>>(); - if !reasons.is_empty() { - writeln!(stdout)?; - for (command, reason) in reasons { - writeln!(stdout, "{command}: {reason}")?; - } - } - Ok(()) -} - -fn render_myc_custody_identity( - stdout: &mut dyn Write, - heading: &str, - identity: &crate::domain::runtime::MycCustodyIdentityView, -) -> Result<(), RuntimeError> { - writeln!(stdout, "{heading}")?; - writeln!(stdout, " resolved: {}", yes_no(identity.resolved))?; - if let Some(selected_account_id) = &identity.selected_account_id { - writeln!(stdout, " selected account id: {selected_account_id}")?; - } - if let Some(selected_account_state) = &identity.selected_account_state { - writeln!(stdout, " selected account state: {selected_account_state}")?; - } - if let Some(identity_id) = &identity.identity_id { - writeln!(stdout, " identity id: {identity_id}")?; - } - if let Some(public_key_hex) = &identity.public_key_hex { - writeln!(stdout, " public key hex: {public_key_hex}")?; - } - if let Some(error) = &identity.error { - writeln!(stdout, " error: {error}")?; - } - Ok(()) -} - -fn render_table(stdout: &mut dyn Write, table: &Table) -> Result<(), RuntimeError> { - let mut widths: Vec<usize> = table.headers.iter().map(|header| header.len()).collect(); - for row in &table.rows { - for (index, cell) in row.iter().enumerate() { - if let Some(width) = widths.get_mut(index) { - *width = (*width).max(cell.len()); - } - } - } - - for (index, header) in table.headers.iter().enumerate() { - if index > 0 { - write!(stdout, " ")?; - } - write!(stdout, "{header:width$}", width = widths[index])?; - } - writeln!(stdout)?; - - for row in &table.rows { - for (index, cell) in row.iter().enumerate() { - if index > 0 { - write!(stdout, " ")?; - } - write!(stdout, "{cell:width$}", width = widths[index])?; - } - writeln!(stdout)?; - } - - Ok(()) -} - -fn format_price(amount: f64, currency: &str, per_amount: u32, per_unit: &str) -> String { - format!( - "{} {currency}/{} {per_unit}", - trim_decimal(amount), - per_amount - ) -} - -fn quantity_offer_text(quantity: &crate::domain::runtime::FindQuantityView) -> Option<String> { - quantity - .label - .as_deref() - .and_then(non_empty_str) - .map(str::to_owned) - .or_else(|| Some(format!("{} {}", quantity.total_amount, quantity.total_unit))) -} - -fn format_available(amount: i64, unit: &str) -> String { - format!("{amount} {unit}") -} - -fn trim_decimal(value: f64) -> String { - let formatted = format!("{value:.2}"); - formatted - .trim_end_matches('0') - .trim_end_matches('.') - .to_owned() -} - -struct Table { - headers: &'static [&'static str], - rows: Vec<Vec<String>>, -} - -fn human_command_name(view: &CommandView) -> &'static str { - match view { - CommandView::AccountClearDefault(_) => "account clear-default", - CommandView::AccountImport(_) => "account import", - CommandView::AccountList(_) => "account list", - CommandView::AccountNew(_) => "account create", - CommandView::AccountRemove(_) => "account remove", - CommandView::AccountUse(_) => "account select", - CommandView::AccountWhoami(_) => "account view", - CommandView::ConfigShow(_) => "config show", - CommandView::Doctor(_) => "doctor", - CommandView::FarmGet(_) => "farm show", - CommandView::FarmPublish(_) => "farm publish", - CommandView::FarmSet(_) => "farm set", - CommandView::FarmSetup(view) => { - if view.state == "saved" { - "farm init" - } else { - "farm setup" - } - } - CommandView::FarmStatus(_) => "farm check", - CommandView::Find(_) => "find", - CommandView::JobGet(_) => "job get", - CommandView::JobList(_) => "job ls", - CommandView::JobWatch(_) => "job watch", - CommandView::ListingGet(_) => "listing get", - CommandView::ListingMutation(view) => match view.operation.as_str() { - "publish" => "listing publish", - "update" => "listing update", - "archive" => "listing archive", - _ => "listing publish", - }, - CommandView::ListingNew(_) => "listing new", - CommandView::ListingValidate(_) => "listing validate", - CommandView::LocalBackup(_) => "local backup", - CommandView::LocalExport(_) => "local export", - CommandView::LocalInit(_) => "local init", - CommandView::LocalStatus(_) => "local status", - CommandView::MarketSearch(_) => "market search", - CommandView::MarketUpdate(_) => "market update", - CommandView::MarketView(_) => "market view", - CommandView::MycStatus(_) => "myc status", - CommandView::NetStatus(_) => "net status", - CommandView::OrderCancel(_) => "order cancel", - CommandView::OrderGet(_) => "order view", - CommandView::OrderHistory(_) => "order history", - CommandView::OrderList(_) => "order list", - CommandView::OrderNew(_) => "order create", - CommandView::OrderSubmit(_) => "order submit", - CommandView::OrderSubmitWatch(_) => "order submit --watch", - CommandView::OrderWatch(_) => "order watch", - CommandView::RpcSessions(_) => "rpc sessions", - CommandView::RpcStatus(_) => "rpc status", - CommandView::RelayList(_) => "relay ls", - CommandView::RuntimeAction(view) => match view.action.as_str() { - "install" => "runtime install", - "uninstall" => "runtime uninstall", - "start" => "runtime start", - "stop" => "runtime stop", - "restart" => "runtime restart", - "config_set" => "runtime config set", - _ => "runtime", - }, - CommandView::RuntimeConfigShow(_) => "runtime config show", - CommandView::RuntimeLogs(_) => "runtime logs", - CommandView::RuntimeStatus(_) => "runtime status", - CommandView::SellAdd(_) => "sell add", - CommandView::SellCheck(_) => "sell check", - CommandView::SellDraftMutation(view) => match view.operation.as_str() { - "reprice" => "sell reprice", - "restock" => "sell restock", - _ => "sell", - }, - CommandView::SellMutation(view) => match view.operation.as_str() { - "publish" => "sell publish", - "update" => "sell update", - "pause" => "sell pause", - _ => "sell", - }, - CommandView::SellShow(_) => "sell show", - CommandView::Setup(_) => "setup", - CommandView::SignerSessionAction(_) => "signer session", - CommandView::SignerStatus(_) => "signer status", - CommandView::Status(_) => "status", - CommandView::SyncPull(_) => "sync pull", - CommandView::SyncPush(_) => "sync push", - CommandView::SyncStatus(_) => "sync status", - CommandView::SyncWatch(_) => "sync watch", - } -} - -#[cfg(test)] -mod tests { - use super::{ - Table, render_human_to, render_human_with_config_to, render_ndjson_to, render_table, - }; - use crate::commands::runtime; - use crate::domain::runtime::{ - AccountListView, CommandOutput, CommandView, DoctorCheckView, DoctorView, MycStatusView, - RelayEntryView, RelayListView, - }; - use crate::runtime::config::{ - AccountConfig, AccountSecretContractConfig, HyfConfig, IdentityConfig, InteractionConfig, - LocalConfig, LoggingConfig, MigrationConfig, MycConfig, OutputConfig, OutputFormat, - PathsConfig, RelayConfig, RelayConfigSource, RelayPublishPolicy, RpcConfig, RuntimeConfig, - SignerBackend, SignerConfig, Verbosity, - }; - use crate::runtime::logging::LoggingState; - use radroots_runtime_paths::RadrootsMigrationReport; - use radroots_secret_vault::RadrootsSecretBackend; - - #[test] - fn human_render_contains_config_sections() { - let view = runtime::show( - &RuntimeConfig { - output: OutputConfig { - format: OutputFormat::Human, - verbosity: Verbosity::Normal, - color: true, - dry_run: false, - }, - interaction: InteractionConfig { - input_enabled: true, - assume_yes: false, - stdin_tty: true, - stdout_tty: true, - prompts_allowed: true, - confirmations_allowed: true, - }, - paths: PathsConfig { - profile: "interactive_user".into(), - profile_source: "default".into(), - allowed_profiles: vec!["interactive_user".into(), "repo_local".into()], - root_source: "host_defaults".into(), - repo_local_root: None, - repo_local_root_source: None, - subordinate_path_override_source: "runtime_config".into(), - app_namespace: "apps/cli".into(), - shared_accounts_namespace: "shared/accounts".into(), - shared_identities_namespace: "shared/identities".into(), - app_config_path: "/home/tester/.radroots/config/apps/cli/config.toml".into(), - workspace_config_path: None, - app_data_root: "/home/tester/.radroots/data/apps/cli".into(), - app_logs_root: "/home/tester/.radroots/logs/apps/cli".into(), - shared_accounts_data_root: "/home/tester/.radroots/data/shared/accounts".into(), - shared_accounts_secrets_root: "/home/tester/.radroots/secrets/shared/accounts" - .into(), - default_identity_path: - "/home/tester/.radroots/secrets/shared/identities/default.json".into(), - }, - migration: MigrationConfig { - report: RadrootsMigrationReport::empty(), - }, - logging: LoggingConfig { - filter: "info".to_owned(), - directory: None, - stdout: false, - }, - account: AccountConfig { - selector: Some("acct_demo".into()), - store_path: "/home/tester/.radroots/data/shared/accounts/store.json".into(), - secrets_dir: "/home/tester/.radroots/secrets/shared/accounts".into(), - secret_backend: RadrootsSecretBackend::EncryptedFile, - secret_fallback: None, - }, - account_secret_contract: AccountSecretContractConfig { - default_backend: "host_vault".into(), - default_fallback: Some("encrypted_file".into()), - allowed_backends: vec!["host_vault".into(), "encrypted_file".into()], - host_vault_policy: Some("desktop".into()), - uses_protected_store: true, - }, - identity: IdentityConfig { - path: "/home/tester/.radroots/secrets/shared/identities/default.json".into(), - }, - signer: SignerConfig { - backend: SignerBackend::Local, - }, - relay: RelayConfig { - urls: vec!["wss://relay.one".into(), "wss://relay.two".into()], - publish_policy: RelayPublishPolicy::Any, - source: RelayConfigSource::WorkspaceConfig, - }, - local: LocalConfig { - root: "/home/tester/.radroots/data/apps/cli/replica".into(), - replica_db_path: "/home/tester/.radroots/data/apps/cli/replica/replica.sqlite" - .into(), - backups_dir: "/home/tester/.radroots/data/apps/cli/replica/backups".into(), - exports_dir: "/home/tester/.radroots/data/apps/cli/replica/exports".into(), - }, - myc: MycConfig { - executable: "myc".into(), - status_timeout_ms: 2_000, - }, - hyf: HyfConfig { - enabled: false, - executable: "hyfd".into(), - }, - rpc: RpcConfig { - url: "http://127.0.0.1:7070".to_owned(), - bridge_bearer_token: None, - }, - capability_bindings: Vec::new(), - }, - &LoggingState { - initialized: true, - current_file: None, - }, - ) - .expect("runtime show"); - assert_eq!(view.output.format, "human"); - assert!(view.interaction.input_enabled); - assert!(view.interaction.prompts_allowed); - assert_eq!(view.paths.profile, "interactive_user"); - assert_eq!(view.paths.app_namespace, "apps/cli"); - assert_eq!(view.paths.shared_accounts_namespace, "shared/accounts"); - assert!(!view.paths.workspace_config_enabled); - assert_eq!(view.paths.workspace_config_path, None); - assert_eq!(view.account.selector.as_deref(), Some("acct_demo")); - assert!( - view.account - .store_path - .ends_with(".radroots/data/shared/accounts/store.json") - ); - assert_eq!(view.relay.count, 2); - assert_eq!(view.relay.publish_policy, "any"); - assert!(!view.hyf.enabled); - assert_eq!(view.hyf.executable, "hyfd"); - assert_eq!(view.capability_bindings.len(), 4); - assert_eq!( - view.account.secret_backend.contract_default_backend, - "host_vault" - ); - assert!( - view.local - .replica_db_path - .ends_with(".radroots/data/apps/cli/replica/replica.sqlite") - ); - } - - #[test] - fn human_render_omits_placeholder_tokens() { - let output = CommandOutput::success(CommandView::MycStatus(MycStatusView { - executable: "myc".to_owned(), - state: "unavailable".to_owned(), - source: "myc status command · local first".to_owned(), - service_status: None, - ready: false, - reason: None, - reasons: Vec::new(), - remote_session_count: 0, - local_signer: None, - remote_sessions: Vec::new(), - custody: None, - })); - let mut buffer = Vec::new(); - render_human_to(&mut buffer, &output).expect("render human"); - let rendered = String::from_utf8(buffer).expect("utf8"); - assert!(!rendered.contains("<none>")); - assert!(!rendered.contains("<unknown>")); - assert!(!rendered.contains("<disabled>")); - } - - #[test] - fn ndjson_rejects_singular_views() { - let output = CommandOutput::success(CommandView::ConfigShow( - runtime::show( - &RuntimeConfig { - output: OutputConfig { - format: OutputFormat::Ndjson, - verbosity: Verbosity::Trace, - color: false, - dry_run: true, - }, - interaction: InteractionConfig { - input_enabled: true, - assume_yes: false, - stdin_tty: true, - stdout_tty: true, - prompts_allowed: true, - confirmations_allowed: true, - }, - paths: PathsConfig { - profile: "interactive_user".into(), - profile_source: "default".into(), - allowed_profiles: vec!["interactive_user".into(), "repo_local".into()], - root_source: "host_defaults".into(), - repo_local_root: None, - repo_local_root_source: None, - subordinate_path_override_source: "runtime_config".into(), - app_namespace: "apps/cli".into(), - shared_accounts_namespace: "shared/accounts".into(), - shared_identities_namespace: "shared/identities".into(), - app_config_path: "/home/tester/.radroots/config/apps/cli/config.toml" - .into(), - workspace_config_path: None, - app_data_root: "/home/tester/.radroots/data/apps/cli".into(), - app_logs_root: "/home/tester/.radroots/logs/apps/cli".into(), - shared_accounts_data_root: "/home/tester/.radroots/data/shared/accounts" - .into(), - shared_accounts_secrets_root: - "/home/tester/.radroots/secrets/shared/accounts".into(), - default_identity_path: - "/home/tester/.radroots/secrets/shared/identities/default.json".into(), - }, - migration: MigrationConfig { - report: RadrootsMigrationReport::empty(), - }, - logging: LoggingConfig { - filter: "info".to_owned(), - directory: None, - stdout: false, - }, - account: AccountConfig { - selector: None, - store_path: "/home/tester/.radroots/data/shared/accounts/store.json".into(), - secrets_dir: "/home/tester/.radroots/secrets/shared/accounts".into(), - secret_backend: RadrootsSecretBackend::EncryptedFile, - secret_fallback: None, - }, - account_secret_contract: AccountSecretContractConfig { - default_backend: "host_vault".into(), - default_fallback: Some("encrypted_file".into()), - allowed_backends: vec!["host_vault".into(), "encrypted_file".into()], - host_vault_policy: Some("desktop".into()), - uses_protected_store: true, - }, - identity: IdentityConfig { - path: "/home/tester/.radroots/secrets/shared/identities/default.json" - .into(), - }, - signer: SignerConfig { - backend: SignerBackend::Local, - }, - relay: RelayConfig { - urls: Vec::new(), - publish_policy: RelayPublishPolicy::Any, - source: RelayConfigSource::Defaults, - }, - local: LocalConfig { - root: "/home/tester/.radroots/data/apps/cli/replica".into(), - replica_db_path: - "/home/tester/.radroots/data/apps/cli/replica/replica.sqlite".into(), - backups_dir: "/home/tester/.radroots/data/apps/cli/replica/backups".into(), - exports_dir: "/home/tester/.radroots/data/apps/cli/replica/exports".into(), - }, - myc: MycConfig { - executable: "myc".into(), - status_timeout_ms: 2_000, - }, - hyf: HyfConfig { - enabled: false, - executable: "hyfd".into(), - }, - rpc: RpcConfig { - url: "http://127.0.0.1:7070".to_owned(), - bridge_bearer_token: None, - }, - capability_bindings: Vec::new(), - }, - &LoggingState { - initialized: true, - current_file: None, - }, - ) - .expect("runtime show"), - )); - let mut buffer = Vec::new(); - let error = render_ndjson_to(&mut buffer, &output).expect_err("unsupported ndjson"); - assert!( - error - .to_string() - .contains("`config show` does not support --ndjson") - ); - } - - #[test] - fn account_list_ndjson_emits_one_json_object_per_account() { - let output = CommandOutput::success(CommandView::AccountList(AccountListView { - source: "shared account store · local first".to_owned(), - count: 2, - accounts: vec![ - crate::domain::runtime::AccountSummaryView { - id: "acct_a".to_owned(), - display_name: Some("Alpha".to_owned()), - signer: "local".to_owned(), - is_default: true, - }, - crate::domain::runtime::AccountSummaryView { - id: "acct_b".to_owned(), - display_name: None, - signer: "local".to_owned(), - is_default: false, - }, - ], - actions: Vec::new(), - })); - let mut buffer = Vec::new(); - render_ndjson_to(&mut buffer, &output).expect("render ndjson"); - let rendered = String::from_utf8(buffer).expect("utf8"); - let lines = rendered.lines().collect::<Vec<_>>(); - assert_eq!(lines.len(), 2); - assert!(lines[0].contains("\"id\":\"acct_a\"")); - assert!(lines[1].contains("\"id\":\"acct_b\"")); - } - - #[test] - fn relay_list_ndjson_emits_one_json_object_per_relay() { - let output = CommandOutput::success(CommandView::RelayList(RelayListView { - state: "configured".to_owned(), - source: "workspace config · local first".to_owned(), - publish_policy: "any".to_owned(), - count: 2, - reason: None, - relays: vec![ - RelayEntryView { - url: "wss://relay.one".to_owned(), - read: true, - write: true, - }, - RelayEntryView { - url: "wss://relay.two".to_owned(), - read: true, - write: true, - }, - ], - actions: Vec::new(), - })); - let mut buffer = Vec::new(); - render_ndjson_to(&mut buffer, &output).expect("render relay ndjson"); - let rendered = String::from_utf8(buffer).expect("utf8"); - let lines = rendered.lines().collect::<Vec<_>>(); - assert_eq!(lines.len(), 2); - assert!(lines[0].contains("\"url\":\"wss://relay.one\"")); - assert!(lines[1].contains("\"url\":\"wss://relay.two\"")); - } - - #[test] - fn human_render_doctor_uses_readiness_sections() { - let output = CommandOutput::unconfigured(CommandView::Doctor(DoctorView { - ok: false, - state: "warn".to_owned(), - account_resolution: crate::domain::runtime::AccountResolutionView { - source: "none".to_owned(), - resolved_account: None, - default_account: None, - }, - checks: vec![ - DoctorCheckView { - name: "config".to_owned(), - status: "ok".to_owned(), - detail: "defaults active".to_owned(), - }, - DoctorCheckView { - name: "account".to_owned(), - status: "warn".to_owned(), - detail: "no local account in store".to_owned(), - }, - ], - source: "local diagnostics".to_owned(), - actions: vec!["radroots account new".to_owned()], - })); - let mut buffer = Vec::new(); - render_human_to(&mut buffer, &output).expect("render human"); - let rendered = String::from_utf8(buffer).expect("utf8"); - assert!(rendered.contains("Readiness check")); - assert!(rendered.contains("Ready")); - assert!(rendered.contains("Config: defaults active")); - assert!(rendered.contains("Needs attention")); - assert!(rendered.contains("Account: no local account in store")); - assert!(rendered.contains("Next")); - assert!(rendered.contains("radroots account new")); - assert!(!rendered.contains("source: local diagnostics")); - } - - #[test] - fn human_render_verbose_and_trace_append_diagnostics() { - let output = CommandOutput::success(CommandView::Doctor(DoctorView { - ok: true, - state: "ok".to_owned(), - account_resolution: crate::domain::runtime::AccountResolutionView { - source: "default_account".to_owned(), - resolved_account: None, - default_account: None, - }, - checks: vec![DoctorCheckView { - name: "config".to_owned(), - status: "ok".to_owned(), - detail: "defaults active".to_owned(), - }], - source: "local diagnostics".to_owned(), - actions: Vec::new(), - })); - - let mut verbose_buffer = Vec::new(); - render_human_with_config_to( - &mut verbose_buffer, - &output, - &OutputConfig { - format: OutputFormat::Human, - verbosity: Verbosity::Verbose, - color: false, - dry_run: false, - }, - ) - .expect("render verbose"); - let verbose_rendered = String::from_utf8(verbose_buffer).expect("utf8"); - assert!(verbose_rendered.contains("Details")); - assert!(verbose_rendered.contains("Source")); - assert!(!verbose_rendered.contains("Trace")); - - let mut trace_buffer = Vec::new(); - render_human_with_config_to( - &mut trace_buffer, - &output, - &OutputConfig { - format: OutputFormat::Human, - verbosity: Verbosity::Trace, - color: false, - dry_run: false, - }, - ) - .expect("render trace"); - let trace_rendered = String::from_utf8(trace_buffer).expect("utf8"); - assert!(trace_rendered.contains("Details")); - assert!(trace_rendered.contains("Trace")); - assert!(trace_rendered.contains("\"source\": \"local diagnostics\"")); - } - - #[test] - fn table_renderer_aligns_columns() { - let table = Table { - headers: &["item", "status"], - rows: vec![ - vec!["alpha".to_owned(), "ready".to_owned()], - vec!["beta-long".to_owned(), "pending".to_owned()], - ], - }; - let mut buffer = Vec::new(); - render_table(&mut buffer, &table).expect("render table"); - let rendered = String::from_utf8(buffer).expect("utf8"); - assert!(rendered.contains("item status")); - assert!(rendered.contains("alpha ready")); - assert!(rendered.contains("beta-long pending")); - } -} diff --git a/src/runtime/config.rs b/src/runtime/config.rs @@ -12,10 +12,10 @@ use radroots_secret_vault::{RadrootsHostVaultPolicy, RadrootsSecretBackend}; use serde::Deserialize; use url::Url; -use crate::cli::CliArgs; use crate::runtime::RuntimeError; pub use crate::runtime::paths::PathsConfig; use crate::runtime::paths::{ENV_CLI_PATHS_PROFILE, ENV_CLI_PATHS_REPO_LOCAL_ROOT, resolve_paths}; +use crate::runtime_args::RuntimeInvocationArgs; const DEFAULT_LOG_FILTER: &str = "info"; const DEFAULT_ENV_PATH: &str = ".env"; @@ -470,7 +470,7 @@ impl Environment for SystemEnvironment { } impl RuntimeConfig { - pub fn from_system(args: &CliArgs) -> Result<Self, RuntimeError> { + pub fn from_system(args: &RuntimeInvocationArgs) -> Result<Self, RuntimeError> { let system = SystemEnvironment; let env_file_path = resolve_env_file_path(args, &system); let env_file = load_env_file_values(env_file_path.as_deref())?; @@ -478,7 +478,7 @@ impl RuntimeConfig { } fn resolve_with_env_file( - args: &CliArgs, + args: &RuntimeInvocationArgs, env: &dyn Environment, env_file: &EnvFileValues, ) -> Result<Self, RuntimeError> { @@ -904,7 +904,7 @@ fn normalize_binding_ref(value: Option<&str>) -> Option<String> { } fn resolve_relay_config( - args: &CliArgs, + args: &RuntimeInvocationArgs, env: &dyn Environment, env_file: &EnvFileValues, user_config: Option<&CliConfigFile>, @@ -957,7 +957,7 @@ fn resolve_relay_config( } fn resolve_signer_config( - args: &CliArgs, + args: &RuntimeInvocationArgs, env: &dyn Environment, env_file: &EnvFileValues, user_config: Option<&CliConfigFile>, @@ -985,7 +985,7 @@ fn resolve_signer_config( } fn resolve_myc_config( - args: &CliArgs, + args: &RuntimeInvocationArgs, env: &dyn Environment, env_file: &EnvFileValues, user_config: Option<&CliConfigFile>, @@ -1020,7 +1020,7 @@ fn resolve_myc_config( } fn resolve_myc_status_timeout_ms( - args: &CliArgs, + args: &RuntimeInvocationArgs, env: &dyn Environment, env_file: &EnvFileValues, user_config: Option<&CliConfigFile>, @@ -1064,7 +1064,7 @@ fn validate_myc_status_timeout_ms(source: &str, value: u64) -> Result<u64, Runti } fn resolve_hyf_enabled( - args: &CliArgs, + args: &RuntimeInvocationArgs, env: &dyn Environment, env_file: &EnvFileValues, user_config: Option<&CliConfigFile>, @@ -1103,7 +1103,7 @@ fn resolve_hyf_enabled( } fn resolve_hyf_executable( - args: &CliArgs, + args: &RuntimeInvocationArgs, env: &dyn Environment, env_file: &EnvFileValues, user_config: Option<&CliConfigFile>, @@ -1221,7 +1221,7 @@ fn validate_relay_url(value: &str, source: &str) -> Result<String, RuntimeError> Ok(trimmed.to_owned()) } -fn resolve_env_file_path(args: &CliArgs, env: &dyn Environment) -> Option<PathBuf> { +fn resolve_env_file_path(args: &RuntimeInvocationArgs, env: &dyn Environment) -> Option<PathBuf> { args.env_file .clone() .or_else(|| env.var(ENV_FILE_PATH).map(PathBuf::from)) @@ -1232,7 +1232,7 @@ fn resolve_env_file_path(args: &CliArgs, env: &dyn Environment) -> Option<PathBu } fn resolve_output_format( - args: &CliArgs, + args: &RuntimeInvocationArgs, env: &dyn Environment, env_file: &EnvFileValues, ) -> Result<OutputFormat, RuntimeError> { @@ -1260,7 +1260,7 @@ fn resolve_output_format( } } -fn resolve_verbosity(args: &CliArgs) -> Result<Verbosity, RuntimeError> { +fn resolve_verbosity(args: &RuntimeInvocationArgs) -> Result<Verbosity, RuntimeError> { let selected = [args.quiet, args.verbose, args.trace] .into_iter() .filter(|selected| *selected) @@ -1282,7 +1282,10 @@ fn resolve_verbosity(args: &CliArgs) -> Result<Verbosity, RuntimeError> { } } -fn resolve_interaction_config(args: &CliArgs, env: &dyn Environment) -> InteractionConfig { +fn resolve_interaction_config( + args: &RuntimeInvocationArgs, + env: &dyn Environment, +) -> InteractionConfig { let stdin_tty = env.stdin_is_tty(); let stdout_tty = env.stdout_is_tty(); let input_enabled = !args.no_input; @@ -1436,7 +1439,7 @@ fn parse_signer_mode(source: &str, value: String) -> Result<SignerBackend, Runti } fn resolve_account_secret_backend( - _args: &CliArgs, + _args: &RuntimeInvocationArgs, env: &dyn Environment, env_file: &EnvFileValues, ) -> Result<Option<RadrootsSecretBackend>, RuntimeError> { @@ -1446,7 +1449,7 @@ fn resolve_account_secret_backend( } fn resolve_account_secret_fallback( - _args: &CliArgs, + _args: &RuntimeInvocationArgs, env: &dyn Environment, env_file: &EnvFileValues, ) -> Result<Option<Option<RadrootsSecretBackend>>, RuntimeError> { @@ -1505,7 +1508,7 @@ mod tests { PathsConfig, RelayConfigSource, RelayPublishPolicy, RuntimeConfig, SignerBackend, Verbosity, parse_env_file_values, }; - use crate::cli::CliArgs; + use crate::runtime_args::{RuntimeInvocationArgs, RuntimeOutputFormatArg}; use radroots_runtime_paths::{RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform}; use radroots_secret_vault::{RadrootsHostVaultPolicy, RadrootsSecretBackend}; use std::collections::BTreeMap; @@ -1597,36 +1600,28 @@ mod tests { } } + fn runtime_args() -> RuntimeInvocationArgs { + RuntimeInvocationArgs::default() + } + #[test] fn flags_override_environment_values() { - let args = CliArgs::parse_from([ - "radroots", - "--output", - "human", - "--verbose", - "--dry-run", - "--no-color", - "--log-filter", - "debug", - "--log-stdout", - "--identity-path", - "custom-identity.json", - "--signer", - "local", - "--relay", - "wss://relay.one", - "--relay", - "wss://relay.two", - "--myc-executable", - "bin/myc-cli", - "--myc-status-timeout-ms", - "2500", - "--hyf-enabled", - "--hyf-executable", - "bin/hyfd-cli", - "config", - "show", - ]); + let args = RuntimeInvocationArgs { + output_format: Some(RuntimeOutputFormatArg::Human), + verbose: true, + dry_run: true, + no_color: true, + log_filter: Some("debug".to_owned()), + log_stdout: true, + identity_path: Some(PathBuf::from("custom-identity.json")), + signer: Some("local".to_owned()), + relay: vec!["wss://relay.one".to_owned(), "wss://relay.two".to_owned()], + myc_executable: Some(PathBuf::from("bin/myc-cli")), + myc_status_timeout_ms: Some(2500), + hyf_enabled: true, + hyf_executable: Some(PathBuf::from("bin/hyfd-cli")), + ..runtime_args() + }; let env = MapEnvironment::new(BTreeMap::from([ ("RADROOTS_OUTPUT".to_owned(), "human".to_owned()), ("RADROOTS_LOG_FILTER".to_owned(), "trace".to_owned()), @@ -1746,7 +1741,7 @@ mod tests { #[test] fn environment_values_fill_missing_flags() { - let args = CliArgs::parse_from(["radroots", "config", "show"]); + let args = runtime_args(); let env = MapEnvironment::new(BTreeMap::from([ ("RADROOTS_OUTPUT".to_owned(), "json".to_owned()), ( @@ -1831,25 +1826,21 @@ mod tests { #[test] fn conflicting_boolean_flags_fail() { - let args = CliArgs::parse_from([ - "radroots", - "--log-stdout", - "--no-log-stdout", - "config", - "show", - ]); + let args = RuntimeInvocationArgs { + log_stdout: true, + no_log_stdout: true, + ..runtime_args() + }; let env = MapEnvironment::new(BTreeMap::new()); let error = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default()) .expect_err("conflicting flags"); assert!(error.to_string().contains("cannot be used together")); - let hyf_args = CliArgs::parse_from([ - "radroots", - "--hyf-enabled", - "--no-hyf-enabled", - "config", - "show", - ]); + let hyf_args = RuntimeInvocationArgs { + hyf_enabled: true, + no_hyf_enabled: true, + ..runtime_args() + }; let error = RuntimeConfig::resolve_with_env_file(&hyf_args, &env, &EnvFileValues::default()) .expect_err("conflicting hyf flags"); @@ -1860,8 +1851,11 @@ mod tests { fn conflicting_output_and_verbosity_flags_fail() { let env = MapEnvironment::new(BTreeMap::new()); - let conflicting_output = - CliArgs::parse_from(["radroots", "--json", "--ndjson", "config", "show"]); + let conflicting_output = RuntimeInvocationArgs { + json: true, + ndjson: true, + ..runtime_args() + }; let error = RuntimeConfig::resolve_with_env_file( &conflicting_output, &env, @@ -1870,8 +1864,11 @@ mod tests { .expect_err("conflicting output flags"); assert!(error.to_string().contains("--json and --ndjson")); - let conflicting_verbosity = - CliArgs::parse_from(["radroots", "--quiet", "--trace", "config", "show"]); + let conflicting_verbosity = RuntimeInvocationArgs { + quiet: true, + trace: true, + ..runtime_args() + }; let error = RuntimeConfig::resolve_with_env_file( &conflicting_verbosity, &env, @@ -1884,8 +1881,11 @@ mod tests { .contains("--quiet, --verbose, and --trace") ); - let conflicting_aliases = - CliArgs::parse_from(["radroots", "--output", "json", "--json", "config", "show"]); + let conflicting_aliases = RuntimeInvocationArgs { + output_format: Some(RuntimeOutputFormatArg::Json), + json: true, + ..runtime_args() + }; let error = RuntimeConfig::resolve_with_env_file( &conflicting_aliases, &env, @@ -1899,8 +1899,11 @@ mod tests { fn machine_output_rejects_stdout_logging_flags() { let env = MapEnvironment::new(BTreeMap::new()); - let json_args = - CliArgs::parse_from(["radroots", "--json", "--log-stdout", "config", "show"]); + let json_args = RuntimeInvocationArgs { + json: true, + log_stdout: true, + ..runtime_args() + }; let error = RuntimeConfig::resolve_with_env_file(&json_args, &env, &EnvFileValues::default()) .expect_err("json stdout logging should fail"); @@ -1909,8 +1912,11 @@ mod tests { assert!(message.contains("json output")); assert!(message.contains("--no-log-stdout")); - let ndjson_args = - CliArgs::parse_from(["radroots", "--ndjson", "--log-stdout", "find", "eggs"]); + let ndjson_args = RuntimeInvocationArgs { + ndjson: true, + log_stdout: true, + ..runtime_args() + }; let error = RuntimeConfig::resolve_with_env_file(&ndjson_args, &env, &EnvFileValues::default()) .expect_err("ndjson stdout logging should fail"); @@ -1921,7 +1927,10 @@ mod tests { #[test] fn machine_output_rejects_stdout_logging_environment() { - let json_args = CliArgs::parse_from(["radroots", "--json", "config", "show"]); + let json_args = RuntimeInvocationArgs { + json: true, + ..runtime_args() + }; let env = MapEnvironment::new(BTreeMap::from([( "RADROOTS_CLI_LOGGING_STDOUT".to_owned(), "true".to_owned(), @@ -1933,7 +1942,7 @@ mod tests { assert!(message.contains("RADROOTS_CLI_LOGGING_STDOUT")); assert!(message.contains("RADROOTS_LOG_STDOUT")); - let ndjson_env_args = CliArgs::parse_from(["radroots", "config", "show"]); + let ndjson_env_args = runtime_args(); let env = MapEnvironment::new(BTreeMap::from([ ("RADROOTS_OUTPUT".to_owned(), "ndjson".to_owned()), ("RADROOTS_LOG_STDOUT".to_owned(), "true".to_owned()), @@ -1946,7 +1955,11 @@ mod tests { #[test] fn no_log_stdout_overrides_environment_for_machine_output() { - let args = CliArgs::parse_from(["radroots", "--json", "--no-log-stdout", "config", "show"]); + let args = RuntimeInvocationArgs { + json: true, + no_log_stdout: true, + ..runtime_args() + }; let env = MapEnvironment::new(BTreeMap::from([( "RADROOTS_LOG_STDOUT".to_owned(), "true".to_owned(), @@ -1960,7 +1973,7 @@ mod tests { #[test] fn invalid_environment_value_fails() { - let args = CliArgs::parse_from(["radroots", "config", "show"]); + let args = runtime_args(); let env = MapEnvironment::new(BTreeMap::from([( "RADROOTS_LOG_STDOUT".to_owned(), "maybe".to_owned(), @@ -1977,8 +1990,10 @@ mod tests { .expect_err("invalid myc timeout"); assert!(error.to_string().contains("RADROOTS_MYC_STATUS_TIMEOUT_MS")); - let args = - CliArgs::parse_from(["radroots", "--myc-status-timeout-ms", "0", "config", "show"]); + let args = RuntimeInvocationArgs { + myc_status_timeout_ms: Some(0), + ..runtime_args() + }; let env = MapEnvironment::new(BTreeMap::new()); let error = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default()) .expect_err("zero myc timeout"); @@ -1987,7 +2002,7 @@ mod tests { #[test] fn env_file_values_fill_missing_flags() { - let args = CliArgs::parse_from(["radroots", "config", "show"]); + let args = runtime_args(); let env = MapEnvironment::new(BTreeMap::new()); let env_file = parse_env_file_values( r#" @@ -2035,7 +2050,10 @@ RADROOTS_HYF_EXECUTABLE=bin/hyfd #[test] fn explicit_output_flag_overrides_environment_output() { - let args = CliArgs::parse_from(["radroots", "--output", "ndjson", "find", "eggs"]); + let args = RuntimeInvocationArgs { + output_format: Some(RuntimeOutputFormatArg::Ndjson), + ..runtime_args() + }; let env = MapEnvironment::new(BTreeMap::from([( "RADROOTS_OUTPUT".to_owned(), "json".to_owned(), @@ -2048,7 +2066,11 @@ RADROOTS_HYF_EXECUTABLE=bin/hyfd #[test] fn interaction_config_reflects_tty_and_flags() { - let args = CliArgs::parse_from(["radroots", "--no-input", "--yes", "config", "show"]); + let args = RuntimeInvocationArgs { + no_input: true, + yes: true, + ..runtime_args() + }; let env = MapEnvironment::new(BTreeMap::new()).with_tty(true, true); let resolved = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default()) @@ -2065,7 +2087,7 @@ RADROOTS_HYF_EXECUTABLE=bin/hyfd } ); - let interactive_args = CliArgs::parse_from(["radroots", "config", "show"]); + let interactive_args = runtime_args(); let interactive = RuntimeConfig::resolve_with_env_file( &interactive_args, &env, @@ -2087,7 +2109,7 @@ RADROOTS_HYF_EXECUTABLE=bin/hyfd #[test] fn process_environment_overrides_env_file_values() { - let args = CliArgs::parse_from(["radroots", "config", "show"]); + let args = runtime_args(); let env = MapEnvironment::new(BTreeMap::from([ ("RADROOTS_LOG_FILTER".to_owned(), "info".to_owned()), ("RADROOTS_LOG_STDOUT".to_owned(), "true".to_owned()), @@ -2150,7 +2172,7 @@ RADROOTS_CLI_LOGGING_STDOUT=false stdin_tty: false, stdout_tty: false, }; - let args = CliArgs::parse_from(["radroots", "config", "show"]); + let args = runtime_args(); let resolved = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default()) .expect("resolve config"); @@ -2207,7 +2229,7 @@ RADROOTS_CLI_LOGGING_STDOUT=false stdin_tty: false, stdout_tty: false, }; - let args = CliArgs::parse_from(["radroots", "config", "show"]); + let args = runtime_args(); let resolved = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default()) .expect("resolve config"); @@ -2262,7 +2284,7 @@ RADROOTS_CLI_LOGGING_STDOUT=false stdin_tty: false, stdout_tty: false, }; - let args = CliArgs::parse_from(["radroots", "config", "show"]); + let args = runtime_args(); let resolved = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default()) .expect("resolve config"); @@ -2290,7 +2312,7 @@ RADROOTS_CLI_LOGGING_STDOUT=false .expect("write user config"); let env = repo_local_env(workspace_root, repo_local_root, user_home, BTreeMap::new()); - let args = CliArgs::parse_from(["radroots", "config", "show"]); + let args = runtime_args(); let resolved = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default()) .expect("resolve config"); @@ -2317,7 +2339,7 @@ RADROOTS_CLI_LOGGING_STDOUT=false user_home, BTreeMap::from([("RADROOTS_SIGNER".to_owned(), "myc".to_owned())]), ); - let args = CliArgs::parse_from(["radroots", "config", "show"]); + let args = runtime_args(); let resolved = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default()) .expect("resolve config"); @@ -2338,7 +2360,7 @@ RADROOTS_CLI_LOGGING_STDOUT=false .expect("write workspace config"); let env = repo_local_env(workspace_root, repo_local_root, user_home, BTreeMap::new()); - let args = CliArgs::parse_from(["radroots", "config", "show"]); + let args = runtime_args(); let error = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default()) .expect_err("invalid signer mode"); @@ -2401,7 +2423,7 @@ target = "bin/user-hyfd" stdin_tty: false, stdout_tty: false, }; - let args = CliArgs::parse_from(["radroots", "config", "show"]); + let args = runtime_args(); let resolved = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default()) .expect("resolve config"); @@ -2463,7 +2485,7 @@ target = "https://rpc.workspace.test/jsonrpc" stdin_tty: false, stdout_tty: false, }; - let args = CliArgs::parse_from(["radroots", "config", "show"]); + let args = runtime_args(); let error = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default()) .expect_err("invalid capability binding provider"); @@ -2476,13 +2498,10 @@ target = "https://rpc.workspace.test/jsonrpc" #[test] fn invalid_relay_url_fails() { - let args = CliArgs::parse_from([ - "radroots", - "--relay", - "https://not-a-websocket.example.com", - "relay", - "ls", - ]); + let args = RuntimeInvocationArgs { + relay: vec!["https://not-a-websocket.example.com".to_owned()], + ..runtime_args() + }; let env = MapEnvironment::new(BTreeMap::new()); let error = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default()) .expect_err("invalid relay url"); @@ -2491,7 +2510,7 @@ target = "https://rpc.workspace.test/jsonrpc" #[test] fn state_roots_are_resolved_from_home_and_workspace() { - let args = CliArgs::parse_from(["radroots", "config", "show"]); + let args = runtime_args(); let env = MapEnvironment::new(BTreeMap::new()); let resolved = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default()) .expect("resolve runtime config"); @@ -2527,7 +2546,7 @@ target = "https://rpc.workspace.test/jsonrpc" #[test] fn windows_roots_use_native_user_directories() { - let args = CliArgs::parse_from(["radroots", "config", "show"]); + let args = runtime_args(); let env = MapEnvironment { values: BTreeMap::new(), current_dir: PathBuf::from(r"C:\workspaces\radroots-cli"), @@ -2584,7 +2603,7 @@ target = "https://rpc.workspace.test/jsonrpc" #[test] fn repo_local_profile_uses_explicit_repo_local_root() { - let args = CliArgs::parse_from(["radroots", "config", "show"]); + let args = runtime_args(); let env = MapEnvironment::new(BTreeMap::from([ ( "RADROOTS_CLI_PATHS_PROFILE".to_owned(), @@ -2649,7 +2668,7 @@ target = "https://rpc.workspace.test/jsonrpc" #[test] fn repo_local_profile_requires_explicit_root() { - let args = CliArgs::parse_from(["radroots", "config", "show"]); + let args = runtime_args(); let env = MapEnvironment::new(BTreeMap::from([( "RADROOTS_CLI_PATHS_PROFILE".to_owned(), "repo_local".to_owned(), @@ -2666,7 +2685,7 @@ target = "https://rpc.workspace.test/jsonrpc" #[test] fn env_file_can_select_repo_local_profile() { - let args = CliArgs::parse_from(["radroots", "config", "show"]); + let args = runtime_args(); let env = MapEnvironment::new(BTreeMap::new()); let env_file = parse_env_file_values( r#" @@ -2708,7 +2727,7 @@ RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT=.local/radroots/dev #[test] fn env_output_accepts_ndjson() { - let args = CliArgs::parse_from(["radroots", "config", "show"]); + let args = runtime_args(); let env = MapEnvironment::new(BTreeMap::from([( "RADROOTS_OUTPUT".to_owned(), "ndjson".to_owned(), diff --git a/src/runtime/daemon.rs b/src/runtime/daemon.rs @@ -7,26 +7,19 @@ use radroots_events::trade::RadrootsTradeOrder; use radroots_sdk::{ RadrootsSdkConfig, RadrootsdAuth, SdkPublishError, SdkRadrootsdFarmPublishOptions, SdkRadrootsdListingPublishOptions, SdkRadrootsdProfilePublishOptions, - SdkRadrootsdSignerAuthority, SdkRadrootsdSignerSessionConnectRequest, - SdkRadrootsdSignerSessionHandle, SdkRadrootsdSignerSessionRef, SdkRadrootsdSignerSessionRole, - SdkRadrootsdSignerSessionView, SdkTransportMode, SignerConfig, + SdkRadrootsdSignerAuthority, SdkRadrootsdSignerSessionRef, SdkTransportMode, SignerConfig, }; use reqwest::blocking::Client; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_json::Value; -use crate::domain::runtime::{ - CommandOutput, CommandView, JobDetailView, JobSummaryView, RpcSessionView, RpcSessionsView, - RpcStatusView, SignerSessionActionView, -}; +use crate::domain::runtime::{JobDetailView, JobSummaryView}; use crate::runtime::config::RuntimeConfig; use crate::runtime::provider; -use crate::runtime::signer::{ActorWriteSignerAuthority, configured_myc_signer_authority}; +use crate::runtime::signer::ActorWriteSignerAuthority; -const RPC_SOURCE: &str = "daemon rpc · durable write plane"; const BRIDGE_SOURCE: &str = "daemon bridge · durable write plane"; -const SIGNER_SESSION_SOURCE: &str = "daemon signer session rpc · durable write plane"; const RPC_TIMEOUT_SECS: u64 = 2; #[derive(Debug)] @@ -74,27 +67,6 @@ struct JsonRpcResponseError { } #[derive(Debug, Clone, Deserialize)] -struct BridgeStatusRemote { - enabled: bool, - ready: bool, - auth_mode: String, - signer_mode: String, - default_signer_mode: String, - #[serde(default)] - supported_signer_modes: Vec<String>, - available_nip46_signer_sessions: usize, - relay_count: usize, - job_status_retention: usize, - retained_jobs: usize, - accepted_jobs: usize, - published_jobs: usize, - failed_jobs: usize, - recovered_failed_jobs: usize, - #[serde(default)] - methods: Vec<String>, -} - -#[derive(Debug, Clone, Deserialize)] struct BridgeJobRemote { job_id: String, command: String, @@ -122,17 +94,10 @@ struct BridgeJobRemote { #[derive(Debug, Clone, Deserialize)] struct Nip46SessionRemote { session_id: String, - role: String, - client_pubkey: String, - signer_pubkey: String, user_pubkey: Option<String>, #[serde(default)] - relays: Vec<String>, - #[serde(default)] permissions: Vec<String>, - auth_required: bool, authorized: bool, - expires_in_secs: Option<u64>, #[serde(default)] signer_authority: Option<ActorWriteSignerAuthority>, } @@ -175,376 +140,6 @@ pub struct BridgeOrderRequestResult { pub event_addr: Option<String>, } -pub fn status(config: &RuntimeConfig) -> CommandOutput { - match bridge_status(config) { - Ok(status) => CommandOutput::success(CommandView::RpcStatus(RpcStatusView { - state: if status.ready { - "ready".to_owned() - } else { - "degraded".to_owned() - }, - source: RPC_SOURCE.to_owned(), - url: config.rpc.url.clone(), - reason: if status.ready { - None - } else { - Some("bridge is reachable but not ready for durable publish traffic".to_owned()) - }, - auth_mode: Some(status.auth_mode), - signer_mode: Some(status.signer_mode), - default_signer_mode: Some(status.default_signer_mode), - supported_signer_modes: status.supported_signer_modes, - bridge_enabled: Some(status.enabled), - bridge_ready: Some(status.ready), - relay_count: Some(status.relay_count), - available_nip46_signer_sessions: Some(status.available_nip46_signer_sessions), - job_status_retention: Some(status.job_status_retention), - retained_jobs: Some(status.retained_jobs), - accepted_jobs: Some(status.accepted_jobs), - published_jobs: Some(status.published_jobs), - failed_jobs: Some(status.failed_jobs), - recovered_failed_jobs: Some(status.recovered_failed_jobs), - session_surface_enabled: status - .methods - .iter() - .any(|method| method == "nip46.session.list"), - methods_count: status.methods.len(), - actions: if status.ready { - Vec::new() - } else { - vec!["radroots relay list".to_owned()] - }, - })), - Err(DaemonRpcError::Unconfigured(reason)) - | Err(DaemonRpcError::Unauthorized(reason)) - | Err(DaemonRpcError::MethodUnavailable(reason)) => { - CommandOutput::unconfigured(CommandView::RpcStatus(RpcStatusView { - state: "unconfigured".to_owned(), - source: RPC_SOURCE.to_owned(), - url: config.rpc.url.clone(), - reason: Some(reason), - auth_mode: None, - signer_mode: None, - default_signer_mode: None, - supported_signer_modes: Vec::new(), - bridge_enabled: None, - bridge_ready: None, - relay_count: None, - available_nip46_signer_sessions: None, - job_status_retention: None, - retained_jobs: None, - accepted_jobs: None, - published_jobs: None, - failed_jobs: None, - recovered_failed_jobs: None, - session_surface_enabled: false, - methods_count: 0, - actions: vec![ - "set RADROOTS_RPC_BEARER_TOKEN in .env or your shell".to_owned(), - "start radrootsd with bridge ingress enabled".to_owned(), - ], - })) - } - Err(DaemonRpcError::External(reason)) => { - CommandOutput::external_unavailable(CommandView::RpcStatus(RpcStatusView { - state: "unavailable".to_owned(), - source: RPC_SOURCE.to_owned(), - url: config.rpc.url.clone(), - reason: Some(reason), - auth_mode: None, - signer_mode: None, - default_signer_mode: None, - supported_signer_modes: Vec::new(), - bridge_enabled: None, - bridge_ready: None, - relay_count: None, - available_nip46_signer_sessions: None, - job_status_retention: None, - retained_jobs: None, - accepted_jobs: None, - published_jobs: None, - failed_jobs: None, - recovered_failed_jobs: None, - session_surface_enabled: false, - methods_count: 0, - actions: vec!["start radrootsd and verify the rpc url".to_owned()], - })) - } - Err(DaemonRpcError::InvalidResponse(reason)) | Err(DaemonRpcError::Remote(reason)) => { - CommandOutput::internal_error(CommandView::RpcStatus(RpcStatusView { - state: "error".to_owned(), - source: RPC_SOURCE.to_owned(), - url: config.rpc.url.clone(), - reason: Some(reason), - auth_mode: None, - signer_mode: None, - default_signer_mode: None, - supported_signer_modes: Vec::new(), - bridge_enabled: None, - bridge_ready: None, - relay_count: None, - available_nip46_signer_sessions: None, - job_status_retention: None, - retained_jobs: None, - accepted_jobs: None, - published_jobs: None, - failed_jobs: None, - recovered_failed_jobs: None, - session_surface_enabled: false, - methods_count: 0, - actions: vec!["inspect the daemon rpc response contract".to_owned()], - })) - } - Err(DaemonRpcError::UnknownJob(reason)) => { - CommandOutput::internal_error(CommandView::RpcStatus(RpcStatusView { - state: "error".to_owned(), - source: RPC_SOURCE.to_owned(), - url: config.rpc.url.clone(), - reason: Some(reason), - auth_mode: None, - signer_mode: None, - default_signer_mode: None, - supported_signer_modes: Vec::new(), - bridge_enabled: None, - bridge_ready: None, - relay_count: None, - available_nip46_signer_sessions: None, - job_status_retention: None, - retained_jobs: None, - accepted_jobs: None, - published_jobs: None, - failed_jobs: None, - recovered_failed_jobs: None, - session_surface_enabled: false, - methods_count: 0, - actions: Vec::new(), - })) - } - } -} - -pub fn sessions(config: &RuntimeConfig) -> CommandOutput { - match nip46_sessions(config) { - Ok(sessions) => { - let entries = sessions - .into_iter() - .map(map_session_view) - .collect::<Vec<_>>(); - let state = if entries.is_empty() { "empty" } else { "ready" }; - CommandOutput::success(CommandView::RpcSessions(RpcSessionsView { - state: state.to_owned(), - source: RPC_SOURCE.to_owned(), - url: config.rpc.url.clone(), - count: entries.len(), - reason: None, - sessions: entries, - actions: Vec::new(), - })) - } - Err(DaemonRpcError::MethodUnavailable(reason)) => { - CommandOutput::unconfigured(CommandView::RpcSessions(RpcSessionsView { - state: "unconfigured".to_owned(), - source: RPC_SOURCE.to_owned(), - url: config.rpc.url.clone(), - count: 0, - reason: Some(reason), - sessions: Vec::new(), - actions: vec!["enable nip46.public_jsonrpc_enabled in radrootsd".to_owned()], - })) - } - Err(DaemonRpcError::External(reason)) => { - CommandOutput::external_unavailable(CommandView::RpcSessions(RpcSessionsView { - state: "unavailable".to_owned(), - source: RPC_SOURCE.to_owned(), - url: config.rpc.url.clone(), - count: 0, - reason: Some(reason), - sessions: Vec::new(), - actions: vec!["start radrootsd and verify the rpc url".to_owned()], - })) - } - Err(DaemonRpcError::Unconfigured(reason)) - | Err(DaemonRpcError::Unauthorized(reason)) - | Err(DaemonRpcError::InvalidResponse(reason)) - | Err(DaemonRpcError::Remote(reason)) - | Err(DaemonRpcError::UnknownJob(reason)) => { - CommandOutput::internal_error(CommandView::RpcSessions(RpcSessionsView { - state: "error".to_owned(), - source: RPC_SOURCE.to_owned(), - url: config.rpc.url.clone(), - count: 0, - reason: Some(reason), - sessions: Vec::new(), - actions: Vec::new(), - })) - } - } -} - -pub fn signer_sessions(config: &RuntimeConfig) -> CommandOutput { - match signer_session_views(config) { - Ok((url, sessions)) => { - let entries = sessions - .into_iter() - .map(map_sdk_session_view) - .collect::<Vec<_>>(); - let state = if entries.is_empty() { "empty" } else { "ready" }; - CommandOutput::success(CommandView::RpcSessions(RpcSessionsView { - state: state.to_owned(), - source: SIGNER_SESSION_SOURCE.to_owned(), - url, - count: entries.len(), - reason: None, - sessions: entries, - actions: Vec::new(), - })) - } - Err(DaemonRpcError::External(reason)) => { - CommandOutput::external_unavailable(CommandView::RpcSessions(RpcSessionsView { - state: "unavailable".to_owned(), - source: SIGNER_SESSION_SOURCE.to_owned(), - url: config.rpc.url.clone(), - count: 0, - reason: Some(reason), - sessions: Vec::new(), - actions: vec![ - "start radrootsd and verify the actor write-plane endpoint".to_owned(), - ], - })) - } - Err(DaemonRpcError::Unconfigured(reason)) - | Err(DaemonRpcError::Unauthorized(reason)) - | Err(DaemonRpcError::MethodUnavailable(reason)) => { - CommandOutput::unconfigured(CommandView::RpcSessions(RpcSessionsView { - state: "unconfigured".to_owned(), - source: SIGNER_SESSION_SOURCE.to_owned(), - url: config.rpc.url.clone(), - count: 0, - reason: Some(reason), - sessions: Vec::new(), - actions: vec!["configure the radrootsd actor write-plane binding".to_owned()], - })) - } - Err(DaemonRpcError::InvalidResponse(reason)) - | Err(DaemonRpcError::Remote(reason)) - | Err(DaemonRpcError::UnknownJob(reason)) => { - CommandOutput::internal_error(CommandView::RpcSessions(RpcSessionsView { - state: "error".to_owned(), - source: SIGNER_SESSION_SOURCE.to_owned(), - url: config.rpc.url.clone(), - count: 0, - reason: Some(reason), - sessions: Vec::new(), - actions: Vec::new(), - })) - } - } -} - -pub fn signer_session_connect_bunker( - config: &RuntimeConfig, - url: &str, -) -> Result<SignerSessionActionView, DaemonRpcError> { - let mut request = SdkRadrootsdSignerSessionConnectRequest::bunker(url.to_owned()); - if let Some(authority) = configured_myc_signer_authority(config) { - request = request.with_signer_authority(sdk_signer_authority(&authority)); - } - signer_session_connect(config, "connect_bunker", &request) -} - -pub fn signer_session_connect_nostrconnect( - config: &RuntimeConfig, - url: &str, - client_secret_key: &str, -) -> Result<SignerSessionActionView, DaemonRpcError> { - let mut request = SdkRadrootsdSignerSessionConnectRequest::nostrconnect( - url.to_owned(), - client_secret_key.to_owned(), - ); - if let Some(authority) = configured_myc_signer_authority(config) { - request = request.with_signer_authority(sdk_signer_authority(&authority)); - } - signer_session_connect(config, "connect_nostrconnect", &request) -} - -pub fn signer_session_show( - config: &RuntimeConfig, - session_id: &str, -) -> Result<SignerSessionActionView, DaemonRpcError> { - let sdk = actor_write_sdk_client(config)?; - let session_ref = SdkRadrootsdSignerSessionRef::from_session_id(session_id.to_owned()); - let session = block_on_sdk(sdk.radrootsd().signer_sessions().status(&session_ref))? - .map_err(|error| DaemonRpcError::Remote(error.to_string()))?; - Ok(signer_session_view_action("show", session)) -} - -pub fn signer_session_public_key( - config: &RuntimeConfig, - session_id: &str, -) -> Result<SignerSessionActionView, DaemonRpcError> { - let sdk = actor_write_sdk_client(config)?; - let session_ref = SdkRadrootsdSignerSessionRef::from_session_id(session_id.to_owned()); - let public_key = block_on_sdk( - sdk.radrootsd() - .signer_sessions() - .get_public_key(&session_ref), - )? - .map_err(|error| DaemonRpcError::Remote(error.to_string()))?; - let mut view = signer_session_action("public_key", "ready"); - view.session_id = Some(session_id.to_owned()); - view.pubkey = Some(public_key.pubkey); - Ok(view) -} - -pub fn signer_session_authorize( - config: &RuntimeConfig, - session_id: &str, -) -> Result<SignerSessionActionView, DaemonRpcError> { - let sdk = actor_write_sdk_client(config)?; - let session_ref = SdkRadrootsdSignerSessionRef::from_session_id(session_id.to_owned()); - let result = block_on_sdk(sdk.radrootsd().signer_sessions().authorize(&session_ref))? - .map_err(|error| DaemonRpcError::Remote(error.to_string()))?; - let mut view = signer_session_action("authorize", "ready"); - view.session_id = Some(session_id.to_owned()); - view.authorized = Some(result.authorized); - view.replayed = Some(result.replayed); - Ok(view) -} - -pub fn signer_session_require_auth( - config: &RuntimeConfig, - session_id: &str, - auth_url: &str, -) -> Result<SignerSessionActionView, DaemonRpcError> { - let sdk = actor_write_sdk_client(config)?; - let session_ref = SdkRadrootsdSignerSessionRef::from_session_id(session_id.to_owned()); - let result = block_on_sdk( - sdk.radrootsd() - .signer_sessions() - .require_auth(&session_ref, auth_url), - )? - .map_err(|error| DaemonRpcError::Remote(error.to_string()))?; - let mut view = signer_session_action("require_auth", "ready"); - view.session_id = Some(session_id.to_owned()); - view.required = Some(result.required); - view.auth_url = Some(auth_url.to_owned()); - Ok(view) -} - -pub fn signer_session_close( - config: &RuntimeConfig, - session_id: &str, -) -> Result<SignerSessionActionView, DaemonRpcError> { - let sdk = actor_write_sdk_client(config)?; - let session_ref = SdkRadrootsdSignerSessionRef::from_session_id(session_id.to_owned()); - let result = block_on_sdk(sdk.radrootsd().signer_sessions().close(&session_ref))? - .map_err(|error| DaemonRpcError::Remote(error.to_string()))?; - let mut view = signer_session_action("close", "ready"); - view.session_id = Some(session_id.to_owned()); - view.closed = Some(result.closed); - Ok(view) -} - pub fn bridge_job_list(config: &RuntimeConfig) -> Result<Vec<JobSummaryView>, DaemonRpcError> { bridge_jobs(config).map(|jobs| jobs.into_iter().map(map_job_summary_view).collect()) } @@ -698,15 +293,6 @@ pub fn bridge_order_request( map_order_request_receipt(receipt, idempotency_key) } -fn bridge_status(config: &RuntimeConfig) -> Result<BridgeStatusRemote, DaemonRpcError> { - call( - &default_target(config), - "bridge.status", - None, - RpcAuthMode::BridgeBearer, - ) -} - fn bridge_jobs(config: &RuntimeConfig) -> Result<Vec<BridgeJobRemote>, DaemonRpcError> { call( &default_target(config), @@ -728,10 +314,6 @@ fn bridge_job_status( ) } -fn nip46_sessions(config: &RuntimeConfig) -> Result<Vec<Nip46SessionRemote>, DaemonRpcError> { - nip46_sessions_with_target(&default_target(config)) -} - fn nip46_sessions_with_target( target: &RpcTarget, ) -> Result<Vec<Nip46SessionRemote>, DaemonRpcError> { @@ -826,83 +408,6 @@ fn sdk_signer_authority(value: &ActorWriteSignerAuthority) -> SdkRadrootsdSigner } } -fn signer_session_views( - config: &RuntimeConfig, -) -> Result<(String, Vec<SdkRadrootsdSignerSessionView>), DaemonRpcError> { - let target = actor_write_target(config)?; - let sdk = radrootsd_sdk_client(&target)?; - let sessions = block_on_sdk(sdk.radrootsd().signer_sessions().list())? - .map_err(|error| DaemonRpcError::Remote(error.to_string()))?; - Ok((target.url, sessions)) -} - -fn signer_session_connect( - config: &RuntimeConfig, - action: &str, - request: &SdkRadrootsdSignerSessionConnectRequest, -) -> Result<SignerSessionActionView, DaemonRpcError> { - let sdk = actor_write_sdk_client(config)?; - let handle = block_on_sdk(sdk.radrootsd().signer_sessions().connect(request))? - .map_err(|error| DaemonRpcError::Remote(error.to_string()))?; - Ok(signer_session_handle_action(action, handle)) -} - -fn signer_session_action(action: &str, state: &str) -> SignerSessionActionView { - SignerSessionActionView { - action: action.to_owned(), - state: state.to_owned(), - source: SIGNER_SESSION_SOURCE.to_owned(), - session_id: None, - mode: None, - remote_signer_pubkey: None, - client_pubkey: None, - signer_pubkey: None, - user_pubkey: None, - relays: Vec::new(), - permissions: Vec::new(), - auth_required: None, - authorized: None, - auth_url: None, - expires_in_secs: None, - pubkey: None, - replayed: None, - required: None, - closed: None, - reason: None, - } -} - -fn signer_session_handle_action( - action: &str, - handle: SdkRadrootsdSignerSessionHandle, -) -> SignerSessionActionView { - let mut view = signer_session_action(action, "ready"); - view.session_id = Some(handle.session().session_id().to_owned()); - view.mode = Some(format!("{:?}", handle.mode())); - view.remote_signer_pubkey = Some(handle.remote_signer_pubkey().to_owned()); - view.client_pubkey = Some(handle.client_pubkey().to_owned()); - view.relays = handle.relays().to_vec(); - view -} - -fn signer_session_view_action( - action: &str, - session: SdkRadrootsdSignerSessionView, -) -> SignerSessionActionView { - let mut view = signer_session_action(action, "ready"); - view.session_id = Some(session.session().session_id().to_owned()); - view.signer_pubkey = Some(session.signer_pubkey); - view.user_pubkey = session.user_pubkey; - view.client_pubkey = Some(session.client_pubkey); - view.relays = session.relays; - view.permissions = session.permissions; - view.auth_required = Some(session.auth_required); - view.authorized = Some(session.authorized); - view.auth_url = session.auth_url; - view.expires_in_secs = session.expires_in_secs; - view -} - fn map_sdk_publish_error(error: SdkPublishError) -> DaemonRpcError { match error { SdkPublishError::Config(err) => DaemonRpcError::Unconfigured(err.to_string()), @@ -1322,43 +827,6 @@ fn map_job_detail_view(job: BridgeJobRemote) -> JobDetailView { } } -fn map_session_view(session: Nip46SessionRemote) -> RpcSessionView { - RpcSessionView { - session_id: session.session_id, - role: session.role, - client_pubkey: session.client_pubkey, - signer_pubkey: session.signer_pubkey, - user_pubkey: session.user_pubkey, - relay_count: session.relays.len(), - permissions_count: session.permissions.len(), - auth_required: session.auth_required, - authorized: session.authorized, - expires_in_secs: session.expires_in_secs, - } -} - -fn map_sdk_session_view(session: SdkRadrootsdSignerSessionView) -> RpcSessionView { - RpcSessionView { - session_id: session.session().session_id().to_owned(), - role: sdk_session_role(session.role).to_owned(), - client_pubkey: session.client_pubkey, - signer_pubkey: session.signer_pubkey, - user_pubkey: session.user_pubkey, - relay_count: session.relays.len(), - permissions_count: session.permissions.len(), - auth_required: session.auth_required, - authorized: session.authorized, - expires_in_secs: session.expires_in_secs, - } -} - -fn sdk_session_role(role: SdkRadrootsdSignerSessionRole) -> &'static str { - match role { - SdkRadrootsdSignerSessionRole::InboundLocalSigner => "inbound_local_signer", - SdkRadrootsdSignerSessionRole::OutboundRemoteSigner => "outbound_remote_signer", - } -} - pub fn bridge_source() -> &'static str { BRIDGE_SOURCE } diff --git a/src/runtime/farm.rs b/src/runtime/farm.rs @@ -9,10 +9,6 @@ use radroots_events_codec::d_tag::is_d_tag_base64url; use radroots_events_codec::farm::encode::to_wire_parts_with_kind; use radroots_events_codec::profile::encode::to_wire_parts_with_profile_type; -use crate::cli::{ - FarmFieldArg, FarmInitArgs, FarmPublishArgs, FarmScopeArg, FarmScopedArgs, FarmSetArgs, - FarmSetupArgs, -}; use crate::domain::runtime::{ FarmConfigDocumentView, FarmConfigSummaryView, FarmGetView, FarmListingDefaultsView, FarmPublicationView, FarmPublishComponentView, FarmPublishEventView, FarmPublishJobView, @@ -27,12 +23,15 @@ use crate::runtime::farm_config::{ FarmMissingField, FarmPublicationStatus, ResolvedFarmConfig, SUPPORTED_FARM_CONFIG_VERSION, }; use crate::runtime::signer::{ActorWriteBindingError, resolve_actor_write_authority}; +use crate::runtime_args::{ + FarmCreateArgs, FarmFieldArg, FarmPublishArgs, FarmScopeArg, FarmScopedArgs, FarmUpdateArgs, +}; const FARM_CONFIG_SOURCE: &str = "farm config · local first"; static D_TAG_COUNTER: AtomicU64 = AtomicU64::new(0); -pub fn init(config: &RuntimeConfig, args: &FarmInitArgs) -> Result<FarmSetupView, RuntimeError> { +pub fn init(config: &RuntimeConfig, args: &FarmCreateArgs) -> Result<FarmSetupView, RuntimeError> { let scope = scope_from_arg(args.scope); let resolved_scope = farm_config::resolve_scope(&config.paths, scope)?; let Some(selected_account) = selected_account_for_draft(config)? else { @@ -51,26 +50,7 @@ pub fn init(config: &RuntimeConfig, args: &FarmInitArgs) -> Result<FarmSetupView ) } -pub fn setup(config: &RuntimeConfig, args: &FarmSetupArgs) -> Result<FarmSetupView, RuntimeError> { - let scope = scope_from_arg(args.scope); - let resolved_scope = farm_config::resolve_scope(&config.paths, scope)?; - let Some(selected_account) = selected_account_for_draft(config)? else { - return Ok(missing_selected_account_setup_view()); - }; - let existing = farm_config::load(config, Some(resolved_scope))?; - let document = setup_document(args, resolved_scope, &selected_account, existing.as_ref())?; - save_draft_view( - "configured", - resolved_scope, - &selected_account, - &document, - None, - farm_setup_actions(&document), - config, - ) -} - -pub fn set(config: &RuntimeConfig, args: &FarmSetArgs) -> Result<FarmSetView, RuntimeError> { +pub fn set(config: &RuntimeConfig, args: &FarmUpdateArgs) -> Result<FarmSetView, RuntimeError> { let scope = scope_from_arg(args.scope); let resolved_scope = farm_config::resolve_scope(&config.paths, scope)?; let path = farm_config::config_path(&config.paths, resolved_scope)?; @@ -989,7 +969,7 @@ fn init_document( scope: FarmConfigScope, account: &AccountRecordView, existing: Option<&ResolvedFarmConfig>, - args: &FarmInitArgs, + args: &FarmCreateArgs, ) -> Result<FarmConfigDocument, RuntimeError> { let existing_document = existing.map(|resolved| &resolved.document); let farm_d_tag = match args.farm_d_tag.as_deref() { @@ -1275,123 +1255,6 @@ fn publication_for_document( .unwrap_or_default() } -fn setup_document( - args: &FarmSetupArgs, - scope: FarmConfigScope, - account: &AccountRecordView, - existing: Option<&ResolvedFarmConfig>, -) -> Result<FarmConfigDocument, RuntimeError> { - let existing_document = existing.map(|resolved| &resolved.document); - let name = required_text(args.name.as_str(), "farm.name")?; - let location_primary = required_text(args.location.as_str(), "farm.location.primary")?; - let delivery_method = required_text( - args.delivery_method.as_str(), - "listing_defaults.delivery_method", - )?; - let farm_d_tag = match args.farm_d_tag.as_deref() { - Some(value) => required_d_tag(value, "farm_d_tag")?, - None => existing_document - .map(|document| document.farm.d_tag.clone()) - .unwrap_or_else(generate_d_tag), - }; - if !is_d_tag_base64url(farm_d_tag.as_str()) { - return Err(RuntimeError::Config( - "farm_d_tag must be a 22-character base64url identifier".to_owned(), - )); - } - - let about = optional_arg_or_existing( - args.about.as_ref(), - existing_document.and_then(|document| document.profile.about.as_ref()), - ); - let website = optional_arg_or_existing( - args.website.as_ref(), - existing_document.and_then(|document| document.profile.website.as_ref()), - ); - let picture = optional_arg_or_existing( - args.picture.as_ref(), - existing_document.and_then(|document| document.profile.picture.as_ref()), - ); - let banner = optional_arg_or_existing( - args.banner.as_ref(), - existing_document.and_then(|document| document.profile.banner.as_ref()), - ); - let display_name = optional_arg_or_existing( - args.display_name.as_ref(), - existing_document.and_then(|document| document.profile.display_name.as_ref()), - ) - .or_else(|| Some(name.clone())); - let city = optional_arg_or_existing( - args.city.as_ref(), - existing_document - .and_then(|document| document.farm.location.as_ref()) - .and_then(|location| location.city.as_ref()), - ); - let region = optional_arg_or_existing( - args.region.as_ref(), - existing_document - .and_then(|document| document.farm.location.as_ref()) - .and_then(|location| location.region.as_ref()), - ); - let country = optional_arg_or_existing( - args.country.as_ref(), - existing_document - .and_then(|document| document.farm.location.as_ref()) - .and_then(|location| location.country.as_ref()), - ); - let publication = publication_for_document(existing_document, account, farm_d_tag.as_str()); - - Ok(FarmConfigDocument { - version: SUPPORTED_FARM_CONFIG_VERSION, - selection: FarmConfigSelection { - scope, - account: account.record.account_id.to_string(), - farm_d_tag: farm_d_tag.clone(), - }, - profile: RadrootsProfile { - name: name.clone(), - display_name, - nip05: None, - about: about.clone(), - website: website.clone(), - picture: picture.clone(), - banner: banner.clone(), - lud06: None, - lud16: None, - bot: None, - }, - farm: RadrootsFarm { - d_tag: farm_d_tag, - name, - about, - website, - picture, - banner, - location: Some(RadrootsFarmLocation { - primary: Some(location_primary.clone()), - city: city.clone(), - region: region.clone(), - country: country.clone(), - gcs: None, - }), - tags: None, - }, - listing_defaults: FarmListingDefaults { - delivery_method, - location: RadrootsListingLocation { - primary: location_primary, - city, - region, - country, - lat: None, - lng: None, - geohash: None, - }, - }, - publication, - }) -} - fn configured_account( config: &RuntimeConfig, account_id: &str, diff --git a/src/runtime/find.rs b/src/runtime/find.rs @@ -1,7 +1,6 @@ use radroots_replica_db::ReplicaSql; use radroots_sql_core::SqliteExecutor; -use crate::cli::FindArgs; use crate::domain::runtime::{ FindHyfView, FindPriceView, FindQuantityView, FindResultHyfView, FindResultProvenanceView, FindResultView, FindView, SyncFreshnessView, @@ -10,6 +9,7 @@ use crate::runtime::RuntimeError; use crate::runtime::config::RuntimeConfig; use crate::runtime::hyf::{self, HyfQueryRewriteRequest, HyfRequestContext}; use crate::runtime::sync::freshness_from_executor; +use crate::runtime_args::FindQueryArgs; const FIND_SOURCE: &str = "local replica · local first"; const FIND_HYF_SOURCE: &str = "hyf query_rewrite · local first"; @@ -40,7 +40,7 @@ impl AppliedQueryRewrite { } } -pub fn search(config: &RuntimeConfig, args: &FindArgs) -> Result<FindView, RuntimeError> { +pub fn search(config: &RuntimeConfig, args: &FindQueryArgs) -> Result<FindView, RuntimeError> { let query = args.query.join(" "); if !config.local.replica_db_path.exists() { return Ok(FindView { diff --git a/src/runtime/job.rs b/src/runtime/job.rs @@ -1,278 +0,0 @@ -use std::thread; -use std::time::Duration; - -use chrono::{DateTime, Utc}; - -use crate::cli::JobWatchArgs; -use crate::domain::runtime::{ - CommandOutput, CommandView, JobGetView, JobListView, JobWatchFrameView, JobWatchView, -}; -use crate::runtime::RuntimeError; -use crate::runtime::config::RuntimeConfig; -use crate::runtime::daemon::{self, DaemonRpcError}; - -pub fn list(config: &RuntimeConfig) -> CommandOutput { - match daemon::bridge_job_list(config) { - Ok(jobs) => CommandOutput::success(CommandView::JobList(JobListView { - state: if jobs.is_empty() { - "empty".to_owned() - } else { - "ready".to_owned() - }, - source: daemon::bridge_source().to_owned(), - rpc_url: config.rpc.url.clone(), - count: jobs.len(), - reason: None, - jobs, - actions: Vec::new(), - })), - Err(error) => error_job_list_view(config, error), - } -} - -pub fn get(config: &RuntimeConfig, job_id: &str) -> CommandOutput { - match daemon::bridge_job(config, job_id) { - Ok(Some(job)) => CommandOutput::success(CommandView::JobGet(JobGetView { - state: "ready".to_owned(), - source: daemon::bridge_source().to_owned(), - rpc_url: config.rpc.url.clone(), - lookup: job_id.to_owned(), - reason: None, - job: Some(job), - actions: Vec::new(), - })), - Ok(None) => CommandOutput::success(CommandView::JobGet(JobGetView { - state: "missing".to_owned(), - source: daemon::bridge_source().to_owned(), - rpc_url: config.rpc.url.clone(), - lookup: job_id.to_owned(), - reason: Some(format!("job `{job_id}` was not found in radrootsd")), - job: None, - actions: vec!["radroots job ls".to_owned()], - })), - Err(error) => error_job_get_view(config, job_id, error), - } -} - -pub fn watch(config: &RuntimeConfig, args: &JobWatchArgs) -> Result<CommandOutput, RuntimeError> { - if args.frames == Some(0) { - return Err(RuntimeError::Config( - "--frames must be greater than zero when provided".to_owned(), - )); - } - - let mut frames = Vec::new(); - let max_frames = args.frames.unwrap_or(usize::MAX); - loop { - match daemon::bridge_job(config, args.key.as_str()) { - Ok(Some(job)) => { - frames.push(JobWatchFrameView { - sequence: frames.len() + 1, - observed_at_unix: job.completed_at_unix.unwrap_or(job.requested_at_unix), - state: job.state.clone(), - terminal: job.terminal, - signer: job.signer.clone(), - signer_session_id: job.signer_session_id.clone(), - summary: job.relay_outcome_summary.clone(), - }); - if job.terminal || frames.len() >= max_frames { - let state = if job.terminal { - job.state - } else { - "watching".to_owned() - }; - return Ok(CommandOutput::success(CommandView::JobWatch( - JobWatchView { - state, - source: daemon::bridge_source().to_owned(), - rpc_url: config.rpc.url.clone(), - job_id: args.key.clone(), - interval_ms: args.interval_ms, - reason: None, - frames, - actions: Vec::new(), - }, - ))); - } - } - Ok(None) => { - return Ok(CommandOutput::success(CommandView::JobWatch( - JobWatchView { - state: "missing".to_owned(), - source: daemon::bridge_source().to_owned(), - rpc_url: config.rpc.url.clone(), - job_id: args.key.clone(), - interval_ms: args.interval_ms, - reason: Some(format!("job `{}` was not found in radrootsd", args.key)), - frames, - actions: vec!["radroots job ls".to_owned()], - }, - ))); - } - Err(error) => { - return Ok(error_job_watch_view(config, args, frames, error)); - } - } - - thread::sleep(Duration::from_millis(args.interval_ms)); - } -} - -pub fn format_timestamp(unix: u64) -> String { - DateTime::<Utc>::from_timestamp(unix as i64, 0) - .map(|value| value.format("%Y-%m-%d %H:%M:%S UTC").to_string()) - .unwrap_or_else(|| unix.to_string()) -} - -pub fn format_clock(unix: u64) -> String { - DateTime::<Utc>::from_timestamp(unix as i64, 0) - .map(|value| value.format("%H:%M:%S").to_string()) - .unwrap_or_else(|| unix.to_string()) -} - -fn error_job_list_view(config: &RuntimeConfig, error: DaemonRpcError) -> CommandOutput { - match error { - DaemonRpcError::Unconfigured(reason) - | DaemonRpcError::Unauthorized(reason) - | DaemonRpcError::MethodUnavailable(reason) => { - CommandOutput::unconfigured(CommandView::JobList(JobListView { - state: "unconfigured".to_owned(), - source: daemon::bridge_source().to_owned(), - rpc_url: config.rpc.url.clone(), - count: 0, - reason: Some(reason), - jobs: Vec::new(), - actions: vec![ - "set RADROOTS_RPC_BEARER_TOKEN in .env or your shell".to_owned(), - "start radrootsd with bridge ingress enabled".to_owned(), - ], - })) - } - DaemonRpcError::External(reason) => { - CommandOutput::external_unavailable(CommandView::JobList(JobListView { - state: "unavailable".to_owned(), - source: daemon::bridge_source().to_owned(), - rpc_url: config.rpc.url.clone(), - count: 0, - reason: Some(reason), - jobs: Vec::new(), - actions: vec!["start radrootsd and verify the rpc url".to_owned()], - })) - } - DaemonRpcError::InvalidResponse(reason) - | DaemonRpcError::Remote(reason) - | DaemonRpcError::UnknownJob(reason) => { - CommandOutput::internal_error(CommandView::JobList(JobListView { - state: "error".to_owned(), - source: daemon::bridge_source().to_owned(), - rpc_url: config.rpc.url.clone(), - count: 0, - reason: Some(reason), - jobs: Vec::new(), - actions: Vec::new(), - })) - } - } -} - -fn error_job_get_view( - config: &RuntimeConfig, - job_id: &str, - error: DaemonRpcError, -) -> CommandOutput { - match error { - DaemonRpcError::Unconfigured(reason) - | DaemonRpcError::Unauthorized(reason) - | DaemonRpcError::MethodUnavailable(reason) => { - CommandOutput::unconfigured(CommandView::JobGet(JobGetView { - state: "unconfigured".to_owned(), - source: daemon::bridge_source().to_owned(), - rpc_url: config.rpc.url.clone(), - lookup: job_id.to_owned(), - reason: Some(reason), - job: None, - actions: vec![ - "set RADROOTS_RPC_BEARER_TOKEN in .env or your shell".to_owned(), - "start radrootsd with bridge ingress enabled".to_owned(), - ], - })) - } - DaemonRpcError::External(reason) => { - CommandOutput::external_unavailable(CommandView::JobGet(JobGetView { - state: "unavailable".to_owned(), - source: daemon::bridge_source().to_owned(), - rpc_url: config.rpc.url.clone(), - lookup: job_id.to_owned(), - reason: Some(reason), - job: None, - actions: vec!["start radrootsd and verify the rpc url".to_owned()], - })) - } - DaemonRpcError::InvalidResponse(reason) - | DaemonRpcError::Remote(reason) - | DaemonRpcError::UnknownJob(reason) => { - CommandOutput::internal_error(CommandView::JobGet(JobGetView { - state: "error".to_owned(), - source: daemon::bridge_source().to_owned(), - rpc_url: config.rpc.url.clone(), - lookup: job_id.to_owned(), - reason: Some(reason), - job: None, - actions: Vec::new(), - })) - } - } -} - -fn error_job_watch_view( - config: &RuntimeConfig, - args: &JobWatchArgs, - frames: Vec<JobWatchFrameView>, - error: DaemonRpcError, -) -> CommandOutput { - match error { - DaemonRpcError::Unconfigured(reason) - | DaemonRpcError::Unauthorized(reason) - | DaemonRpcError::MethodUnavailable(reason) => { - CommandOutput::unconfigured(CommandView::JobWatch(JobWatchView { - state: "unconfigured".to_owned(), - source: daemon::bridge_source().to_owned(), - rpc_url: config.rpc.url.clone(), - job_id: args.key.clone(), - interval_ms: args.interval_ms, - reason: Some(reason), - frames, - actions: vec![ - "set RADROOTS_RPC_BEARER_TOKEN in .env or your shell".to_owned(), - "start radrootsd with bridge ingress enabled".to_owned(), - ], - })) - } - DaemonRpcError::External(reason) => { - CommandOutput::external_unavailable(CommandView::JobWatch(JobWatchView { - state: "unavailable".to_owned(), - source: daemon::bridge_source().to_owned(), - rpc_url: config.rpc.url.clone(), - job_id: args.key.clone(), - interval_ms: args.interval_ms, - reason: Some(reason), - frames, - actions: vec!["start radrootsd and verify the rpc url".to_owned()], - })) - } - DaemonRpcError::InvalidResponse(reason) - | DaemonRpcError::Remote(reason) - | DaemonRpcError::UnknownJob(reason) => { - CommandOutput::internal_error(CommandView::JobWatch(JobWatchView { - state: "error".to_owned(), - source: daemon::bridge_source().to_owned(), - rpc_url: config.rpc.url.clone(), - job_id: args.key.clone(), - interval_ms: args.interval_ms, - reason: Some(reason), - frames, - actions: Vec::new(), - })) - } - } -} diff --git a/src/runtime/listing.rs b/src/runtime/listing.rs @@ -25,15 +25,10 @@ use radroots_trade::listing::publish::validate_listing_for_seller; use radroots_trade::listing::validation::validate_listing_event; use serde::{Deserialize, Serialize}; -use crate::cli::{ - ListingFileArgs, ListingMutationArgs, ListingNewArgs, RecordKeyArgs, SellAddArgs, - SellRepriceArgs, SellRestockArgs, SellShowArgs, -}; use crate::domain::runtime::{ FindPriceView, FindQuantityView, FindResultProvenanceView, ListingGetView, ListingMutationEventView, ListingMutationJobView, ListingMutationView, ListingNewView, - ListingValidateView, ListingValidationIssueView, SellAddView, SellCheckView, - SellDraftMutationView, SellMutationView, SellShowView, SyncFreshnessView, + ListingValidateView, ListingValidationIssueView, SyncFreshnessView, }; use crate::runtime::RuntimeError; use crate::runtime::accounts; @@ -43,6 +38,9 @@ use crate::runtime::daemon::DaemonRpcError; use crate::runtime::farm_config; use crate::runtime::signer::{ActorWriteBindingError, resolve_actor_write_authority}; use crate::runtime::sync::freshness_from_executor; +use crate::runtime_args::{ + ListingCreateArgs, ListingFileArgs, ListingMutationArgs, RecordLookupArgs, +}; const DRAFT_KIND: &str = "listing_draft_v1"; const LISTING_SOURCE: &str = "local draft · local first"; @@ -157,33 +155,6 @@ struct ListingAuthoringDefaults { } #[derive(Debug, Clone)] -struct DraftSummary { - product_key: Option<String>, - title: Option<String>, - category: Option<String>, - offer: Option<String>, - price: Option<String>, - stock: Option<String>, - delivery_method: Option<String>, - location_primary: Option<String>, -} - -#[derive(Debug, Clone)] -struct ParsedQuantityExpr { - amount: String, - unit: String, - label: String, -} - -#[derive(Debug, Clone)] -struct ParsedPriceExpr { - amount: String, - currency: String, - per_amount: String, - per_unit: String, -} - -#[derive(Debug, Clone)] struct CanonicalListingDraft { listing_id: String, seller_pubkey: String, @@ -210,7 +181,7 @@ impl ListingMutationOperation { pub fn scaffold( config: &RuntimeConfig, - args: &ListingNewArgs, + args: &ListingCreateArgs, ) -> Result<ListingNewView, RuntimeError> { let (draft, defaults) = build_listing_draft(config, args)?; let output_path = default_listing_output_path(args.output.as_ref(), &draft.listing.d_tag)?; @@ -242,209 +213,9 @@ pub fn scaffold( }) } -pub fn sell_add(config: &RuntimeConfig, args: &SellAddArgs) -> Result<SellAddView, RuntimeError> { - let listing_args = listing_args_from_sell_add(args)?; - let (draft, defaults) = build_listing_draft(config, &listing_args)?; - let output_path = listing_args - .output - .clone() - .expect("sell add always sets an explicit output path"); - write_listing_draft(&output_path, &draft, false)?; - - let summary = summarize_draft(&draft); - let mut actions = vec![format!( - "radroots listing validate {}", - output_path.display() - )]; - if defaults.selected_account_pubkey.is_some() && defaults.selected_farm_d_tag.is_some() { - actions.push(format!( - "radroots listing publish {}", - output_path.display() - )); - } - if defaults.selected_account_pubkey.is_none() { - actions.push("radroots account create".to_owned()); - } - if let Some(action) = &defaults.farm_next_action { - actions.push(action.clone()); - } - - Ok(SellAddView { - state: "draft_saved".to_owned(), - source: LISTING_SOURCE.to_owned(), - file: output_path.display().to_string(), - product_key: summary.product_key, - title: summary.title, - offer: summary.offer, - price: summary.price, - stock: summary.stock, - farm_name: defaults.farm_name, - delivery_method: summary.delivery_method, - location_primary: summary.location_primary, - reason: defaults.farm_reason, - actions, - }) -} - -pub fn sell_show( - _config: &RuntimeConfig, - args: &SellShowArgs, -) -> Result<SellShowView, RuntimeError> { - let draft = read_listing_draft(&args.file)?; - let summary = summarize_draft(&draft); - Ok(SellShowView { - state: "ready".to_owned(), - source: LISTING_SOURCE.to_owned(), - file: args.file.display().to_string(), - product_key: summary.product_key, - title: summary.title, - category: summary.category, - offer: summary.offer, - price: summary.price, - stock: summary.stock, - delivery_method: summary.delivery_method, - location_primary: summary.location_primary, - reason: None, - actions: vec![ - format!("radroots listing validate {}", args.file.display()), - format!("radroots listing publish {}", args.file.display()), - ], - }) -} - -pub fn sell_reprice( - _config: &RuntimeConfig, - args: &SellRepriceArgs, -) -> Result<SellDraftMutationView, RuntimeError> { - let mut draft = read_listing_draft(&args.file)?; - let parsed = parse_price_expr(args.price_expr.as_str())?; - draft.primary_bin.price_amount = parsed.amount; - draft.primary_bin.price_currency = parsed.currency; - draft.primary_bin.price_per_amount = parsed.per_amount; - draft.primary_bin.price_per_unit = parsed.per_unit; - write_listing_draft(&args.file, &draft, true)?; - - let summary = summarize_draft(&draft); - Ok(SellDraftMutationView { - state: "updated".to_owned(), - operation: "reprice".to_owned(), - source: LISTING_SOURCE.to_owned(), - file: args.file.display().to_string(), - product_key: summary.product_key, - changed_label: "Price".to_owned(), - changed_value: summary - .price - .unwrap_or_else(|| args.price_expr.trim().to_owned()), - actions: vec![ - format!("radroots listing validate {}", args.file.display()), - format!("radroots listing update {}", args.file.display()), - ], - }) -} - -pub fn sell_restock( - _config: &RuntimeConfig, - args: &SellRestockArgs, -) -> Result<SellDraftMutationView, RuntimeError> { - let mut draft = read_listing_draft(&args.file)?; - parse_decimal_string(args.available.as_str(), "`sell restock <available>`")?; - draft.inventory.available = args.available.trim().to_owned(); - write_listing_draft(&args.file, &draft, true)?; - - let summary = summarize_draft(&draft); - Ok(SellDraftMutationView { - state: "updated".to_owned(), - operation: "restock".to_owned(), - source: LISTING_SOURCE.to_owned(), - file: args.file.display().to_string(), - product_key: summary.product_key, - changed_label: "Stock".to_owned(), - changed_value: summary - .stock - .unwrap_or_else(|| format!("{} available", args.available.trim())), - actions: vec![ - format!("radroots listing validate {}", args.file.display()), - format!("radroots listing update {}", args.file.display()), - ], - }) -} - -pub fn sell_check( - config: &RuntimeConfig, - args: &ListingFileArgs, -) -> Result<SellCheckView, RuntimeError> { - let view = validate(config, args)?; - let summary = read_listing_draft(&args.file) - .ok() - .map(|draft| summarize_draft(&draft)); - let actions = if view.valid { - vec![format!("radroots listing publish {}", args.file.display())] - } else { - vec![ - format!("edit {}", args.file.display()), - format!("radroots listing validate {}", args.file.display()), - "Edit the draft file and run the command again".to_owned(), - ] - }; - - Ok(SellCheckView { - state: if view.valid { - "ready".to_owned() - } else { - "invalid".to_owned() - }, - source: view.source, - file: view.file, - valid: view.valid, - product_key: summary - .as_ref() - .and_then(|summary| summary.product_key.clone()), - seller_pubkey: view.seller_pubkey, - farm_ref: view.farm_d_tag, - issues: view.issues, - actions, - }) -} - -pub fn sell_publish( - config: &RuntimeConfig, - args: &ListingMutationArgs, -) -> Result<SellMutationView, RuntimeError> { - let view = publish(config, args)?; - Ok(sell_mutation_from_listing( - view, - args.file.as_path(), - "publish", - )) -} - -pub fn sell_update( - config: &RuntimeConfig, - args: &ListingMutationArgs, -) -> Result<SellMutationView, RuntimeError> { - let view = update(config, args)?; - Ok(sell_mutation_from_listing( - view, - args.file.as_path(), - "update", - )) -} - -pub fn sell_pause( - config: &RuntimeConfig, - args: &ListingMutationArgs, -) -> Result<SellMutationView, RuntimeError> { - let view = archive(config, args)?; - Ok(sell_mutation_from_listing( - view, - args.file.as_path(), - "pause", - )) -} - fn build_listing_draft( config: &RuntimeConfig, - args: &ListingNewArgs, + args: &ListingCreateArgs, ) -> Result<(ListingDraftDocument, ListingAuthoringDefaults), RuntimeError> { let defaults = authoring_defaults(config)?; let quantity_unit = args.quantity_unit.clone().unwrap_or_else(|| "g".to_owned()); @@ -509,52 +280,6 @@ fn build_listing_draft( Ok((draft, defaults)) } -fn listing_args_from_sell_add(args: &SellAddArgs) -> Result<ListingNewArgs, RuntimeError> { - let product_key = slugify_ascii(args.product.as_str()); - if product_key.is_empty() { - return Err(RuntimeError::Config( - "`sell add <product>` requires at least one ASCII letter or digit".to_owned(), - )); - } - - let title = args - .title - .clone() - .unwrap_or_else(|| title_case_ascii(args.product.as_str())); - let category = args.category.clone().unwrap_or_else(|| title.clone()); - let pack = args.pack.as_deref().map(parse_quantity_expr).transpose()?; - let price = args - .price_expr - .as_deref() - .map(parse_price_expr) - .transpose()?; - let output = Some(match &args.file { - Some(path) => path.clone(), - None => std::path::PathBuf::from(format!("listing-{product_key}.toml")), - }); - - Ok(ListingNewArgs { - output, - key: Some(product_key), - title: Some(title.clone()), - category: Some(category), - summary: Some( - args.summary - .clone() - .unwrap_or_else(|| format!("Listing for {title}")), - ), - bin_id: None, - quantity_amount: pack.as_ref().map(|pack| pack.amount.clone()), - quantity_unit: pack.as_ref().map(|pack| pack.unit.clone()), - price_amount: price.as_ref().map(|price| price.amount.clone()), - price_currency: price.as_ref().map(|price| price.currency.clone()), - price_per_amount: price.as_ref().map(|price| price.per_amount.clone()), - price_per_unit: price.as_ref().map(|price| price.per_unit.clone()), - available: args.stock.clone(), - label: pack.as_ref().map(|pack| pack.label.clone()), - }) -} - fn default_listing_output_path( explicit: Option<&std::path::PathBuf>, listing_id: &str, @@ -583,268 +308,6 @@ fn write_listing_draft( Ok(()) } -fn read_listing_draft(path: &Path) -> Result<ListingDraftDocument, RuntimeError> { - let contents = fs::read_to_string(path)?; - toml::from_str::<ListingDraftDocument>(&contents).map_err(|error| { - RuntimeError::Config(format!( - "failed to parse listing draft {}: {error}", - path.display() - )) - }) -} - -fn successful_sell_mutation_actions(operation: &str, product_key: Option<&str>) -> Vec<String> { - match operation { - "publish" => { - let mut actions = Vec::new(); - if let Some(product_key) = product_key { - actions.push(format!("radroots market listing get {product_key}")); - actions.push(format!("radroots listing create --key {product_key}")); - } - actions - } - "update" => product_key - .map(|product_key| vec![format!("radroots market listing get {product_key}")]) - .unwrap_or_default(), - "pause" => product_key - .map(|product_key| vec![format!("radroots listing create --key {product_key}")]) - .unwrap_or_default(), - _ => Vec::new(), - } -} - -fn summarize_draft(draft: &ListingDraftDocument) -> DraftSummary { - DraftSummary { - product_key: non_empty(draft.product.key.clone()), - title: non_empty(draft.product.title.clone()), - category: non_empty(draft.product.category.clone()), - offer: draft_offer_text(draft), - price: draft_price_text(draft), - stock: draft_stock_text(draft), - delivery_method: non_empty(draft.delivery.method.clone()), - location_primary: non_empty(draft.location.primary.clone()), - } -} - -fn sell_mutation_from_listing( - view: ListingMutationView, - file: &Path, - operation: &str, -) -> SellMutationView { - let summary = read_listing_draft(file) - .ok() - .map(|draft| summarize_draft(&draft)); - let product_key = summary - .as_ref() - .and_then(|summary| summary.product_key.clone()); - let actions = match view.state.as_str() { - "published" | "deduplicated" => { - successful_sell_mutation_actions(operation, product_key.as_deref()) - } - _ => view.actions, - }; - - SellMutationView { - state: view.state, - operation: operation.to_owned(), - source: view.source, - file: view.file, - product_key, - listing_addr: view.listing_addr, - dry_run: view.dry_run, - deduplicated: view.deduplicated, - publish_mode: Some("runtime_bridge".to_owned()), - job_id: view.job_id, - job_status: view.job_status, - event_id: view.event_id, - reason: view.reason, - actions, - } -} - -fn draft_offer_text(draft: &ListingDraftDocument) -> Option<String> { - non_empty(draft.primary_bin.label.clone()).or_else(|| { - let amount = draft.primary_bin.quantity_amount.trim(); - let unit = draft.primary_bin.quantity_unit.trim(); - if amount.is_empty() || unit.is_empty() { - None - } else { - Some(format!("{} {}", trim_decimal_string(amount), unit)) - } - }) -} - -fn draft_price_text(draft: &ListingDraftDocument) -> Option<String> { - let amount = non_empty(draft.primary_bin.price_amount.clone())?; - let currency = non_empty(draft.primary_bin.price_currency.clone())?; - let per_amount = non_empty(draft.primary_bin.price_per_amount.clone())?; - let per_unit = non_empty(draft.primary_bin.price_per_unit.clone())?; - let denominator = if per_unit == "each" - && numeric_strings_equal( - per_amount.as_str(), - draft.primary_bin.quantity_amount.trim(), - ) - && !draft.primary_bin.label.trim().is_empty() - { - draft.primary_bin.label.trim().to_owned() - } else if per_amount == "1" { - per_unit.to_owned() - } else { - format!("{} {}", trim_decimal_string(&per_amount), per_unit) - }; - Some(format!( - "{} {}/{}", - trim_decimal_string(&amount), - currency.to_ascii_uppercase(), - denominator - )) -} - -fn draft_stock_text(draft: &ListingDraftDocument) -> Option<String> { - non_empty(draft.inventory.available.clone()) - .map(|available| format!("{} available", trim_decimal_string(&available))) -} - -fn parse_quantity_expr(expr: &str) -> Result<ParsedQuantityExpr, RuntimeError> { - let trimmed = expr.trim(); - if trimmed.is_empty() { - return Err(RuntimeError::Config( - "quantity expression must not be empty".to_owned(), - )); - } - if trimmed.eq_ignore_ascii_case("dozen") { - return Ok(ParsedQuantityExpr { - amount: "12".to_owned(), - unit: "each".to_owned(), - label: "dozen".to_owned(), - }); - } - - let parts = trimmed.split_whitespace().collect::<Vec<_>>(); - if parts.is_empty() { - return Err(RuntimeError::Config( - "quantity expression must not be empty".to_owned(), - )); - } - - let (amount, unit) = if parse_decimal_string(parts[0], "quantity amount").is_ok() { - let Some(unit) = parts.get(1) else { - return Err(RuntimeError::Config( - "quantity expression must include a unit, for example `1 kg`".to_owned(), - )); - }; - (parts[0].trim().to_owned(), unit.trim().to_ascii_lowercase()) - } else { - ("1".to_owned(), parts[0].trim().to_ascii_lowercase()) - }; - - unit.parse::<RadrootsCoreUnit>().map_err(|_| { - RuntimeError::Config(format!( - "quantity expression uses unsupported unit `{unit}`" - )) - })?; - - Ok(ParsedQuantityExpr { - amount, - unit, - label: trimmed.to_owned(), - }) -} - -fn parse_price_expr(expr: &str) -> Result<ParsedPriceExpr, RuntimeError> { - let trimmed = expr.trim(); - if trimmed.is_empty() { - return Err(RuntimeError::Config( - "price expression must not be empty".to_owned(), - )); - } - - let segments = trimmed.split_whitespace().collect::<Vec<_>>(); - if segments.len() < 2 { - return Err(RuntimeError::Config( - "price expression must look like `10 USD/kg`".to_owned(), - )); - } - - parse_decimal_string(segments[0], "price amount")?; - let remainder = segments[1..].join(" "); - let Some((currency, per_expr)) = remainder.split_once('/') else { - return Err(RuntimeError::Config( - "price expression must include a `/`, for example `10 USD/kg`".to_owned(), - )); - }; - let per = parse_quantity_expr(per_expr)?; - RadrootsCoreCurrency::from_str_upper(currency.trim().to_ascii_uppercase().as_str()).map_err( - |_| { - RuntimeError::Config(format!( - "price expression uses unsupported currency `{}`", - currency.trim() - )) - }, - )?; - - Ok(ParsedPriceExpr { - amount: segments[0].trim().to_owned(), - currency: currency.trim().to_ascii_uppercase(), - per_amount: per.amount, - per_unit: per.unit, - }) -} - -fn parse_decimal_string(value: &str, label: &str) -> Result<RadrootsCoreDecimal, RuntimeError> { - value - .trim() - .parse::<RadrootsCoreDecimal>() - .map_err(|_| RuntimeError::Config(format!("{label} must be a valid decimal value"))) -} - -fn slugify_ascii(value: &str) -> String { - let mut slug = String::new(); - let mut last_was_dash = false; - for ch in value.chars() { - if ch.is_ascii_alphanumeric() { - slug.push(ch.to_ascii_lowercase()); - last_was_dash = false; - } else if !slug.is_empty() && !last_was_dash { - slug.push('-'); - last_was_dash = true; - } - } - slug.trim_matches('-').to_owned() -} - -fn title_case_ascii(value: &str) -> String { - value - .split(|ch: char| !ch.is_ascii_alphanumeric()) - .filter(|segment| !segment.is_empty()) - .map(capitalize_ascii_word) - .collect::<Vec<_>>() - .join(" ") -} - -fn capitalize_ascii_word(word: &str) -> String { - let mut chars = word.chars(); - let Some(first) = chars.next() else { - return String::new(); - }; - let mut rendered = String::new(); - rendered.push(first.to_ascii_uppercase()); - rendered.push_str(chars.as_str()); - rendered -} - -fn numeric_strings_equal(lhs: &str, rhs: &str) -> bool { - trim_decimal_string(lhs) == trim_decimal_string(rhs) -} - -fn trim_decimal_string(value: &str) -> String { - if let Ok(parsed) = value.trim().parse::<RadrootsCoreDecimal>() { - parsed.to_string() - } else { - value.trim().to_owned() - } -} - pub fn validate( config: &RuntimeConfig, args: &ListingFileArgs, @@ -930,7 +393,10 @@ pub fn validate( } } -pub fn get(config: &RuntimeConfig, args: &RecordKeyArgs) -> Result<ListingGetView, RuntimeError> { +pub fn get( + config: &RuntimeConfig, + args: &RecordLookupArgs, +) -> Result<ListingGetView, RuntimeError> { let freshness = if config.local.replica_db_path.exists() { let executor = SqliteExecutor::open(&config.local.replica_db_path)?; freshness_from_executor(&executor)? diff --git a/src/runtime/local.rs b/src/runtime/local.rs @@ -8,13 +8,13 @@ use radroots_replica_sync::radroots_replica_sync_status; use radroots_sql_core::SqliteExecutor; use serde_json::json; -use crate::cli::LocalExportFormatArg; use crate::domain::runtime::{ LocalBackupView, LocalExportView, LocalInitView, LocalReplicaCountsView, LocalReplicaSyncView, LocalStatusView, }; use crate::runtime::RuntimeError; use crate::runtime::config::RuntimeConfig; +use crate::runtime_args::LocalExportFormatArg; const LOCAL_SOURCE: &str = "local replica · local first"; diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs @@ -1,5 +1,3 @@ -#![allow(dead_code)] - pub mod accounts; pub mod config; pub mod daemon; @@ -8,7 +6,6 @@ pub mod farm; pub mod farm_config; pub mod find; pub mod hyf; -pub mod job; pub mod listing; pub mod local; pub mod logging; @@ -20,7 +17,6 @@ pub mod paths; pub mod provider; pub mod signer; pub mod sync; -pub mod workflow; use std::process::ExitCode; diff --git a/src/runtime/network.rs b/src/runtime/network.rs @@ -1,6 +1,4 @@ -use crate::domain::runtime::{NetStatusView, RelayEntryView, RelayListView}; -use crate::runtime::RuntimeError; -use crate::runtime::accounts; +use crate::domain::runtime::{RelayEntryView, RelayListView}; use crate::runtime::config::RuntimeConfig; pub fn relay_list(config: &RuntimeConfig) -> RelayListView { @@ -35,33 +33,6 @@ pub fn relay_list(config: &RuntimeConfig) -> RelayListView { } } -pub fn net_status(config: &RuntimeConfig) -> Result<NetStatusView, RuntimeError> { - let account_resolution = accounts::resolve_account_resolution(config)?; - let relay_count = config.relay.urls.len(); - let configured = relay_count > 0; - - Ok(NetStatusView { - state: if configured { - "configured".to_owned() - } else { - "unconfigured".to_owned() - }, - source: config.relay.source.as_str().to_owned(), - session: if configured { - "not_started".to_owned() - } else { - "not_configured".to_owned() - }, - relay_count, - publish_policy: config.relay.publish_policy.as_str().to_owned(), - signer_mode: config.signer.backend.as_str().to_owned(), - account_resolution: accounts::account_resolution_view(&account_resolution), - reason: (!configured) - .then_some("no relays are configured for this operator session".to_owned()), - actions: relay_actions(config), - }) -} - fn relay_actions(config: &RuntimeConfig) -> Vec<String> { if config.relay.urls.is_empty() { vec!["radroots --relay wss://relay.example.com relay list".to_owned()] diff --git a/src/runtime/order.rs b/src/runtime/order.rs @@ -23,7 +23,6 @@ use rhi::rhi::{Rhi, start_subscriber}; use serde::{Deserialize, Serialize}; use serde_json::Value as JsonValue; -use crate::cli::{OrderNewArgs, OrderSubmitArgs, OrderWatchArgs, RecordKeyArgs}; use crate::domain::runtime::{ OrderCancelView, OrderDraftItemView, OrderGetView, OrderHistoryEntryView, OrderHistoryView, OrderIssueView, OrderJobView, OrderListView, OrderNewView, OrderSubmitView, OrderSummaryView, @@ -36,6 +35,9 @@ use crate::runtime::config::{ }; use crate::runtime::daemon::{self, DaemonRpcError}; use crate::runtime::signer::{ActorWriteBindingError, resolve_actor_write_authority}; +use crate::runtime_args::{ + OrderDraftCreateArgs, OrderSubmitArgs, OrderWatchArgs, RecordLookupArgs, +}; const ORDER_DRAFT_KIND: &str = "order_draft_v1"; const ORDER_SOURCE: &str = "local order drafts · local first"; @@ -152,7 +154,10 @@ struct ResolvedOrderListing { seller_pubkey: String, } -pub fn scaffold(config: &RuntimeConfig, args: &OrderNewArgs) -> Result<OrderNewView, RuntimeError> { +pub fn scaffold( + config: &RuntimeConfig, + args: &OrderDraftCreateArgs, +) -> Result<OrderNewView, RuntimeError> { validate_scaffold_args(args)?; let listing_lookup = normalize_optional(args.listing.as_deref()); @@ -226,7 +231,7 @@ pub fn scaffold(config: &RuntimeConfig, args: &OrderNewArgs) -> Result<OrderNewV Ok(view) } -pub fn get(config: &RuntimeConfig, args: &RecordKeyArgs) -> Result<OrderGetView, RuntimeError> { +pub fn get(config: &RuntimeConfig, args: &RecordLookupArgs) -> Result<OrderGetView, RuntimeError> { let lookup = args.key.clone(); let file = draft_lookup_path(config, lookup.as_str()); if !file.exists() { @@ -821,7 +826,7 @@ pub fn history(config: &RuntimeConfig) -> Result<OrderHistoryView, RuntimeError> pub fn cancel( config: &RuntimeConfig, - args: &RecordKeyArgs, + args: &RecordLookupArgs, ) -> Result<OrderCancelView, RuntimeError> { let file = draft_lookup_path(config, args.key.as_str()); if !file.exists() { @@ -887,7 +892,7 @@ pub fn cancel( }) } -fn validate_scaffold_args(args: &OrderNewArgs) -> Result<(), RuntimeError> { +fn validate_scaffold_args(args: &OrderDraftCreateArgs) -> Result<(), RuntimeError> { match (normalize_optional(args.bin_id.as_deref()), args.bin_count) { (None, Some(_)) => Err(RuntimeError::Config( "`--qty` requires `--bin` when creating an order draft".to_owned(), diff --git a/src/runtime/signer.rs b/src/runtime/signer.rs @@ -107,27 +107,6 @@ pub fn resolve_actor_write_authority( })) } -pub fn configured_myc_signer_authority( - config: &RuntimeConfig, -) -> Option<ActorWriteSignerAuthority> { - let binding = config.capability_binding(SIGNER_REMOTE_NIP46_CAPABILITY)?; - if binding.provider_runtime_id != SIGNER_BINDING_PROVIDER_RUNTIME_ID { - return None; - } - if !matches!( - binding.target_kind, - CapabilityBindingTargetKind::ManagedInstance - ) || binding.target != "default" - { - return None; - } - Some(ActorWriteSignerAuthority { - provider_runtime_id: SIGNER_BINDING_PROVIDER_RUNTIME_ID.to_owned(), - account_identity_id: binding.managed_account_ref.clone()?, - provider_signer_session_id: binding.signer_session_ref.clone(), - }) -} - fn resolve_local_signer_status(config: &RuntimeConfig) -> SignerStatusView { let (account_resolution, resolved_account_id) = match crate::runtime::accounts::resolve_account_resolution(config) { diff --git a/src/runtime/sync.rs b/src/runtime/sync.rs @@ -5,13 +5,13 @@ use radroots_replica_db::ReplicaSql; use radroots_replica_sync::radroots_replica_sync_status; use radroots_sql_core::SqliteExecutor; -use crate::cli::SyncWatchArgs; use crate::domain::runtime::{ SyncActionView, SyncFreshnessView, SyncQueueView, SyncStatusView, SyncWatchFrameView, SyncWatchView, }; use crate::runtime::RuntimeError; use crate::runtime::config::RuntimeConfig; +use crate::runtime_args::SyncWatchArgs; const SYNC_SOURCE: &str = "local replica · local first"; const RELAY_SETUP_ACTION: &str = "radroots --relay wss://relay.example.com relay list"; diff --git a/src/runtime/workflow.rs b/src/runtime/workflow.rs @@ -1,300 +0,0 @@ -use crate::cli::{FarmScopedArgs, SetupRoleArg}; -use crate::domain::runtime::{SetupView, StatusView}; -use crate::runtime::RuntimeError; -use crate::runtime::accounts::{self}; -use crate::runtime::config::RuntimeConfig; -use crate::runtime::{farm, local}; - -const WORKFLOW_SOURCE: &str = "workflow summary · local first"; -const RELAY_SETUP_ACTION: &str = "radroots relay list --relay wss://relay.example.com"; - -pub fn setup(config: &RuntimeConfig, role: SetupRoleArg) -> Result<SetupView, RuntimeError> { - let account_resolution = accounts::resolve_account_resolution(config)?; - let local_status = ensure_local_status(config)?; - let farm = inspect_farm(config)?; - let relay_configured = relay_configured(config); - let relay_count = config.relay.urls.len(); - - let mut state = "saved"; - let mut ready = Vec::new(); - let mut needs_attention = Vec::new(); - let mut next = Vec::new(); - - if account_resolution.resolved_account.is_some() { - ready.push("Resolved account".to_owned()); - } else { - state = "unconfigured"; - needs_attention.push("Resolved account".to_owned()); - push_unresolved_account_actions(config, &mut next, Some(role))?; - } - - if local_status.state == "ready" { - ready.push("Local market data".to_owned()); - } else { - state = "unconfigured"; - needs_attention.push("Local market data".to_owned()); - } - - if relay_configured { - ready.push("Relay configuration".to_owned()); - } else { - needs_attention.push("Relay configuration".to_owned()); - } - - if account_resolution.resolved_account.is_some() { - match role { - SetupRoleArg::Seller | SetupRoleArg::Both => { - apply_farm_attention(&mut ready, &mut needs_attention, &mut next, &farm); - push_next(&mut next, farm.primary_next_action.as_deref()); - } - SetupRoleArg::Buyer => {} - } - - match role { - SetupRoleArg::Buyer | SetupRoleArg::Both if relay_configured => { - push_next(&mut next, Some("radroots market product search tomatoes")); - } - _ => {} - } - - if !relay_configured { - push_next(&mut next, Some(RELAY_SETUP_ACTION)); - } - - push_next(&mut next, Some("radroots health status get")); - } - - Ok(SetupView { - state: state.to_owned(), - source: WORKFLOW_SOURCE.to_owned(), - role: role_name(role).to_owned(), - account_resolution: accounts::account_resolution_view(&account_resolution), - local_state: local_status.state, - local_root: local_status.local_root, - relay_state: relay_state(config).to_owned(), - relay_count, - farm_state: farm.state.to_owned(), - ready, - needs_attention, - next, - }) -} - -pub fn status(config: &RuntimeConfig) -> Result<StatusView, RuntimeError> { - let account_resolution = accounts::resolve_account_resolution(config)?; - let local_status = local::status(config)?; - let farm = inspect_farm(config)?; - let relay_configured = relay_configured(config); - let relay_count = config.relay.urls.len(); - - let mut ready = Vec::new(); - let mut needs_attention = Vec::new(); - let mut next = Vec::new(); - let mut state = "ready"; - - if account_resolution.resolved_account.is_some() { - ready.push("Resolved account".to_owned()); - } else { - state = "unconfigured"; - needs_attention.push("Resolved account".to_owned()); - push_unresolved_account_actions(config, &mut next, None)?; - } - - if local_status.state == "ready" { - ready.push("Local market data".to_owned()); - } else { - state = "unconfigured"; - needs_attention.push("Local market data".to_owned()); - } - - if relay_configured { - ready.push("Relay configuration".to_owned()); - } else { - state = "unconfigured"; - needs_attention.push("Relay configuration".to_owned()); - } - - if state == "ready" { - apply_farm_attention(&mut ready, &mut needs_attention, &mut next, &farm); - - if relay_configured { - match farm.state { - "draft" | "published" => { - push_next(&mut next, Some("radroots listing create --key tomatoes")) - } - "missing" => push_next(&mut next, Some("radroots market product search tomatoes")), - _ => {} - } - } - } else if account_resolution.resolved_account.is_some() { - push_next(&mut next, Some("radroots basket create")); - push_next(&mut next, Some("radroots farm create")); - if account_resolution.resolved_account.is_some() - && local_status.state == "ready" - && !relay_configured - { - next.clear(); - push_next(&mut next, Some(RELAY_SETUP_ACTION)); - push_next(&mut next, Some("radroots health status get")); - } - } - - Ok(StatusView { - state: state.to_owned(), - source: WORKFLOW_SOURCE.to_owned(), - account_resolution: accounts::account_resolution_view(&account_resolution), - local_state: local_status.state, - local_root: local_status.local_root, - relay_state: relay_state(config).to_owned(), - relay_count, - farm_state: farm.state.to_owned(), - ready, - needs_attention, - next, - }) -} - -fn ensure_local_status( - config: &RuntimeConfig, -) -> Result<crate::domain::runtime::LocalStatusView, RuntimeError> { - let _ = local::init(config)?; - local::status(config) -} - -#[derive(Debug, Clone)] -struct FarmWorkflowState { - state: &'static str, - primary_next_action: Option<String>, -} - -fn inspect_farm(config: &RuntimeConfig) -> Result<FarmWorkflowState, RuntimeError> { - let view = farm::status(config, &FarmScopedArgs::default())?; - if !view.config_present { - return Ok(FarmWorkflowState { - state: "missing", - primary_next_action: view.actions.into_iter().next(), - }); - } - - if view.account_state != "ready" { - return Ok(FarmWorkflowState { - state: "account_missing", - primary_next_action: view.actions.into_iter().next(), - }); - } - - let Some(config_summary) = view.config else { - return Ok(FarmWorkflowState { - state: "missing", - primary_next_action: view.actions.into_iter().next(), - }); - }; - - let published = config_summary.publication.profile_state == "published" - && config_summary.publication.farm_state == "published"; - - Ok(FarmWorkflowState { - state: if published { "published" } else { "draft" }, - primary_next_action: (!published).then(|| "radroots farm publish".to_owned()), - }) -} - -fn apply_farm_attention( - ready: &mut Vec<String>, - needs_attention: &mut Vec<String>, - next: &mut Vec<String>, - farm: &FarmWorkflowState, -) { - match farm.state { - "missing" => { - needs_attention.push("Farm draft".to_owned()); - } - "draft" => { - needs_attention.push("Farm not yet published".to_owned()); - push_next(next, Some("radroots farm publish")); - } - "published" => { - ready.push("Farm published".to_owned()); - } - "account_missing" => { - needs_attention.push("Farm draft account not available locally".to_owned()); - } - _ => {} - } -} - -fn relay_configured(config: &RuntimeConfig) -> bool { - !config.relay.urls.is_empty() -} - -fn relay_state(config: &RuntimeConfig) -> &'static str { - if relay_configured(config) { - "configured" - } else { - "unconfigured" - } -} - -fn role_name(role: SetupRoleArg) -> &'static str { - match role { - SetupRoleArg::Seller => "seller", - SetupRoleArg::Buyer => "buyer", - SetupRoleArg::Both => "both", - } -} - -fn push_unresolved_account_actions( - config: &RuntimeConfig, - next: &mut Vec<String>, - setup_role: Option<SetupRoleArg>, -) -> Result<(), RuntimeError> { - match unresolved_account_resolution_state(config)? { - UnresolvedAccountResolutionState::NoAccounts => { - push_next(next, Some("radroots account create")); - } - UnresolvedAccountResolutionState::AccountsExistWithoutDefault => { - push_next(next, Some("radroots account list")); - push_next(next, Some("radroots account select <selector>")); - } - } - - if let Some(role) = setup_role { - push_next(next, Some(setup_command(role))); - } - - Ok(()) -} - -fn unresolved_account_resolution_state( - config: &RuntimeConfig, -) -> Result<UnresolvedAccountResolutionState, RuntimeError> { - let snapshot = accounts::snapshot(config)?; - Ok(if snapshot.accounts.is_empty() { - UnresolvedAccountResolutionState::NoAccounts - } else { - UnresolvedAccountResolutionState::AccountsExistWithoutDefault - }) -} - -fn setup_command(role: SetupRoleArg) -> &'static str { - match role { - SetupRoleArg::Seller => "radroots farm create", - SetupRoleArg::Buyer => "radroots basket create", - SetupRoleArg::Both => "radroots workspace init", - } -} - -fn push_next(next: &mut Vec<String>, command: Option<&str>) { - let Some(command) = command else { - return; - }; - if !next.iter().any(|existing| existing == command) { - next.push(command.to_owned()); - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum UnresolvedAccountResolutionState { - NoAccounts, - AccountsExistWithoutDefault, -} diff --git a/src/runtime_args.rs b/src/runtime_args.rs @@ -0,0 +1,192 @@ +use std::path::PathBuf; + +use crate::runtime::config::OutputFormat; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RuntimeOutputFormatArg { + Human, + Json, + Ndjson, +} + +impl RuntimeOutputFormatArg { + pub fn as_output_format(self) -> OutputFormat { + match self { + Self::Human => OutputFormat::Human, + Self::Json => OutputFormat::Json, + Self::Ndjson => OutputFormat::Ndjson, + } + } +} + +#[derive(Debug, Clone, Default)] +pub struct RuntimeInvocationArgs { + pub output_format: Option<RuntimeOutputFormatArg>, + pub json: bool, + pub ndjson: bool, + pub env_file: Option<PathBuf>, + pub quiet: bool, + pub verbose: bool, + pub trace: bool, + pub dry_run: bool, + pub no_color: bool, + pub no_input: bool, + pub yes: bool, + pub log_filter: Option<String>, + pub log_dir: Option<PathBuf>, + pub log_stdout: bool, + pub no_log_stdout: bool, + pub account: Option<String>, + pub identity_path: Option<PathBuf>, + pub signer: Option<String>, + pub relay: Vec<String>, + pub myc_executable: Option<PathBuf>, + pub myc_status_timeout_ms: Option<u64>, + pub hyf_enabled: bool, + pub no_hyf_enabled: bool, + pub hyf_executable: Option<PathBuf>, +} + +#[derive(Debug, Clone, Copy)] +pub enum LocalExportFormatArg { + Json, + Ndjson, +} + +impl LocalExportFormatArg { + pub fn as_str(self) -> &'static str { + match self { + Self::Json => "json", + Self::Ndjson => "ndjson", + } + } +} + +#[derive(Debug, Clone)] +pub struct SyncWatchArgs { + pub frames: usize, + pub interval_ms: u64, +} + +#[derive(Debug, Clone)] +pub struct FindQueryArgs { + pub query: Vec<String>, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FarmScopeArg { + User, + Workspace, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FarmFieldArg { + Name, + DisplayName, + About, + Website, + Picture, + Banner, + Location, + City, + Region, + Country, + Delivery, +} + +#[derive(Debug, Clone, Default)] +pub struct FarmScopedArgs { + pub scope: Option<FarmScopeArg>, +} + +#[derive(Debug, Clone, Default)] +pub struct FarmCreateArgs { + pub scope: Option<FarmScopeArg>, + pub farm_d_tag: Option<String>, + pub name: Option<String>, + pub display_name: Option<String>, + pub about: Option<String>, + pub website: Option<String>, + pub picture: Option<String>, + pub banner: Option<String>, + pub location: Option<String>, + pub city: Option<String>, + pub region: Option<String>, + pub country: Option<String>, + pub delivery_method: Option<String>, +} + +#[derive(Debug, Clone)] +pub struct FarmUpdateArgs { + pub scope: Option<FarmScopeArg>, + pub field: FarmFieldArg, + pub value: Vec<String>, +} + +#[derive(Debug, Clone, Default)] +pub struct FarmPublishArgs { + pub scope: Option<FarmScopeArg>, + pub idempotency_key: Option<String>, + pub signer_session_id: Option<String>, + pub print_job: bool, + pub print_event: bool, +} + +#[derive(Debug, Clone, Default)] +pub struct ListingCreateArgs { + pub output: Option<PathBuf>, + pub key: Option<String>, + pub title: Option<String>, + pub category: Option<String>, + pub summary: Option<String>, + pub bin_id: Option<String>, + pub quantity_amount: Option<String>, + pub quantity_unit: Option<String>, + pub price_amount: Option<String>, + pub price_currency: Option<String>, + pub price_per_amount: Option<String>, + pub price_per_unit: Option<String>, + pub available: Option<String>, + pub label: Option<String>, +} + +#[derive(Debug, Clone)] +pub struct ListingFileArgs { + pub file: PathBuf, +} + +#[derive(Debug, Clone)] +pub struct ListingMutationArgs { + pub file: PathBuf, + pub idempotency_key: Option<String>, + pub signer_session_id: Option<String>, + pub print_job: bool, + pub print_event: bool, +} + +#[derive(Debug, Clone, Default)] +pub struct OrderDraftCreateArgs { + pub listing: Option<String>, + pub listing_addr: Option<String>, + pub bin_id: Option<String>, + pub bin_count: Option<u32>, +} + +#[derive(Debug, Clone)] +pub struct OrderSubmitArgs { + pub key: String, + pub idempotency_key: Option<String>, + pub signer_session_id: Option<String>, +} + +#[derive(Debug, Clone)] +pub struct OrderWatchArgs { + pub key: String, + pub frames: Option<usize>, + pub interval_ms: u64, +} + +#[derive(Debug, Clone)] +pub struct RecordLookupArgs { + pub key: String, +}