commit f97e6106c21f54f8fd6c8e4463eb4f37b5f02773
parent 35329b336ccff3ee4b7e31552cfdad7ee6c5c174
Author: triesap <tyson@radroots.org>
Date: Mon, 6 Apr 2026 23:19:44 +0000
cli: add typed command exit outcomes
Diffstat:
9 files changed, 132 insertions(+), 34 deletions(-)
diff --git a/src/commands/mod.rs b/src/commands/mod.rs
@@ -4,7 +4,7 @@ pub mod runtime;
pub mod signer;
use crate::cli::{Command, IdentityCommand, MycCommand, RuntimeCommand, SignerCommand};
-use crate::domain::CommandOutput;
+use crate::domain::runtime::{CommandOutput, CommandView};
use crate::runtime::RuntimeError;
use crate::runtime::config::RuntimeConfig;
use crate::runtime::logging::LoggingState;
@@ -16,17 +16,23 @@ pub fn dispatch(
) -> Result<CommandOutput, RuntimeError> {
match command {
Command::Identity(identity) => match identity.command {
- IdentityCommand::Init => Ok(CommandOutput::IdentityInit(identity::init(config)?)),
- IdentityCommand::Show => Ok(CommandOutput::IdentityShow(identity::show(config)?)),
+ IdentityCommand::Init => Ok(CommandOutput::success(CommandView::IdentityInit(
+ identity::init(config)?,
+ ))),
+ IdentityCommand::Show => Ok(CommandOutput::success(CommandView::IdentityShow(
+ identity::show(config)?,
+ ))),
},
Command::Myc(myc) => match myc.command {
- MycCommand::Status => Ok(CommandOutput::MycStatus(myc::status(config))),
+ MycCommand::Status => Ok(myc::status(config)),
},
Command::Runtime(runtime) => match runtime.command {
- RuntimeCommand::Show => Ok(CommandOutput::RuntimeShow(runtime::show(config, logging))),
+ RuntimeCommand::Show => Ok(CommandOutput::success(CommandView::RuntimeShow(
+ runtime::show(config, logging),
+ ))),
},
Command::Signer(signer) => match signer.command {
- SignerCommand::Status => Ok(CommandOutput::SignerStatus(signer::status(config))),
+ SignerCommand::Status => Ok(signer::status(config)),
},
}
}
diff --git a/src/commands/myc.rs b/src/commands/myc.rs
@@ -1,6 +1,15 @@
-use crate::domain::runtime::MycStatusView;
+use crate::domain::runtime::{CommandDisposition, CommandOutput, CommandView, MycStatusView};
use crate::runtime::config::RuntimeConfig;
-pub fn status(config: &RuntimeConfig) -> MycStatusView {
- crate::runtime::myc::resolve_status(&config.myc)
+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))
+ }
+ }
}
diff --git a/src/commands/signer.rs b/src/commands/signer.rs
@@ -1,7 +1,16 @@
-use crate::domain::runtime::SignerStatusView;
+use crate::domain::runtime::{CommandDisposition, CommandOutput, CommandView, SignerStatusView};
use crate::runtime::config::RuntimeConfig;
use crate::runtime::signer::resolve_signer_status;
-pub fn status(config: &RuntimeConfig) -> SignerStatusView {
- resolve_signer_status(config)
+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))
+ }
+ }
}
diff --git a/src/domain/mod.rs b/src/domain/mod.rs
@@ -1,3 +1 @@
pub mod runtime;
-
-pub use runtime::CommandOutput;
diff --git a/src/domain/runtime.rs b/src/domain/runtime.rs
@@ -1,7 +1,63 @@
+use std::process::ExitCode;
+
use serde::Serialize;
#[derive(Debug, Clone)]
-pub enum CommandOutput {
+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 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,
+ Unconfigured,
+ ExternalUnavailable,
+}
+
+impl CommandDisposition {
+ pub fn exit_code(self) -> ExitCode {
+ match self {
+ Self::Success => ExitCode::SUCCESS,
+ Self::Unconfigured => ExitCode::from(3),
+ Self::ExternalUnavailable => ExitCode::from(4),
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+pub enum CommandView {
IdentityInit(IdentityInitView),
IdentityShow(IdentityShowView),
MycStatus(MycStatusView),
@@ -82,6 +138,16 @@ pub struct SignerStatusView {
pub myc: Option<MycStatusView>,
}
+impl SignerStatusView {
+ pub fn disposition(&self) -> CommandDisposition {
+ match self.state.as_str() {
+ "unconfigured" => CommandDisposition::Unconfigured,
+ "unavailable" => CommandDisposition::ExternalUnavailable,
+ _ => CommandDisposition::Success,
+ }
+ }
+}
+
#[derive(Debug, Clone, Serialize)]
pub struct LocalSignerStatusView {
pub account_id: String,
@@ -105,6 +171,16 @@ pub struct MycStatusView {
pub custody: Option<MycCustodyView>,
}
+impl MycStatusView {
+ pub fn disposition(&self) -> CommandDisposition {
+ match self.state.as_str() {
+ "unconfigured" => CommandDisposition::Unconfigured,
+ "unavailable" => CommandDisposition::ExternalUnavailable,
+ _ => CommandDisposition::Success,
+ }
+ }
+}
+
#[derive(Debug, Clone, Serialize)]
pub struct MycCustodyView {
pub signer: MycCustodyIdentityView,
diff --git a/src/main.rs b/src/main.rs
@@ -18,7 +18,7 @@ use crate::runtime::logging::initialize_logging;
fn main() -> ExitCode {
match run() {
- Ok(()) => ExitCode::SUCCESS,
+ Ok(exit_code) => exit_code,
Err(error) => {
let _ = writeln!(std::io::stderr(), "{error}");
error.exit_code()
@@ -26,11 +26,11 @@ fn main() -> ExitCode {
}
}
-fn run() -> Result<(), runtime::RuntimeError> {
+fn run() -> Result<ExitCode, runtime::RuntimeError> {
let args = CliArgs::parse();
let config = RuntimeConfig::from_system(&args)?;
let logging = initialize_logging(&config.logging)?;
let output = dispatch(&args.command, &config, &logging)?;
render_output(&output, config.output_format)?;
- Ok(())
+ Ok(output.exit_code())
}
diff --git a/src/render/mod.rs b/src/render/mod.rs
@@ -1,6 +1,6 @@
use std::io::{self, Write};
-use crate::domain::runtime::CommandOutput;
+use crate::domain::runtime::{CommandOutput, CommandView};
use crate::runtime::RuntimeError;
use crate::runtime::config::OutputFormat;
@@ -13,8 +13,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 {
- CommandOutput::IdentityInit(view) => {
+ match output.view() {
+ CommandView::IdentityInit(view) => {
writeln!(stdout, "identity init")?;
writeln!(stdout, " path: {}", view.path)?;
writeln!(stdout, " created: {}", yes_no(view.created))?;
@@ -30,7 +30,7 @@ fn render_human(output: &CommandOutput) -> Result<(), RuntimeError> {
view.public_identity.public_key_npub
)?;
}
- CommandOutput::IdentityShow(view) => {
+ CommandView::IdentityShow(view) => {
writeln!(stdout, "identity")?;
writeln!(stdout, " path: {}", view.path)?;
writeln!(stdout, " id: {}", view.public_identity.id)?;
@@ -45,10 +45,10 @@ fn render_human(output: &CommandOutput) -> Result<(), RuntimeError> {
view.public_identity.public_key_npub
)?;
}
- CommandOutput::MycStatus(view) => {
+ CommandView::MycStatus(view) => {
render_myc_status(&mut stdout, view)?;
}
- CommandOutput::RuntimeShow(view) => {
+ CommandView::RuntimeShow(view) => {
writeln!(stdout, "runtime")?;
writeln!(stdout, " output format: {}", view.output_format)?;
writeln!(stdout, "logging")?;
@@ -81,7 +81,7 @@ fn render_human(output: &CommandOutput) -> Result<(), RuntimeError> {
writeln!(stdout, "myc")?;
writeln!(stdout, " executable: {}", view.myc.executable)?;
}
- CommandOutput::SignerStatus(view) => {
+ CommandView::SignerStatus(view) => {
writeln!(stdout, "signer")?;
writeln!(stdout, " backend: {}", view.backend)?;
writeln!(stdout, " state: {}", view.state)?;
@@ -103,24 +103,24 @@ fn render_human(output: &CommandOutput) -> Result<(), RuntimeError> {
fn render_json(output: &CommandOutput) -> Result<(), RuntimeError> {
let mut stdout = io::stdout().lock();
- match output {
- CommandOutput::IdentityInit(view) => {
+ match output.view() {
+ CommandView::IdentityInit(view) => {
serde_json::to_writer_pretty(&mut stdout, view)?;
writeln!(stdout)?;
}
- CommandOutput::IdentityShow(view) => {
+ CommandView::IdentityShow(view) => {
serde_json::to_writer_pretty(&mut stdout, view)?;
writeln!(stdout)?;
}
- CommandOutput::MycStatus(view) => {
+ CommandView::MycStatus(view) => {
serde_json::to_writer_pretty(&mut stdout, view)?;
writeln!(stdout)?;
}
- CommandOutput::RuntimeShow(view) => {
+ CommandView::RuntimeShow(view) => {
serde_json::to_writer_pretty(&mut stdout, view)?;
writeln!(stdout)?;
}
- CommandOutput::SignerStatus(view) => {
+ CommandView::SignerStatus(view) => {
serde_json::to_writer_pretty(&mut stdout, view)?;
writeln!(stdout)?;
}
diff --git a/tests/myc_status.rs b/tests/myc_status.rs
@@ -58,7 +58,7 @@ fn myc_status_reports_unavailable_for_invalid_status_payload() {
.output()
.expect("run myc status");
- assert!(output.status.success());
+ assert_eq!(output.status.code(), Some(4));
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["state"], "unavailable");
@@ -123,7 +123,7 @@ fn myc_status_reports_unavailable_when_executable_is_missing() {
.output()
.expect("run myc status");
- assert!(output.status.success());
+ assert_eq!(output.status.code(), Some(4));
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["state"], "unavailable");
diff --git a/tests/signer_status.rs b/tests/signer_status.rs
@@ -65,7 +65,7 @@ fn signer_status_reports_local_unconfigured_when_identity_is_missing() {
.output()
.expect("run signer status");
- assert!(output.status.success());
+ assert_eq!(output.status.code(), Some(3));
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["backend"], "local");