diff --git a/crates/relicario-cli/src/commands/item_build.rs b/crates/relicario-cli/src/commands/item_build.rs new file mode 100644 index 0000000..4231a28 --- /dev/null +++ b/crates/relicario-cli/src/commands/item_build.rs @@ -0,0 +1,85 @@ +//! Shared per-type item construction + interactive editing for both the +//! personal vault (`commands/add.rs`, `commands/edit.rs`) and the org vault +//! (`commands/org.rs`). Centralizing it keeps the two surfaces from drifting. + +use std::collections::HashMap; +use std::path::PathBuf; + +use anyhow::{Context, Result}; +use zeroize::Zeroizing; + +use relicario_core::item::FieldHistoryEntry; +use relicario_core::item_types::{CardKind, TotpAlgorithm}; +use relicario_core::{EncryptedAttachment, FieldId, Item, ItemCore}; + +pub(crate) type FieldHistory = HashMap>; + +/// Resolve a single-line secret: from stdin when `from_stdin`, else an +/// interactive masked prompt (which honours `RELICARIO_TEST_ITEM_SECRET`). +pub(crate) fn resolve_secret_line(from_stdin: bool, label: &str) -> Result { + if from_stdin { + let mut s = String::new(); + std::io::stdin().read_line(&mut s)?; + Ok(s.trim_end_matches(['\n', '\r']).to_string()) + } else { + crate::prompt::prompt_secret(&format!("{label}: ")) + } +} + +/// Resolve a multiline secret (key material, note body). Both paths read stdin +/// to EOF; the interactive path first prints `hint` to stderr. +pub(crate) fn resolve_secret_multiline(from_stdin: bool, hint: &str) -> Result { + if !from_stdin { + eprintln!("{hint}"); + } + let mut s = String::new(); + std::io::Read::read_to_string(&mut std::io::stdin(), &mut s)?; + Ok(s) +} + +pub(crate) fn parse_card_kind(s: &str) -> Result { + Ok(match s { + "credit" => CardKind::Credit, + "debit" => CardKind::Debit, + "gift" => CardKind::Gift, + "loyalty" => CardKind::Loyalty, + "other" => CardKind::Other, + other => anyhow::bail!("unknown card kind: {other}"), + }) +} + +pub(crate) fn parse_totp_algorithm(s: &str) -> Result { + Ok(match s.to_ascii_lowercase().as_str() { + "sha1" => TotpAlgorithm::Sha1, + "sha256" => TotpAlgorithm::Sha256, + "sha512" => TotpAlgorithm::Sha512, + other => anyhow::bail!("unknown algorithm: {other}"), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use relicario_core::item_types::{CardKind, TotpAlgorithm}; + + #[test] + fn card_kind_parses_known_values() { + assert_eq!(parse_card_kind("credit").unwrap(), CardKind::Credit); + assert_eq!(parse_card_kind("loyalty").unwrap(), CardKind::Loyalty); + } + + #[test] + fn card_kind_rejects_unknown() { + assert!(parse_card_kind("platinum").is_err()); + } + + #[test] + fn totp_algorithm_is_case_insensitive() { + assert_eq!(parse_totp_algorithm("SHA256").unwrap(), TotpAlgorithm::Sha256); + } + + #[test] + fn totp_algorithm_rejects_unknown() { + assert!(parse_totp_algorithm("md5").is_err()); + } +} diff --git a/crates/relicario-cli/src/commands/mod.rs b/crates/relicario-cli/src/commands/mod.rs index 077e366..0c83da0 100644 --- a/crates/relicario-cli/src/commands/mod.rs +++ b/crates/relicario-cli/src/commands/mod.rs @@ -14,6 +14,7 @@ pub mod edit; pub mod generate; pub mod get; pub mod import; +pub mod item_build; pub mod org; pub mod init; pub mod list;