commit b07692e148c6e139a25ec5dd2f14b7459419b3ee
parent f97e6106c21f54f8fd6c8e4463eb4f37b5f02773
Author: triesap <tyson@radroots.org>
Date: Mon, 6 Apr 2026 23:24:34 +0000
cli: make identity show read-only
Diffstat:
5 files changed, 100 insertions(+), 18 deletions(-)
diff --git a/src/commands/identity.rs b/src/commands/identity.rs
@@ -1,7 +1,11 @@
-use crate::domain::runtime::{IdentityInitView, IdentityPublicView, IdentityShowView};
+use crate::domain::runtime::{
+ CommandDisposition, CommandOutput, CommandView, IdentityInitView, IdentityPublicView,
+ IdentityShowView,
+};
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> {
let identity = initialize_identity(&config.identity)?;
@@ -12,10 +16,35 @@ pub fn init(config: &RuntimeConfig) -> Result<IdentityInitView, RuntimeError> {
})
}
-pub fn show(config: &RuntimeConfig) -> Result<IdentityShowView, RuntimeError> {
- let identity = load_identity(&config.identity)?;
- Ok(IdentityShowView {
- path: identity.path.display().to_string(),
- public_identity: IdentityPublicView::from_public_identity(&identity.public_identity),
+pub fn show(config: &RuntimeConfig) -> Result<CommandOutput, RuntimeError> {
+ let view = match load_identity(&config.identity) {
+ Ok(identity) => IdentityShowView {
+ path: identity.path.display().to_string(),
+ state: "ready".to_owned(),
+ reason: None,
+ public_identity: Some(IdentityPublicView::from_public_identity(
+ &identity.public_identity,
+ )),
+ },
+ Err(RuntimeError::Identity(IdentityError::NotFound(path))) => IdentityShowView {
+ path: path.display().to_string(),
+ state: "unconfigured".to_owned(),
+ reason: Some(format!(
+ "local identity file was not found at {}",
+ path.display()
+ )),
+ public_identity: None,
+ },
+ Err(error) => return Err(error),
+ };
+
+ Ok(match view.disposition() {
+ CommandDisposition::Success => CommandOutput::success(CommandView::IdentityShow(view)),
+ CommandDisposition::Unconfigured => {
+ CommandOutput::unconfigured(CommandView::IdentityShow(view))
+ }
+ CommandDisposition::ExternalUnavailable => {
+ CommandOutput::external_unavailable(CommandView::IdentityShow(view))
+ }
})
}
diff --git a/src/commands/mod.rs b/src/commands/mod.rs
@@ -19,9 +19,7 @@ pub fn dispatch(
IdentityCommand::Init => Ok(CommandOutput::success(CommandView::IdentityInit(
identity::init(config)?,
))),
- IdentityCommand::Show => Ok(CommandOutput::success(CommandView::IdentityShow(
- identity::show(config)?,
- ))),
+ IdentityCommand::Show => identity::show(config),
},
Command::Myc(myc) => match myc.command {
MycCommand::Status => Ok(myc::status(config)),
diff --git a/src/domain/runtime.rs b/src/domain/runtime.rs
@@ -119,7 +119,20 @@ impl IdentityPublicView {
#[derive(Debug, Clone, Serialize)]
pub struct IdentityShowView {
pub path: String,
- pub public_identity: IdentityPublicView,
+ pub state: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub reason: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub public_identity: Option<IdentityPublicView>,
+}
+
+impl IdentityShowView {
+ pub fn disposition(&self) -> CommandDisposition {
+ match self.state.as_str() {
+ "unconfigured" => CommandDisposition::Unconfigured,
+ _ => CommandDisposition::Success,
+ }
+ }
}
#[derive(Debug, Clone, Serialize)]
diff --git a/src/render/mod.rs b/src/render/mod.rs
@@ -33,17 +33,25 @@ fn render_human(output: &CommandOutput) -> Result<(), RuntimeError> {
CommandView::IdentityShow(view) => {
writeln!(stdout, "identity")?;
writeln!(stdout, " path: {}", view.path)?;
- writeln!(stdout, " id: {}", view.public_identity.id)?;
- writeln!(
- stdout,
- " public key hex: {}",
- view.public_identity.public_key_hex
- )?;
+ writeln!(stdout, " state: {}", view.state)?;
writeln!(
stdout,
- " public key npub: {}",
- view.public_identity.public_key_npub
+ " reason: {}",
+ view.reason.as_deref().unwrap_or("<none>")
)?;
+ if let Some(public_identity) = &view.public_identity {
+ writeln!(stdout, " id: {}", public_identity.id)?;
+ writeln!(
+ stdout,
+ " public key hex: {}",
+ public_identity.public_key_hex
+ )?;
+ writeln!(
+ stdout,
+ " public key npub: {}",
+ public_identity.public_key_npub
+ )?;
+ }
}
CommandView::MycStatus(view) => {
render_myc_status(&mut stdout, view)?;
diff --git a/tests/identity_commands.rs b/tests/identity_commands.rs
@@ -68,8 +68,42 @@ fn identity_show_json_reads_existing_public_identity() {
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["path"], identity_path.display().to_string());
+ assert_eq!(json["state"], "ready");
assert!(json["public_identity"]["id"].is_string());
assert!(json["public_identity"]["public_key_hex"].is_string());
assert!(json["public_identity"]["public_key_npub"].is_string());
assert!(json.get("secret_key").is_none());
}
+
+#[test]
+fn identity_show_json_reports_unconfigured_without_creating_identity() {
+ let dir = tempdir().expect("tempdir");
+ let identity_path = dir.path().join("missing-identity.json");
+
+ let output = Command::cargo_bin("radroots")
+ .expect("binary")
+ .args([
+ "--json",
+ "--identity-path",
+ identity_path.to_str().expect("identity path"),
+ "--allow-generate-identity",
+ "identity",
+ "show",
+ ])
+ .output()
+ .expect("run identity show");
+
+ assert_eq!(output.status.code(), Some(3));
+ assert!(!identity_path.exists());
+
+ 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["path"], identity_path.display().to_string());
+ assert_eq!(json["state"], "unconfigured");
+ assert!(
+ json["reason"]
+ .as_str()
+ .is_some_and(|value| value.contains("local identity file was not found"))
+ );
+ assert_eq!(json.get("public_identity"), None);
+}