commit af3f1a213c721b7d6d9ec33a1fcd4cf073c72f73
parent 5c0844bdee59e50c7b1b50c71bca9ad27696acce
Author: triesap <tyson@radroots.org>
Date: Tue, 7 Apr 2026 02:51:48 +0000
freeze cli v1 taxonomy and roots
Diffstat:
10 files changed, 577 insertions(+), 158 deletions(-)
diff --git a/src/cli.rs b/src/cli.rs
@@ -29,33 +29,50 @@ pub struct CliArgs {
#[derive(Debug, Clone, Subcommand)]
pub enum Command {
- Identity(IdentityArgs),
+ Account(AccountArgs),
+ Config(ConfigArgs),
+ Doctor,
+ Find(FindArgs),
+ Job(JobArgs),
+ Listing(ListingArgs),
+ Local(LocalArgs),
Myc(MycArgs),
- Runtime(RuntimeArgs),
+ Net(NetArgs),
+ Order(OrderArgs),
+ Relay(RelayArgs),
+ Rpc(RpcArgs),
Signer(SignerArgs),
+ Sync(SyncArgs),
}
#[derive(Debug, Clone, Args)]
-pub struct RuntimeArgs {
+pub struct ConfigArgs {
#[command(subcommand)]
- pub command: RuntimeCommand,
+ pub command: ConfigCommand,
}
#[derive(Debug, Clone, Subcommand)]
-pub enum RuntimeCommand {
+pub enum ConfigCommand {
Show,
}
#[derive(Debug, Clone, Args)]
-pub struct IdentityArgs {
+pub struct AccountArgs {
#[command(subcommand)]
- pub command: IdentityCommand,
+ pub command: AccountCommand,
}
#[derive(Debug, Clone, Subcommand)]
-pub enum IdentityCommand {
- Init,
- Show,
+pub enum AccountCommand {
+ New,
+ Whoami,
+ Ls,
+ Use(AccountUseArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct AccountUseArgs {
+ pub selector: String,
}
#[derive(Debug, Clone, Args)]
@@ -80,21 +97,141 @@ pub enum SignerCommand {
Status,
}
+#[derive(Debug, Clone, Args)]
+pub struct RelayArgs {
+ #[command(subcommand)]
+ pub command: RelayCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum RelayCommand {
+ Ls,
+}
+
+#[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,
+ Backup,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct SyncArgs {
+ #[command(subcommand)]
+ pub command: SyncCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum SyncCommand {
+ Status,
+ Pull,
+ Push,
+ Watch,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct FindArgs {
+ #[arg(value_name = "query", num_args = 1..)]
+ pub query: Vec<String>,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct ListingArgs {
+ #[command(subcommand)]
+ pub command: ListingCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum ListingCommand {
+ New,
+ Validate,
+ Get(RecordKeyArgs),
+ Publish,
+ Update(RecordKeyArgs),
+ Archive(RecordKeyArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct JobArgs {
+ #[command(subcommand)]
+ pub command: JobCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum JobCommand {
+ Ls,
+ Get(RecordKeyArgs),
+ Watch(RecordKeyArgs),
+}
+
+#[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 OrderArgs {
+ #[command(subcommand)]
+ pub command: OrderCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum OrderCommand {
+ New,
+ Get(RecordKeyArgs),
+ Ls,
+ Submit,
+ Watch(RecordKeyArgs),
+ Cancel(RecordKeyArgs),
+ History,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct RecordKeyArgs {
+ pub key: String,
+}
+
#[cfg(test)]
mod tests {
- use super::{CliArgs, Command, IdentityCommand, MycCommand, RuntimeCommand, SignerCommand};
+ use super::{
+ AccountCommand, CliArgs, Command, ConfigCommand, JobCommand, ListingCommand, LocalCommand,
+ MycCommand, NetCommand, OrderCommand, RelayCommand, RpcCommand, SignerCommand, SyncCommand,
+ };
use clap::Parser;
#[test]
- fn parses_runtime_show_command() {
- let parsed = CliArgs::parse_from(["radroots", "runtime", "show"]);
+ fn parses_config_show_command() {
+ let parsed = CliArgs::parse_from(["radroots", "config", "show"]);
match parsed.command {
- Command::Identity(_) | Command::Myc(_) | Command::Signer(_) => {
- panic!("unexpected command variant")
- }
- Command::Runtime(runtime) => match runtime.command {
- RuntimeCommand::Show => {}
+ Command::Config(config) => match config.command {
+ ConfigCommand::Show => {}
},
+ _ => panic!("unexpected command variant"),
}
}
@@ -116,7 +253,7 @@ mod tests {
"myc",
"--myc-executable",
"bin/myc",
- "runtime",
+ "config",
"show",
]);
assert!(parsed.json);
@@ -151,27 +288,41 @@ mod tests {
}
#[test]
- fn parses_identity_commands() {
- let init = CliArgs::parse_from(["radroots", "identity", "init"]);
- match init.command {
- Command::Identity(identity) => match identity.command {
- IdentityCommand::Init => {}
- IdentityCommand::Show => panic!("unexpected identity subcommand"),
+ fn parses_account_commands() {
+ let new = CliArgs::parse_from(["radroots", "account", "new"]);
+ match new.command {
+ Command::Account(account) => match account.command {
+ AccountCommand::New => {}
+ _ => panic!("unexpected account subcommand"),
+ },
+ _ => panic!("unexpected command variant"),
+ }
+
+ let whoami = CliArgs::parse_from(["radroots", "account", "whoami"]);
+ match whoami.command {
+ Command::Account(account) => match account.command {
+ AccountCommand::Whoami => {}
+ _ => panic!("unexpected account subcommand"),
},
- Command::Myc(_) | Command::Runtime(_) | Command::Signer(_) => {
- panic!("unexpected command variant")
- }
+ _ => panic!("unexpected command variant"),
}
- let show = CliArgs::parse_from(["radroots", "identity", "show"]);
- match show.command {
- Command::Identity(identity) => match identity.command {
- IdentityCommand::Show => {}
- IdentityCommand::Init => panic!("unexpected identity subcommand"),
+ let ls = CliArgs::parse_from(["radroots", "account", "ls"]);
+ match ls.command {
+ Command::Account(account) => match account.command {
+ AccountCommand::Ls => {}
+ _ => panic!("unexpected account subcommand"),
},
- Command::Myc(_) | Command::Runtime(_) | Command::Signer(_) => {
- panic!("unexpected command variant")
- }
+ _ => panic!("unexpected command variant"),
+ }
+
+ let use_account = CliArgs::parse_from(["radroots", "account", "use", "market-main"]);
+ match use_account.command {
+ Command::Account(account) => match account.command {
+ AccountCommand::Use(args) => assert_eq!(args.selector, "market-main"),
+ _ => panic!("unexpected account subcommand"),
+ },
+ _ => panic!("unexpected command variant"),
}
}
@@ -182,9 +333,7 @@ mod tests {
Command::Signer(signer) => match signer.command {
SignerCommand::Status => {}
},
- Command::Identity(_) | Command::Myc(_) | Command::Runtime(_) => {
- panic!("unexpected command variant")
- }
+ _ => panic!("unexpected command variant"),
}
}
@@ -195,9 +344,89 @@ mod tests {
Command::Myc(myc) => match myc.command {
MycCommand::Status => {}
},
- Command::Identity(_) | Command::Runtime(_) | Command::Signer(_) => {
- panic!("unexpected command variant")
- }
+ _ => panic!("unexpected command variant"),
+ }
+ }
+
+ #[test]
+ fn parses_v1_command_skeleton() {
+ let doctor = CliArgs::parse_from(["radroots", "doctor"]);
+ assert!(matches!(doctor.command, Command::Doctor));
+
+ let find = CliArgs::parse_from(["radroots", "find", "tomatoes"]);
+ match find.command {
+ Command::Find(args) => assert_eq!(args.query, vec!["tomatoes"]),
+ _ => panic!("unexpected command variant"),
+ }
+
+ let relay = CliArgs::parse_from(["radroots", "relay", "ls"]);
+ match relay.command {
+ Command::Relay(args) => match args.command {
+ RelayCommand::Ls => {}
+ },
+ _ => panic!("unexpected command variant"),
+ }
+
+ let net = CliArgs::parse_from(["radroots", "net", "status"]);
+ match net.command {
+ Command::Net(args) => match args.command {
+ NetCommand::Status => {}
+ },
+ _ => panic!("unexpected command variant"),
+ }
+
+ let local = CliArgs::parse_from(["radroots", "local", "init"]);
+ match local.command {
+ Command::Local(args) => match args.command {
+ LocalCommand::Init => {}
+ _ => panic!("unexpected local subcommand"),
+ },
+ _ => panic!("unexpected command variant"),
+ }
+
+ let sync = CliArgs::parse_from(["radroots", "sync", "status"]);
+ match sync.command {
+ Command::Sync(args) => match args.command {
+ SyncCommand::Status => {}
+ _ => panic!("unexpected sync subcommand"),
+ },
+ _ => panic!("unexpected command variant"),
+ }
+
+ let listing = CliArgs::parse_from(["radroots", "listing", "get", "lst_123"]);
+ match listing.command {
+ Command::Listing(args) => match args.command {
+ ListingCommand::Get(key) => assert_eq!(key.key, "lst_123"),
+ _ => panic!("unexpected listing subcommand"),
+ },
+ _ => panic!("unexpected command variant"),
+ }
+
+ let job = CliArgs::parse_from(["radroots", "job", "get", "job_123"]);
+ match job.command {
+ Command::Job(args) => match args.command {
+ JobCommand::Get(key) => assert_eq!(key.key, "job_123"),
+ _ => panic!("unexpected job subcommand"),
+ },
+ _ => panic!("unexpected command variant"),
+ }
+
+ let rpc = CliArgs::parse_from(["radroots", "rpc", "status"]);
+ match rpc.command {
+ Command::Rpc(args) => match args.command {
+ RpcCommand::Status => {}
+ _ => panic!("unexpected rpc subcommand"),
+ },
+ _ => panic!("unexpected command variant"),
+ }
+
+ let order = CliArgs::parse_from(["radroots", "order", "history"]);
+ match order.command {
+ Command::Order(args) => match args.command {
+ OrderCommand::History => {}
+ _ => panic!("unexpected order subcommand"),
+ },
+ _ => panic!("unexpected command variant"),
}
}
}
diff --git a/src/commands/identity.rs b/src/commands/identity.rs
@@ -1,15 +1,15 @@
use crate::domain::runtime::{
- CommandDisposition, CommandOutput, CommandView, IdentityInitView, IdentityPublicView,
- IdentityShowView,
+ AccountNewView, AccountWhoamiView, CommandDisposition, CommandOutput, CommandView,
+ IdentityPublicView,
};
use crate::runtime::RuntimeError;
use crate::runtime::config::RuntimeConfig;
use crate::runtime::identity::{initialize_identity, load_identity};
use radroots_identity::IdentityError;
-pub fn init(config: &RuntimeConfig) -> Result<IdentityInitView, RuntimeError> {
+pub fn init(config: &RuntimeConfig) -> Result<AccountNewView, RuntimeError> {
let identity = initialize_identity(&config.identity)?;
- Ok(IdentityInitView {
+ Ok(AccountNewView {
path: identity.path.display().to_string(),
created: identity.created,
public_identity: IdentityPublicView::from_public_identity(&identity.public_identity),
@@ -18,7 +18,7 @@ pub fn init(config: &RuntimeConfig) -> Result<IdentityInitView, RuntimeError> {
pub fn show(config: &RuntimeConfig) -> Result<CommandOutput, RuntimeError> {
let view = match load_identity(&config.identity) {
- Ok(identity) => IdentityShowView {
+ Ok(identity) => AccountWhoamiView {
path: identity.path.display().to_string(),
state: "ready".to_owned(),
reason: None,
@@ -26,7 +26,7 @@ pub fn show(config: &RuntimeConfig) -> Result<CommandOutput, RuntimeError> {
&identity.public_identity,
)),
},
- Err(RuntimeError::Identity(IdentityError::NotFound(path))) => IdentityShowView {
+ Err(RuntimeError::Identity(IdentityError::NotFound(path))) => AccountWhoamiView {
path: path.display().to_string(),
state: "unconfigured".to_owned(),
reason: Some(format!(
@@ -39,15 +39,15 @@ pub fn show(config: &RuntimeConfig) -> Result<CommandOutput, RuntimeError> {
};
Ok(match view.disposition() {
- CommandDisposition::Success => CommandOutput::success(CommandView::IdentityShow(view)),
+ CommandDisposition::Success => CommandOutput::success(CommandView::AccountWhoami(view)),
CommandDisposition::Unconfigured => {
- CommandOutput::unconfigured(CommandView::IdentityShow(view))
+ CommandOutput::unconfigured(CommandView::AccountWhoami(view))
}
CommandDisposition::ExternalUnavailable => {
- CommandOutput::external_unavailable(CommandView::IdentityShow(view))
+ CommandOutput::external_unavailable(CommandView::AccountWhoami(view))
}
CommandDisposition::InternalError => {
- CommandOutput::internal_error(CommandView::IdentityShow(view))
+ CommandOutput::internal_error(CommandView::AccountWhoami(view))
}
})
}
diff --git a/src/commands/mod.rs b/src/commands/mod.rs
@@ -3,7 +3,10 @@ pub mod myc;
pub mod runtime;
pub mod signer;
-use crate::cli::{Command, IdentityCommand, MycCommand, RuntimeCommand, SignerCommand};
+use crate::cli::{
+ AccountCommand, Command, ConfigCommand, JobCommand, ListingCommand, LocalCommand, MycCommand,
+ NetCommand, OrderCommand, RelayCommand, RpcCommand, SignerCommand, SyncCommand,
+};
use crate::domain::runtime::{CommandOutput, CommandView};
use crate::runtime::RuntimeError;
use crate::runtime::config::RuntimeConfig;
@@ -15,22 +18,76 @@ pub fn dispatch(
logging: &LoggingState,
) -> Result<CommandOutput, RuntimeError> {
match command {
- Command::Identity(identity) => match identity.command {
- IdentityCommand::Init => Ok(CommandOutput::success(CommandView::IdentityInit(
+ Command::Account(account) => match &account.command {
+ AccountCommand::New => Ok(CommandOutput::success(CommandView::AccountNew(
identity::init(config)?,
))),
- IdentityCommand::Show => identity::show(config),
+ AccountCommand::Whoami => identity::show(config),
+ AccountCommand::Ls => unimplemented_command("account ls"),
+ AccountCommand::Use(_) => unimplemented_command("account use"),
},
- Command::Myc(myc) => match myc.command {
+ Command::Myc(myc) => match &myc.command {
MycCommand::Status => Ok(myc::status(config)),
},
- Command::Runtime(runtime) => match runtime.command {
- RuntimeCommand::Show => Ok(CommandOutput::success(CommandView::RuntimeShow(
+ 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 {
+ Command::Signer(signer) => match &signer.command {
SignerCommand::Status => Ok(signer::status(config)),
},
+ Command::Doctor => unimplemented_command("doctor"),
+ Command::Find(_) => unimplemented_command("find"),
+ Command::Job(job) => match &job.command {
+ JobCommand::Ls => unimplemented_command("job ls"),
+ JobCommand::Get(_) => unimplemented_command("job get"),
+ JobCommand::Watch(_) => unimplemented_command("job watch"),
+ },
+ Command::Listing(listing) => match &listing.command {
+ ListingCommand::New => unimplemented_command("listing new"),
+ ListingCommand::Validate => unimplemented_command("listing validate"),
+ ListingCommand::Get(_) => unimplemented_command("listing get"),
+ ListingCommand::Publish => unimplemented_command("listing publish"),
+ ListingCommand::Update(_) => unimplemented_command("listing update"),
+ ListingCommand::Archive(_) => unimplemented_command("listing archive"),
+ },
+ Command::Local(local) => match &local.command {
+ LocalCommand::Init => unimplemented_command("local init"),
+ LocalCommand::Status => unimplemented_command("local status"),
+ LocalCommand::Export => unimplemented_command("local export"),
+ LocalCommand::Backup => unimplemented_command("local backup"),
+ },
+ Command::Net(net) => match &net.command {
+ NetCommand::Status => unimplemented_command("net status"),
+ },
+ Command::Order(order) => match &order.command {
+ OrderCommand::New => unimplemented_command("order new"),
+ OrderCommand::Get(_) => unimplemented_command("order get"),
+ OrderCommand::Ls => unimplemented_command("order ls"),
+ OrderCommand::Submit => unimplemented_command("order submit"),
+ OrderCommand::Watch(_) => unimplemented_command("order watch"),
+ OrderCommand::Cancel(_) => unimplemented_command("order cancel"),
+ OrderCommand::History => unimplemented_command("order history"),
+ },
+ Command::Relay(relay) => match &relay.command {
+ RelayCommand::Ls => unimplemented_command("relay ls"),
+ },
+ Command::Rpc(rpc) => match &rpc.command {
+ RpcCommand::Status => unimplemented_command("rpc status"),
+ RpcCommand::Sessions => unimplemented_command("rpc sessions"),
+ },
+ Command::Sync(sync) => match &sync.command {
+ SyncCommand::Status => unimplemented_command("sync status"),
+ SyncCommand::Pull => unimplemented_command("sync pull"),
+ SyncCommand::Push => unimplemented_command("sync push"),
+ SyncCommand::Watch => unimplemented_command("sync watch"),
+ },
}
}
+
+fn unimplemented_command(name: &str) -> Result<CommandOutput, RuntimeError> {
+ Err(RuntimeError::Config(format!(
+ "`{name}` is not implemented yet"
+ )))
+}
diff --git a/src/commands/runtime.rs b/src/commands/runtime.rs
@@ -1,12 +1,18 @@
use crate::domain::runtime::{
- IdentityRuntimeView, LoggingRuntimeView, MycRuntimeView, RuntimeShowView, SignerRuntimeView,
+ AccountRuntimeView, ConfigShowView, LoggingRuntimeView, MycRuntimeView, PathsRuntimeView,
+ SignerRuntimeView,
};
use crate::runtime::config::RuntimeConfig;
use crate::runtime::logging::LoggingState;
-pub fn show(config: &RuntimeConfig, logging: &LoggingState) -> RuntimeShowView {
- RuntimeShowView {
+pub fn show(config: &RuntimeConfig, logging: &LoggingState) -> ConfigShowView {
+ ConfigShowView {
output_format: config.output_format.as_str().to_owned(),
+ paths: PathsRuntimeView {
+ user_config_path: config.paths.user_config_path.display().to_string(),
+ workspace_config_path: config.paths.workspace_config_path.display().to_string(),
+ user_state_root: config.paths.user_state_root.display().to_string(),
+ },
logging: LoggingRuntimeView {
initialized: logging.initialized,
filter: config.logging.filter.clone(),
@@ -21,8 +27,8 @@ pub fn show(config: &RuntimeConfig, logging: &LoggingState) -> RuntimeShowView {
.as_ref()
.map(|path| path.display().to_string()),
},
- identity: IdentityRuntimeView {
- path: config.identity.path.display().to_string(),
+ account: AccountRuntimeView {
+ identity_path: config.identity.path.display().to_string(),
},
signer: SignerRuntimeView {
backend: config.signer.backend.as_str().to_owned(),
diff --git a/src/domain/runtime.rs b/src/domain/runtime.rs
@@ -67,18 +67,19 @@ impl CommandDisposition {
#[derive(Debug, Clone)]
pub enum CommandView {
- IdentityInit(IdentityInitView),
- IdentityShow(IdentityShowView),
+ AccountNew(AccountNewView),
+ AccountWhoami(AccountWhoamiView),
+ ConfigShow(ConfigShowView),
MycStatus(MycStatusView),
- RuntimeShow(RuntimeShowView),
SignerStatus(SignerStatusView),
}
#[derive(Debug, Clone, Serialize)]
-pub struct RuntimeShowView {
+pub struct ConfigShowView {
pub output_format: String,
+ pub paths: PathsRuntimeView,
pub logging: LoggingRuntimeView,
- pub identity: IdentityRuntimeView,
+ pub account: AccountRuntimeView,
pub signer: SignerRuntimeView,
pub myc: MycRuntimeView,
}
@@ -93,8 +94,15 @@ pub struct LoggingRuntimeView {
}
#[derive(Debug, Clone, Serialize)]
-pub struct IdentityRuntimeView {
- pub path: String,
+pub struct PathsRuntimeView {
+ pub user_config_path: String,
+ pub workspace_config_path: String,
+ pub user_state_root: String,
+}
+
+#[derive(Debug, Clone, Serialize)]
+pub struct AccountRuntimeView {
+ pub identity_path: String,
}
#[derive(Debug, Clone, Serialize)]
@@ -125,7 +133,7 @@ impl IdentityPublicView {
}
#[derive(Debug, Clone, Serialize)]
-pub struct IdentityShowView {
+pub struct AccountWhoamiView {
pub path: String,
pub state: String,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -134,7 +142,7 @@ pub struct IdentityShowView {
pub public_identity: Option<IdentityPublicView>,
}
-impl IdentityShowView {
+impl AccountWhoamiView {
pub fn disposition(&self) -> CommandDisposition {
match self.state.as_str() {
"unconfigured" => CommandDisposition::Unconfigured,
@@ -144,7 +152,7 @@ impl IdentityShowView {
}
#[derive(Debug, Clone, Serialize)]
-pub struct IdentityInitView {
+pub struct AccountNewView {
pub path: String,
pub created: bool,
pub public_identity: IdentityPublicView,
diff --git a/src/render/mod.rs b/src/render/mod.rs
@@ -14,8 +14,8 @@ pub fn render_output(output: &CommandOutput, format: OutputFormat) -> Result<(),
fn render_human(output: &CommandOutput) -> Result<(), RuntimeError> {
let mut stdout = io::stdout().lock();
match output.view() {
- CommandView::IdentityInit(view) => {
- writeln!(stdout, "identity init")?;
+ CommandView::AccountNew(view) => {
+ writeln!(stdout, "account new")?;
writeln!(stdout, " path: {}", view.path)?;
writeln!(stdout, " created: {}", yes_no(view.created))?;
writeln!(stdout, " id: {}", view.public_identity.id)?;
@@ -30,15 +30,13 @@ fn render_human(output: &CommandOutput) -> Result<(), RuntimeError> {
view.public_identity.public_key_npub
)?;
}
- CommandView::IdentityShow(view) => {
- writeln!(stdout, "identity")?;
+ CommandView::AccountWhoami(view) => {
+ writeln!(stdout, "account")?;
writeln!(stdout, " path: {}", view.path)?;
writeln!(stdout, " state: {}", view.state)?;
- writeln!(
- stdout,
- " reason: {}",
- view.reason.as_deref().unwrap_or("<none>")
- )?;
+ if let Some(reason) = &view.reason {
+ writeln!(stdout, " reason: {reason}")?;
+ }
if let Some(public_identity) = &view.public_identity {
writeln!(stdout, " id: {}", public_identity.id)?;
writeln!(
@@ -56,9 +54,17 @@ fn render_human(output: &CommandOutput) -> Result<(), RuntimeError> {
CommandView::MycStatus(view) => {
render_myc_status(&mut stdout, view)?;
}
- CommandView::RuntimeShow(view) => {
- writeln!(stdout, "runtime")?;
+ CommandView::ConfigShow(view) => {
+ writeln!(stdout, "config")?;
writeln!(stdout, " output format: {}", view.output_format)?;
+ writeln!(stdout, "paths")?;
+ writeln!(stdout, " user config: {}", view.paths.user_config_path)?;
+ writeln!(
+ stdout,
+ " workspace config: {}",
+ view.paths.workspace_config_path
+ )?;
+ writeln!(stdout, " user state root: {}", view.paths.user_state_root)?;
writeln!(stdout, "logging")?;
writeln!(
stdout,
@@ -67,18 +73,14 @@ fn render_human(output: &CommandOutput) -> Result<(), RuntimeError> {
)?;
writeln!(stdout, " filter: {}", view.logging.filter)?;
writeln!(stdout, " stdout: {}", yes_no(view.logging.stdout))?;
- writeln!(
- stdout,
- " directory: {}",
- view.logging.directory.as_deref().unwrap_or("<disabled>")
- )?;
- writeln!(
- stdout,
- " current file: {}",
- view.logging.current_file.as_deref().unwrap_or("<disabled>")
- )?;
- writeln!(stdout, "identity")?;
- writeln!(stdout, " path: {}", view.identity.path)?;
+ if let Some(directory) = &view.logging.directory {
+ writeln!(stdout, " directory: {directory}")?;
+ }
+ if let Some(current_file) = &view.logging.current_file {
+ writeln!(stdout, " current file: {current_file}")?;
+ }
+ writeln!(stdout, "account")?;
+ writeln!(stdout, " identity path: {}", view.account.identity_path)?;
writeln!(stdout, "signer")?;
writeln!(stdout, " backend: {}", view.signer.backend)?;
writeln!(stdout, "myc")?;
@@ -107,11 +109,11 @@ fn render_human(output: &CommandOutput) -> Result<(), RuntimeError> {
fn render_json(output: &CommandOutput) -> Result<(), RuntimeError> {
let mut stdout = io::stdout().lock();
match output.view() {
- CommandView::IdentityInit(view) => {
+ CommandView::AccountNew(view) => {
serde_json::to_writer_pretty(&mut stdout, view)?;
writeln!(stdout)?;
}
- CommandView::IdentityShow(view) => {
+ CommandView::AccountWhoami(view) => {
serde_json::to_writer_pretty(&mut stdout, view)?;
writeln!(stdout)?;
}
@@ -119,7 +121,7 @@ fn render_json(output: &CommandOutput) -> Result<(), RuntimeError> {
serde_json::to_writer_pretty(&mut stdout, view)?;
writeln!(stdout)?;
}
- CommandView::RuntimeShow(view) => {
+ CommandView::ConfigShow(view) => {
serde_json::to_writer_pretty(&mut stdout, view)?;
writeln!(stdout)?;
}
@@ -233,16 +235,21 @@ fn render_myc_custody_identity(
mod tests {
use crate::commands::runtime;
use crate::runtime::config::{
- IdentityConfig, LoggingConfig, MycConfig, OutputFormat, RuntimeConfig, SignerBackend,
- SignerConfig,
+ IdentityConfig, LoggingConfig, MycConfig, OutputFormat, PathsConfig, RuntimeConfig,
+ SignerBackend, SignerConfig,
};
use crate::runtime::logging::LoggingState;
#[test]
- fn human_render_contains_runtime_sections() {
+ fn human_render_contains_config_sections() {
let view = runtime::show(
&RuntimeConfig {
output_format: OutputFormat::Human,
+ paths: PathsConfig {
+ user_config_path: "/home/tester/.config/radroots/config.toml".into(),
+ workspace_config_path: "/workspace/.radroots/config.toml".into(),
+ user_state_root: "/home/tester/.local/share/radroots".into(),
+ },
logging: LoggingConfig {
filter: "info".to_owned(),
directory: None,
@@ -263,16 +270,11 @@ mod tests {
current_file: None,
},
);
- let rendered = format!(
- "runtime\n output format: {}\nlogging\n initialized: {}\n",
- view.output_format,
- if view.logging.initialized {
- "yes"
- } else {
- "no"
- }
+ assert_eq!(view.output_format, "human");
+ assert_eq!(
+ view.paths.workspace_config_path,
+ "/workspace/.radroots/config.toml"
);
- assert!(rendered.contains("runtime"));
- assert!(rendered.contains("logging"));
+ assert_eq!(view.account.identity_path, "identity.json");
}
}
diff --git a/src/runtime/config.rs b/src/runtime/config.rs
@@ -8,6 +8,9 @@ use crate::runtime::RuntimeError;
const DEFAULT_LOG_FILTER: &str = "info";
const DEFAULT_ENV_PATH: &str = ".env";
+const DEFAULT_WORKSPACE_CONFIG_PATH: &str = ".radroots/config.toml";
+const DEFAULT_USER_CONFIG_PATH: &str = ".config/radroots/config.toml";
+const DEFAULT_USER_STATE_ROOT: &str = ".local/share/radroots";
const ENV_FILE_PATH: &str = "RADROOTS_ENV_FILE";
const ENV_OUTPUT: &str = "RADROOTS_OUTPUT";
const ENV_CLI_LOG_FILTER: &str = "RADROOTS_CLI_LOGGING_FILTER";
@@ -87,17 +90,27 @@ pub struct MycConfig {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RuntimeConfig {
pub output_format: OutputFormat,
+ pub paths: PathsConfig,
pub logging: LoggingConfig,
pub identity: IdentityConfig,
pub signer: SignerConfig,
pub myc: MycConfig,
}
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct PathsConfig {
+ pub user_config_path: PathBuf,
+ pub workspace_config_path: PathBuf,
+ pub user_state_root: PathBuf,
+}
+
#[derive(Debug, Default)]
struct EnvFileValues(BTreeMap<String, String>);
pub trait Environment {
fn var(&self, key: &str) -> Option<String>;
+ fn current_dir(&self) -> Result<PathBuf, RuntimeError>;
+ fn home_dir(&self) -> Option<PathBuf>;
}
pub struct SystemEnvironment;
@@ -106,6 +119,16 @@ impl Environment for SystemEnvironment {
fn var(&self, key: &str) -> Option<String> {
std::env::var(key).ok()
}
+
+ fn current_dir(&self) -> Result<PathBuf, RuntimeError> {
+ std::env::current_dir().map_err(|err| {
+ RuntimeError::Config(format!("failed to resolve current directory: {err}"))
+ })
+ }
+
+ fn home_dir(&self) -> Option<PathBuf> {
+ std::env::var_os("HOME").map(PathBuf::from)
+ }
}
impl RuntimeConfig {
@@ -123,6 +146,7 @@ impl RuntimeConfig {
) -> Result<Self, RuntimeError> {
Ok(Self {
output_format: resolve_output_format(args, env, env_file)?,
+ paths: resolve_paths(env)?,
logging: LoggingConfig {
filter: args
.log_filter
@@ -170,6 +194,21 @@ impl RuntimeConfig {
}
}
+fn resolve_paths(env: &dyn Environment) -> Result<PathsConfig, RuntimeError> {
+ let current_dir = env.current_dir()?;
+ let home_dir = env.home_dir().ok_or_else(|| {
+ RuntimeError::Config(
+ "failed to resolve home directory for Radroots config roots".to_owned(),
+ )
+ })?;
+
+ Ok(PathsConfig {
+ user_config_path: home_dir.join(DEFAULT_USER_CONFIG_PATH),
+ workspace_config_path: current_dir.join(DEFAULT_WORKSPACE_CONFIG_PATH),
+ user_state_root: home_dir.join(DEFAULT_USER_STATE_ROOT),
+ })
+}
+
fn resolve_env_file_path(args: &CliArgs, env: &dyn Environment) -> Option<PathBuf> {
args.env_file
.clone()
@@ -329,7 +368,7 @@ fn parse_bool_env(key: &str, value: &str) -> Result<bool, RuntimeError> {
#[cfg(test)]
mod tests {
use super::{
- EnvFileValues, Environment, OutputFormat, RuntimeConfig, SignerBackend,
+ EnvFileValues, Environment, OutputFormat, PathsConfig, RuntimeConfig, SignerBackend,
parse_env_file_values,
};
use crate::cli::CliArgs;
@@ -337,11 +376,33 @@ mod tests {
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
- struct MapEnvironment(BTreeMap<String, String>);
+ struct MapEnvironment {
+ values: BTreeMap<String, String>,
+ current_dir: PathBuf,
+ home_dir: PathBuf,
+ }
+
+ impl MapEnvironment {
+ fn new(values: BTreeMap<String, String>) -> Self {
+ Self {
+ values,
+ current_dir: PathBuf::from("/workspaces/radroots-cli"),
+ home_dir: PathBuf::from("/home/tester"),
+ }
+ }
+ }
impl Environment for MapEnvironment {
fn var(&self, key: &str) -> Option<String> {
- self.0.get(key).cloned()
+ self.values.get(key).cloned()
+ }
+
+ fn current_dir(&self) -> Result<PathBuf, crate::runtime::RuntimeError> {
+ Ok(self.current_dir.clone())
+ }
+
+ fn home_dir(&self) -> Option<PathBuf> {
+ Some(self.home_dir.clone())
}
}
@@ -359,10 +420,10 @@ mod tests {
"local",
"--myc-executable",
"bin/myc-cli",
- "runtime",
+ "config",
"show",
]);
- let env = MapEnvironment(BTreeMap::from([
+ let env = MapEnvironment::new(BTreeMap::from([
("RADROOTS_OUTPUT".to_owned(), "human".to_owned()),
("RADROOTS_LOG_FILTER".to_owned(), "trace".to_owned()),
("RADROOTS_LOG_STDOUT".to_owned(), "false".to_owned()),
@@ -377,6 +438,16 @@ mod tests {
let resolved = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
.expect("resolve runtime config");
assert_eq!(resolved.output_format, OutputFormat::Json);
+ assert_eq!(
+ resolved.paths,
+ PathsConfig {
+ user_config_path: PathBuf::from("/home/tester/.config/radroots/config.toml"),
+ workspace_config_path: PathBuf::from(
+ "/workspaces/radroots-cli/.radroots/config.toml"
+ ),
+ user_state_root: PathBuf::from("/home/tester/.local/share/radroots"),
+ }
+ );
assert_eq!(resolved.logging.filter, "debug");
assert!(resolved.logging.stdout);
assert_eq!(
@@ -389,8 +460,8 @@ mod tests {
#[test]
fn environment_values_fill_missing_flags() {
- let args = CliArgs::parse_from(["radroots", "runtime", "show"]);
- let env = MapEnvironment(BTreeMap::from([
+ let args = CliArgs::parse_from(["radroots", "config", "show"]);
+ let env = MapEnvironment::new(BTreeMap::from([
("RADROOTS_OUTPUT".to_owned(), "json".to_owned()),
(
"RADROOTS_LOG_FILTER".to_owned(),
@@ -426,10 +497,10 @@ mod tests {
"radroots",
"--log-stdout",
"--no-log-stdout",
- "runtime",
+ "config",
"show",
]);
- let env = MapEnvironment(BTreeMap::new());
+ 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"));
@@ -437,8 +508,8 @@ mod tests {
#[test]
fn invalid_environment_value_fails() {
- let args = CliArgs::parse_from(["radroots", "runtime", "show"]);
- let env = MapEnvironment(BTreeMap::from([(
+ let args = CliArgs::parse_from(["radroots", "config", "show"]);
+ let env = MapEnvironment::new(BTreeMap::from([(
"RADROOTS_LOG_STDOUT".to_owned(),
"maybe".to_owned(),
)]));
@@ -449,8 +520,8 @@ mod tests {
#[test]
fn env_file_values_fill_missing_flags() {
- let args = CliArgs::parse_from(["radroots", "runtime", "show"]);
- let env = MapEnvironment(BTreeMap::new());
+ let args = CliArgs::parse_from(["radroots", "config", "show"]);
+ let env = MapEnvironment::new(BTreeMap::new());
let env_file = parse_env_file_values(
r#"
RADROOTS_OUTPUT=json
@@ -481,8 +552,8 @@ RADROOTS_MYC_EXECUTABLE=bin/myc
#[test]
fn process_environment_overrides_env_file_values() {
- let args = CliArgs::parse_from(["radroots", "runtime", "show"]);
- let env = MapEnvironment(BTreeMap::from([
+ let args = CliArgs::parse_from(["radroots", "config", "show"]);
+ let env = MapEnvironment::new(BTreeMap::from([
("RADROOTS_LOG_FILTER".to_owned(), "info".to_owned()),
("RADROOTS_LOG_STDOUT".to_owned(), "true".to_owned()),
]));
@@ -502,6 +573,27 @@ RADROOTS_CLI_LOGGING_STDOUT=false
}
#[test]
+ fn state_roots_are_resolved_from_home_and_workspace() {
+ let args = CliArgs::parse_from(["radroots", "config", "show"]);
+ let env = MapEnvironment::new(BTreeMap::new());
+ let resolved = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
+ .expect("resolve runtime config");
+
+ assert_eq!(
+ resolved.paths.user_config_path,
+ PathBuf::from("/home/tester/.config/radroots/config.toml")
+ );
+ assert_eq!(
+ resolved.paths.workspace_config_path,
+ PathBuf::from("/workspaces/radroots-cli/.radroots/config.toml")
+ );
+ assert_eq!(
+ resolved.paths.user_state_root,
+ PathBuf::from("/home/tester/.local/share/radroots")
+ );
+ }
+
+ #[test]
fn unknown_env_file_variable_fails() {
let error = parse_env_file_values(
"RADROOTS_CLI_LOGGING_FILTRE=debug\n",
diff --git a/tests/identity_commands.rs b/tests/identity_commands.rs
@@ -27,7 +27,7 @@ fn cli_command_in(workdir: &Path) -> Command {
}
#[test]
-fn identity_init_json_creates_identity_file() {
+fn account_new_json_creates_identity_file() {
let dir = tempdir().expect("tempdir");
let identity_path = dir.path().join("identity.json");
@@ -36,11 +36,11 @@ fn identity_init_json_creates_identity_file() {
"--json",
"--identity-path",
identity_path.to_str().expect("identity path"),
- "identity",
- "init",
+ "account",
+ "new",
])
.output()
- .expect("run identity init");
+ .expect("run account new");
assert!(output.status.success());
assert!(identity_path.exists());
@@ -56,7 +56,7 @@ fn identity_init_json_creates_identity_file() {
}
#[test]
-fn identity_show_json_reads_existing_public_identity() {
+fn account_whoami_json_reads_existing_public_identity() {
let dir = tempdir().expect("tempdir");
let identity_path = dir.path().join("identity.json");
@@ -65,11 +65,11 @@ fn identity_show_json_reads_existing_public_identity() {
"--json",
"--identity-path",
identity_path.to_str().expect("identity path"),
- "identity",
- "init",
+ "account",
+ "new",
])
.output()
- .expect("run identity init");
+ .expect("run account new");
assert!(init.status.success());
let output = cli_command_in(dir.path())
@@ -77,11 +77,11 @@ fn identity_show_json_reads_existing_public_identity() {
"--json",
"--identity-path",
identity_path.to_str().expect("identity path"),
- "identity",
- "show",
+ "account",
+ "whoami",
])
.output()
- .expect("run identity show");
+ .expect("run account whoami");
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
@@ -95,7 +95,7 @@ fn identity_show_json_reads_existing_public_identity() {
}
#[test]
-fn identity_show_json_reports_unconfigured_without_creating_identity() {
+fn account_whoami_json_reports_unconfigured_without_creating_identity() {
let dir = tempdir().expect("tempdir");
let identity_path = dir.path().join("missing-identity.json");
@@ -104,11 +104,11 @@ fn identity_show_json_reports_unconfigured_without_creating_identity() {
"--json",
"--identity-path",
identity_path.to_str().expect("identity path"),
- "identity",
- "show",
+ "account",
+ "whoami",
])
.output()
- .expect("run identity show");
+ .expect("run account whoami");
assert_eq!(output.status.code(), Some(3));
assert!(!identity_path.exists());
diff --git a/tests/runtime_show.rs b/tests/runtime_show.rs
@@ -9,6 +9,7 @@ use tempfile::tempdir;
fn runtime_show_command_in(workdir: &Path) -> Command {
let mut command = Command::cargo_bin("radroots").expect("binary");
command.current_dir(workdir);
+ command.env("HOME", workdir.join("home"));
for key in [
"RADROOTS_ENV_FILE",
"RADROOTS_OUTPUT",
@@ -28,27 +29,51 @@ fn runtime_show_command_in(workdir: &Path) -> Command {
}
#[test]
-fn runtime_show_json_reports_default_bootstrap_state() {
+fn config_show_json_reports_default_bootstrap_state() {
let dir = tempdir().expect("tempdir");
+ let canonical_root = dir.path().canonicalize().expect("canonical tempdir");
let output = runtime_show_command_in(dir.path())
- .args(["--json", "runtime", "show"])
+ .args(["--json", "config", "show"])
.output()
- .expect("run runtime show");
+ .expect("run config show");
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
assert_eq!(json["output_format"], "json");
+ assert_eq!(
+ json["paths"]["user_config_path"],
+ dir.path()
+ .join("home")
+ .join(".config/radroots/config.toml")
+ .display()
+ .to_string()
+ );
+ assert_eq!(
+ json["paths"]["workspace_config_path"],
+ canonical_root
+ .join(".radroots/config.toml")
+ .display()
+ .to_string()
+ );
+ assert_eq!(
+ json["paths"]["user_state_root"],
+ dir.path()
+ .join("home")
+ .join(".local/share/radroots")
+ .display()
+ .to_string()
+ );
assert_eq!(json["logging"]["initialized"], true);
assert_eq!(json["logging"]["stdout"], false);
assert_eq!(json["logging"]["directory"], Value::Null);
- assert_eq!(json["identity"]["path"], "identity.json");
+ assert_eq!(json["account"]["identity_path"], "identity.json");
assert_eq!(json["signer"]["backend"], "local");
assert_eq!(json["myc"]["executable"], "myc");
}
#[test]
-fn runtime_show_json_reflects_environment_configuration() {
+fn config_show_json_reflects_environment_configuration() {
let dir = tempdir().expect("tempdir");
let output = runtime_show_command_in(dir.path())
.env("RADROOTS_OUTPUT", "json")
@@ -58,22 +83,22 @@ fn runtime_show_json_reflects_environment_configuration() {
.env("RADROOTS_IDENTITY_PATH", "state/identity.json")
.env("RADROOTS_SIGNER_BACKEND", "myc")
.env("RADROOTS_MYC_EXECUTABLE", "bin/myc")
- .args(["runtime", "show"])
+ .args(["config", "show"])
.output()
- .expect("run runtime show");
+ .expect("run config show");
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
assert_eq!(json["logging"]["filter"], "debug");
assert_eq!(json["logging"]["directory"], "logs/runtime");
- assert_eq!(json["identity"]["path"], "state/identity.json");
+ assert_eq!(json["account"]["identity_path"], "state/identity.json");
assert_eq!(json["signer"]["backend"], "myc");
assert_eq!(json["myc"]["executable"], "bin/myc");
}
#[test]
-fn runtime_show_json_reads_logging_from_default_env_file() {
+fn config_show_json_reads_logging_from_default_env_file() {
let temp = tempdir().expect("tempdir");
let env_path = temp.path().join(".env");
let logs_dir = temp.path().join("logs").join("radroots-cli");
@@ -87,9 +112,9 @@ fn runtime_show_json_reads_logging_from_default_env_file() {
.expect("write env file");
let output = runtime_show_command_in(temp.path())
- .args(["--json", "runtime", "show"])
+ .args(["--json", "config", "show"])
.output()
- .expect("run runtime show");
+ .expect("run config show");
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
diff --git a/tests/signer_status.rs b/tests/signer_status.rs
@@ -37,11 +37,11 @@ fn signer_status_reports_local_ready_when_identity_exists() {
"--json",
"--identity-path",
identity_path.to_str().expect("identity path"),
- "identity",
- "init",
+ "account",
+ "new",
])
.output()
- .expect("run identity init");
+ .expect("run account new");
assert!(init.status.success());
let output = cli_command_in(dir.path())