cli

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

commit 9f8d72269b7ac7b27d801b2997edb52d03bf46b3
parent 92cbd690a18ce6938284c4e779843f631a6809ea
Author: triesap <tyson@radroots.org>
Date:   Thu, 16 Apr 2026 20:04:20 +0000

add human first cli scaffolding

Diffstat:
Msrc/cli.rs | 459++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Msrc/commands/mod.rs | 35+++++++++++++++++++++++++++++++++--
Atests/help.rs | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtests/identity_commands.rs | 2+-
4 files changed, 582 insertions(+), 28 deletions(-)

diff --git a/src/cli.rs b/src/cli.rs @@ -1,4 +1,7 @@ -use clap::{error::ErrorKind, ArgAction, Args, CommandFactory, Parser, Subcommand, ValueEnum}; +use clap::{ + error::ErrorKind, ArgAction, Args, CommandFactory, FromArgMatches, Parser, Subcommand, + ValueEnum, +}; use std::ffi::{OsStr, OsString}; use std::path::PathBuf; @@ -21,6 +24,125 @@ impl OutputFormatArg { } } +const ROOT_HELP: &str = "\ +radroots - local food trade on Nostr + +Start here + setup Guided first-time setup for sellers and buyers + status Show what is ready and what needs attention + +Sell from your farm + farm Set up and publish your farm + sell Create, check, and publish listings + +Buy from the market + market Update local market data and search listings + order Create and manage order requests + +Accounts and settings + account Create and select local accounts + config Show effective configuration + +Advanced and troubleshooting + doctor Check readiness and suggest next steps + local Manage local market data storage + sync Inspect sync status and watch updates + relay Show relay configuration + net Show network posture + signer Show signer readiness + rpc Show runtime bridge status + myc Show myc status + runtime Manage runtimes + job Inspect background jobs + listing Advanced listing commands + find Advanced search command + +Global options + --output <human|json|ndjson> + --json + --ndjson + --dry-run + --no-input + --yes + --quiet + --verbose + --trace + --no-color + --account <ACCOUNT> + --signer <SIGNER> + --relay <RELAY> + +Examples + radroots setup seller + radroots market search eggs + radroots sell check ./listing.toml + radroots order create --listing sf-tomatoes --bin bin-1 --qty 2 +"; + +const SETUP_HELP: &str = "\ +Examples: + radroots setup seller + radroots setup buyer + radroots setup both + +This workflow layer is being added over the existing account, local, and farm commands. +"; + +const STATUS_HELP: &str = "\ +Examples: + radroots status + radroots doctor + radroots config show + +This workflow summary is being added over the existing readiness and configuration surfaces. +"; + +const ACCOUNT_HELP: &str = "\ +Examples: + radroots account create + radroots account view + radroots account list + radroots account select market-main + +Compatibility aliases: new, whoami, ls, use. +"; + +const FARM_HELP: &str = "\ +Examples: + radroots farm check + radroots farm show --scope workspace + radroots farm publish + +Compatibility aliases: status, get. The all-at-once `farm setup` surface remains available. +"; + +const MARKET_HELP: &str = "\ +Examples: + radroots market update + radroots market search tomatoes + radroots market view sf-tomatoes + +Compatibility paths: `sync pull`, `find`, and `listing get` remain available. +"; + +const SELL_HELP: &str = "\ +Examples: + radroots sell add --output ./listing.toml --title Tomatoes + radroots sell check ./listing.toml + radroots sell publish ./listing.toml + +Compatibility path: the advanced `listing` command family remains available. +"; + +const ORDER_HELP: &str = "\ +Examples: + radroots order create --listing sf-tomatoes --bin bin-1 --qty 2 + radroots order view ord_demo + radroots order list + +Compatibility aliases: new, get, ls. +"; + #[derive(Debug, Parser, Clone)] #[command(name = "radroots")] #[command(version)] @@ -108,13 +230,27 @@ impl CliArgs { { let args = itr.into_iter().map(Into::into).collect::<Vec<_>>(); let (filtered_args, output_format) = extract_global_output_format(args)?; - let mut parsed = <Self as Parser>::try_parse_from(filtered_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() + .override_help(ROOT_HELP) + .mut_subcommand("setup", |command| command.after_help(SETUP_HELP)) + .mut_subcommand("status", |command| command.after_help(STATUS_HELP)) + .mut_subcommand("account", |command| command.after_help(ACCOUNT_HELP)) + .mut_subcommand("farm", |command| command.after_help(FARM_HELP)) + .mut_subcommand("market", |command| command.after_help(MARKET_HELP)) + .mut_subcommand("sell", |command| command.after_help(SELL_HELP)) + .mut_subcommand("order", |command| command.after_help(ORDER_HELP)) + } + fn command_error(message: impl Into<String>, kind: ErrorKind) -> clap::Error { - let mut command = Self::command(); + let mut command = Self::build_command(); command.error(kind, message.into()) } } @@ -240,26 +376,52 @@ fn matches_local_output_context(command_tokens: &[String]) -> bool { ) || 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 and select 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), } @@ -267,10 +429,10 @@ impl Command { pub fn display_name(&self) -> &'static str { match self { Self::Account(account) => match account.command { - AccountCommand::New => "account new", - AccountCommand::Whoami => "account whoami", - AccountCommand::Ls => "account ls", - AccountCommand::Use(_) => "account use", + AccountCommand::New => "account create", + AccountCommand::Whoami => "account view", + AccountCommand::Ls => "account list", + AccountCommand::Use(_) => "account select", }, Self::Config(config) => match config.command { ConfigCommand::Show => "config show", @@ -279,12 +441,12 @@ impl Command { Self::Farm(farm) => match farm.command { FarmCommand::Publish(_) => "farm publish", FarmCommand::Setup(_) => "farm setup", - FarmCommand::Status(_) => "farm status", - FarmCommand::Get(_) => "farm get", + FarmCommand::Status(_) => "farm check", + FarmCommand::Get(_) => "farm show", }, Self::Find(_) => "find", Self::Job(job) => match job.command { - JobCommand::Ls => "job ls", + JobCommand::Ls => "job list", JobCommand::Get(_) => "job get", JobCommand::Watch(_) => "job watch", }, @@ -302,6 +464,11 @@ impl Command { 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", }, @@ -309,21 +476,36 @@ impl Command { NetCommand::Status => "net status", }, Self::Order(order) => match order.command { - OrderCommand::New(_) => "order new", - OrderCommand::Get(_) => "order get", - OrderCommand::Ls => "order ls", + 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 ls", + 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", @@ -340,6 +522,7 @@ impl Command { Self::Signer(signer) => match signer.command { SignerCommand::Status => "signer status", }, + Self::Status => "status", Self::Sync(sync) => match sync.command { SyncCommand::Status => "sync status", SyncCommand::Pull => "sync pull", @@ -369,6 +552,9 @@ impl Command { }) | Self::Sync(SyncArgs { command: SyncCommand::Watch(_), }) | Self::Find(_) + | Self::Market(MarketArgs { + command: MarketCommand::Search(_), + }) ), } } @@ -386,13 +572,30 @@ impl Command { 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)] @@ -412,9 +615,17 @@ pub struct AccountArgs { #[derive(Debug, Clone, Subcommand)] pub enum AccountCommand { + #[command(name = "create", visible_alias = "new", about = "Create a local account")] New, + #[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), } @@ -453,6 +664,7 @@ pub struct RelayArgs { #[derive(Debug, Clone, Subcommand)] pub enum RelayCommand { + #[command(name = "list", visible_alias = "ls", about = "List configured relays")] Ls, } @@ -464,9 +676,13 @@ pub struct FarmArgs { #[derive(Debug, Clone, Subcommand)] pub enum FarmCommand { + #[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), } @@ -609,6 +825,22 @@ pub struct FindArgs { } #[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, @@ -682,8 +914,11 @@ pub struct JobArgs { #[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), } @@ -761,12 +996,19 @@ pub struct OrderArgs { #[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 = "Cancel a submitted order")] Cancel(RecordKeyArgs), + #[command(about = "Show submitted order history")] History, } @@ -805,13 +1047,57 @@ pub struct RecordKeyArgs { pub key: String, } +#[derive(Debug, Clone, Args)] +pub struct SellArgs { + #[command(subcommand)] + pub command: SellCommand, +} + +#[derive(Debug, Clone, Subcommand)] +pub enum SellCommand { + #[command(about = "Create a listing draft")] + Add(ListingNewArgs), + #[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 target: String, +} + +#[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, +} + #[cfg(test)] mod tests { use super::{ AccountCommand, CliArgs, Command, ConfigCommand, FarmCommand, FarmScopeArg, JobCommand, - JobWatchArgs, ListingCommand, LocalCommand, LocalExportFormatArg, MycCommand, NetCommand, - OrderCommand, OrderWatchArgs, OutputFormatArg, RelayCommand, RpcCommand, RuntimeCommand, - RuntimeConfigCommand, SignerCommand, SyncCommand, SyncWatchArgs, + JobWatchArgs, ListingCommand, LocalCommand, LocalExportFormatArg, MarketCommand, + MycCommand, NetCommand, OrderCommand, OrderWatchArgs, OutputFormatArg, RelayCommand, + RpcCommand, RuntimeCommand, RuntimeConfigCommand, SellCommand, SetupRoleArg, + SignerCommand, SyncCommand, SyncWatchArgs, }; use crate::runtime::config::OutputFormat; #[test] @@ -993,6 +1279,111 @@ mod tests { } #[test] + fn parses_human_first_top_level_commands() { + let setup = CliArgs::parse_from(["radroots", "setup", "seller"]); + match setup.command { + Command::Setup(args) => assert_eq!(args.role, SetupRoleArg::Seller), + _ => panic!("unexpected command variant"), + } + + let status = CliArgs::parse_from(["radroots", "status"]); + assert!(matches!(status.command, Command::Status)); + + let market = CliArgs::parse_from(["radroots", "market", "search", "eggs"]); + match market.command { + Command::Market(args) => match args.command { + MarketCommand::Search(find) => assert_eq!(find.query, vec!["eggs"]), + _ => panic!("unexpected market subcommand"), + }, + _ => panic!("unexpected command variant"), + } + + let sell = CliArgs::parse_from(["radroots", "sell", "check", "draft.toml"]); + match sell.command { + Command::Sell(args) => match args.command { + SellCommand::Check(file) => assert_eq!(file.file.to_str(), Some("draft.toml")), + _ => panic!("unexpected sell subcommand"), + }, + _ => panic!("unexpected command variant"), + } + } + + #[test] + fn parses_human_first_aliases() { + let account_create = CliArgs::parse_from(["radroots", "account", "create"]); + match account_create.command { + Command::Account(account) => assert!(matches!(account.command, AccountCommand::New)), + _ => panic!("unexpected command variant"), + } + + let account_view = CliArgs::parse_from(["radroots", "account", "view"]); + match account_view.command { + Command::Account(account) => assert!(matches!(account.command, AccountCommand::Whoami)), + _ => panic!("unexpected command variant"), + } + + let account_list = CliArgs::parse_from(["radroots", "account", "list"]); + match account_list.command { + Command::Account(account) => assert!(matches!(account.command, AccountCommand::Ls)), + _ => panic!("unexpected command variant"), + } + + let account_select = CliArgs::parse_from(["radroots", "account", "select", "market-main"]); + match account_select.command { + Command::Account(account) => match account.command { + AccountCommand::Use(args) => assert_eq!(args.selector, "market-main"), + _ => panic!("unexpected account subcommand"), + }, + _ => panic!("unexpected command variant"), + } + + let farm_check = CliArgs::parse_from(["radroots", "farm", "check"]); + match farm_check.command { + Command::Farm(farm) => assert!(matches!(farm.command, FarmCommand::Status(_))), + _ => panic!("unexpected command variant"), + } + + let farm_show = CliArgs::parse_from(["radroots", "farm", "show"]); + match farm_show.command { + Command::Farm(farm) => assert!(matches!(farm.command, FarmCommand::Get(_))), + _ => panic!("unexpected command variant"), + } + + let market_view = CliArgs::parse_from(["radroots", "market", "view", "lst_123"]); + match market_view.command { + Command::Market(market) => match market.command { + MarketCommand::View(args) => assert_eq!(args.key, "lst_123"), + _ => panic!("unexpected market subcommand"), + }, + _ => panic!("unexpected command variant"), + } + + let order_create = CliArgs::parse_from(["radroots", "order", "create", "--listing", "eggs"]); + match order_create.command { + Command::Order(order) => match order.command { + OrderCommand::New(args) => assert_eq!(args.listing.as_deref(), Some("eggs")), + _ => panic!("unexpected order subcommand"), + }, + _ => panic!("unexpected command variant"), + } + + let order_view = CliArgs::parse_from(["radroots", "order", "view", "ord_demo"]); + match order_view.command { + Command::Order(order) => match order.command { + OrderCommand::Get(args) => assert_eq!(args.key, "ord_demo"), + _ => panic!("unexpected order subcommand"), + }, + _ => panic!("unexpected command variant"), + } + + let order_list = CliArgs::parse_from(["radroots", "order", "list"]); + match order_list.command { + Command::Order(order) => assert!(matches!(order.command, OrderCommand::Ls)), + _ => panic!("unexpected command variant"), + } + } + + #[test] fn parses_account_commands() { let new = CliArgs::parse_from(["radroots", "account", "new"]); match new.command { @@ -1573,9 +1964,9 @@ mod tests { .supports_output_format(OutputFormat::Ndjson)); assert!(config_show.command.supports_dry_run()); - let account_new = CliArgs::parse_from(["radroots", "account", "new"]); - assert_eq!(account_new.command.display_name(), "account new"); - assert!(!account_new.command.supports_dry_run()); + let account_create = CliArgs::parse_from(["radroots", "account", "create"]); + assert_eq!(account_create.command.display_name(), "account create"); + assert!(!account_create.command.supports_dry_run()); let farm_setup = CliArgs::parse_from([ "radroots", @@ -1589,10 +1980,10 @@ mod tests { assert_eq!(farm_setup.command.display_name(), "farm setup"); assert!(!farm_setup.command.supports_dry_run()); - let farm_status = CliArgs::parse_from(["radroots", "farm", "status"]); - assert_eq!(farm_status.command.display_name(), "farm status"); - assert!(farm_status.command.supports_dry_run()); - assert!(!farm_status + let farm_check = CliArgs::parse_from(["radroots", "farm", "check"]); + assert_eq!(farm_check.command.display_name(), "farm check"); + assert!(farm_check.command.supports_dry_run()); + assert!(!farm_check .command .supports_output_format(OutputFormat::Ndjson)); @@ -1603,11 +1994,21 @@ mod tests { let find = CliArgs::parse_from(["radroots", "find", "eggs"]); assert!(find.command.supports_output_format(OutputFormat::Ndjson)); + let market_search = CliArgs::parse_from(["radroots", "market", "search", "eggs"]); + assert_eq!(market_search.command.display_name(), "market search"); + assert!(market_search + .command + .supports_output_format(OutputFormat::Ndjson)); + let sync_watch = CliArgs::parse_from(["radroots", "sync", "watch", "--frames", "1"]); assert!(sync_watch .command .supports_output_format(OutputFormat::Ndjson)); + let sell_add = CliArgs::parse_from(["radroots", "sell", "add"]); + assert_eq!(sell_add.command.display_name(), "sell add"); + assert!(!sell_add.command.supports_dry_run()); + let order_watch = CliArgs::parse_from(["radroots", "order", "watch", "ord_demo"]); assert!(order_watch .command @@ -1617,6 +2018,14 @@ mod tests { assert_eq!(order_submit.command.display_name(), "order submit"); assert!(order_submit.command.supports_dry_run()); + let setup = CliArgs::parse_from(["radroots", "setup", "buyer"]); + assert_eq!(setup.command.display_name(), "setup buyer"); + assert!(!setup.command.supports_dry_run()); + + let status = CliArgs::parse_from(["radroots", "status"]); + assert_eq!(status.command.display_name(), "status"); + assert!(status.command.supports_dry_run()); + let runtime_status = CliArgs::parse_from(["radroots", "runtime", "status", "radrootsd"]); assert_eq!(runtime_status.command.display_name(), "runtime status"); assert!(runtime_status.command.supports_dry_run()); diff --git a/src/commands/mod.rs b/src/commands/mod.rs @@ -16,8 +16,8 @@ pub mod sync; use crate::cli::{ AccountCommand, Command, ConfigCommand, FarmCommand, JobCommand, ListingCommand, LocalCommand, - MycCommand, NetCommand, OrderCommand, RelayCommand, RpcCommand, RuntimeCommand, - RuntimeConfigCommand, SignerCommand, SyncCommand, + MarketCommand, MycCommand, NetCommand, OrderCommand, RelayCommand, RpcCommand, RuntimeCommand, + RuntimeConfigCommand, SellCommand, SignerCommand, SyncCommand, }; use crate::domain::runtime::{CommandOutput, CommandView}; use crate::runtime::RuntimeError; @@ -78,6 +78,11 @@ pub fn dispatch( LocalCommand::Export(args) => local::export(config, args), LocalCommand::Backup(args) => local::backup(config, args), }, + Command::Market(market) => match &market.command { + MarketCommand::Update => sync::pull(config), + MarketCommand::Search(args) => find::search(config, args), + MarketCommand::View(args) => listing::get(config, args), + }, Command::Net(net) => match &net.command { NetCommand::Status => net::status(config), }, @@ -97,6 +102,25 @@ pub fn dispatch( RpcCommand::Status => Ok(rpc::status(config)), RpcCommand::Sessions => Ok(rpc::sessions(config)), }, + Command::Sell(sell) => match &sell.command { + SellCommand::Add(args) => listing::new(config, args), + SellCommand::Show(_args) => planned_command( + "`sell show` will inspect local drafts in the next slice; use `listing validate <file>` for now", + ), + SellCommand::Check(args) => listing::validate(config, args), + SellCommand::Publish(args) => listing::publish(config, args), + SellCommand::Update(args) => listing::update(config, args), + SellCommand::Pause(args) => listing::archive(config, args), + SellCommand::Reprice(_args) => planned_command( + "`sell reprice` will land in the draft-mutation slice; edit the draft file directly for now", + ), + SellCommand::Restock(_args) => planned_command( + "`sell restock` will land in the draft-mutation slice; edit the draft file directly for now", + ), + }, + Command::Setup(_setup) => planned_command( + "`setup` will land in the workflow slice; use `account`, `local`, and `farm` directly for now", + ), Command::Runtime(runtime_command) => match &runtime_command.command { RuntimeCommand::Install(args) => runtime::install(config, args), RuntimeCommand::Uninstall(args) => runtime::uninstall(config, args), @@ -110,6 +134,9 @@ pub fn dispatch( RuntimeConfigCommand::Set(args) => runtime::config_set(config, args), }, }, + Command::Status => planned_command( + "`status` will land in the workflow slice; use `doctor` for readiness details right now", + ), Command::Sync(sync) => match &sync.command { SyncCommand::Status => sync::status(config), SyncCommand::Pull => sync::pull(config), @@ -118,3 +145,7 @@ pub fn dispatch( }, } } + +fn planned_command(message: &str) -> Result<CommandOutput, RuntimeError> { + Err(RuntimeError::Config(message.to_owned())) +} diff --git a/tests/help.rs b/tests/help.rs @@ -0,0 +1,114 @@ +use std::process::Command; + +use assert_cmd::prelude::*; + +fn help_command() -> Command { + Command::cargo_bin("radroots").expect("binary") +} + +#[test] +fn root_help_is_workflow_grouped() { + let output = help_command() + .arg("--help") + .output() + .expect("run root help"); + + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).expect("utf8 stdout"); + assert!(stdout.contains("Start here")); + assert!(stdout.contains("Sell from your farm")); + assert!(stdout.contains("Buy from the market")); + assert!(stdout.contains("Accounts and settings")); + assert!(stdout.contains("Advanced and troubleshooting")); + assert!(stdout.contains("setup")); + assert!(stdout.contains("status")); + assert!(stdout.contains("market")); + assert!(stdout.contains("sell")); + assert!(stdout.contains("Examples")); +} + +#[test] +fn account_help_prefers_human_first_aliases() { + let output = help_command() + .args(["account", "--help"]) + .output() + .expect("run account help"); + + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).expect("utf8 stdout"); + assert!(stdout.contains("create")); + assert!(stdout.contains("view")); + assert!(stdout.contains("list")); + assert!(stdout.contains("select")); + assert!(stdout.contains("Compatibility aliases: new, whoami, ls, use.")); +} + +#[test] +fn farm_help_mentions_human_first_subcommands() { + let output = help_command() + .args(["farm", "--help"]) + .output() + .expect("run farm help"); + + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).expect("utf8 stdout"); + assert!(stdout.contains("check")); + assert!(stdout.contains("show")); + assert!(stdout.contains("publish")); + assert!(stdout.contains("Compatibility aliases: status, get.")); +} + +#[test] +fn market_help_is_example_first() { + let output = help_command() + .args(["market", "--help"]) + .output() + .expect("run market help"); + + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).expect("utf8 stdout"); + assert!(stdout.contains("update")); + assert!(stdout.contains("search")); + assert!(stdout.contains("view")); + assert!(stdout.contains("radroots market search tomatoes")); + assert!(stdout.contains("Compatibility paths: `sync pull`, `find`, and `listing get` remain available.")); +} + +#[test] +fn sell_help_mentions_listing_compatibility() { + let output = help_command() + .args(["sell", "--help"]) + .output() + .expect("run sell help"); + + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).expect("utf8 stdout"); + assert!(stdout.contains("add")); + assert!(stdout.contains("show")); + assert!(stdout.contains("check")); + assert!(stdout.contains("publish")); + assert!(stdout.contains("update")); + assert!(stdout.contains("pause")); + assert!(stdout.contains("reprice")); + assert!(stdout.contains("restock")); + assert!(stdout.contains("Compatibility path: the advanced `listing` command family remains available.")); +} + +#[test] +fn order_help_prefers_create_view_and_list() { + let output = help_command() + .args(["order", "--help"]) + .output() + .expect("run order help"); + + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).expect("utf8 stdout"); + assert!(stdout.contains("create")); + assert!(stdout.contains("view")); + assert!(stdout.contains("list")); + assert!(stdout.contains("submit")); + assert!(stdout.contains("watch")); + assert!(stdout.contains("cancel")); + assert!(stdout.contains("history")); + assert!(stdout.contains("Compatibility aliases: new, get, ls.")); +} diff --git a/tests/identity_commands.rs b/tests/identity_commands.rs @@ -125,7 +125,7 @@ fn account_new_rejects_dry_run_without_creating_store_state() { assert!(!store_path.exists()); assert!(output.stdout.is_empty()); let stderr = String::from_utf8(output.stderr).expect("utf8 stderr"); - assert!(stderr.contains("`account new` does not support --dry-run yet")); + assert!(stderr.contains("`account create` does not support --dry-run yet")); } #[test]