From 89b22cb089fd7c4bbcb3371ab60a9e5105e59d01 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sun, 19 Apr 2026 22:13:17 -0400 Subject: [PATCH] feat(cli): relicario add login with flag + interactive prompting Unlocks vault, builds LoginCore from flags (password via rpassword if --password-prompt), saves item + manifest, commits via hardened git. Co-Authored-By: Claude Sonnet 4.6 --- crates/relicario-cli/Cargo.toml | 1 + crates/relicario-cli/src/main.rs | 82 +++++++++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/crates/relicario-cli/Cargo.toml b/crates/relicario-cli/Cargo.toml index bd87584..5f6a773 100644 --- a/crates/relicario-cli/Cargo.toml +++ b/crates/relicario-cli/Cargo.toml @@ -22,6 +22,7 @@ rand = "0.8" serde = { version = "1", features = ["derive"] } serde_json = "1" zeroize = "1" +url = "2" [dev-dependencies] assert_cmd = "2" diff --git a/crates/relicario-cli/src/main.rs b/crates/relicario-cli/src/main.rs index ce187b9..1662f02 100644 --- a/crates/relicario-cli/src/main.rs +++ b/crates/relicario-cli/src/main.rs @@ -350,7 +350,87 @@ fn cmd_init(image: PathBuf, output: PathBuf) -> Result<()> { eprintln!(" \u{2192} back this file up somewhere safe; it is your second factor."); Ok(()) } -fn cmd_add(_kind: AddKind) -> Result<()> { bail!("not yet implemented"); } +fn cmd_add(kind: AddKind) -> Result<()> { + let vault = crate::session::UnlockedVault::unlock_interactive()?; + let mut manifest = vault.load_manifest()?; + + let item = match kind { + AddKind::Login { title, username, url, password_prompt, password, group, tags, favorite } => { + use relicario_core::item_types::LoginCore; + use relicario_core::{Item, ItemCore}; + use zeroize::Zeroizing; + + let title = title.map(Ok).unwrap_or_else(|| prompt("Title"))?; + let username = username.or_else(|| prompt_optional("Username").ok().flatten()); + let url = url.or_else(|| prompt_optional("URL").ok().flatten()); + let parsed_url = match url { + Some(s) => Some(url::Url::parse(&s) + .with_context(|| format!("invalid URL: {s}"))?), + None => None, + }; + let password = if let Some(p) = password { + Some(Zeroizing::new(p)) + } else if password_prompt { + Some(Zeroizing::new(rpassword::prompt_password("Password: ")?)) + } else { + None + }; + let core = ItemCore::Login(LoginCore { + username, + password, + url: parsed_url, + totp: None, + }); + let mut item = Item::new(title, core); + item.group = group; + item.tags = tags; + item.favorite = favorite; + item + } + // Task 8 fills in the other variants. + _ => anyhow::bail!("item kind not yet implemented"), + }; + + vault.save_item(&item)?; + manifest.upsert(&item); + vault.save_manifest(&manifest)?; + + commit_paths(&vault, &format!("add: {} ({})", item.title, item.id.as_str()), + &[&format!("items/{}.enc", item.id.as_str()), "manifest.enc"])?; + + eprintln!("Added: {} (id={})", item.title, item.id.as_str()); + Ok(()) +} + +fn prompt(label: &str) -> Result { + eprint!("{label}: "); + std::io::Write::flush(&mut std::io::stderr())?; + let mut s = String::new(); + std::io::stdin().read_line(&mut s)?; + let trimmed = s.trim().to_string(); + if trimmed.is_empty() { anyhow::bail!("{label} required"); } + Ok(trimmed) +} + +fn prompt_optional(label: &str) -> Result> { + eprint!("{label} (leave blank to skip): "); + std::io::Write::flush(&mut std::io::stderr())?; + let mut s = String::new(); + std::io::stdin().read_line(&mut s)?; + let trimmed = s.trim().to_string(); + Ok(if trimmed.is_empty() { None } else { Some(trimmed) }) +} + +fn commit_paths(vault: &crate::session::UnlockedVault, message: &str, paths: &[&str]) -> Result<()> { + let mut args: Vec<&str> = vec!["add"]; + args.extend_from_slice(paths); + let status = crate::helpers::git_command(vault.root(), &args).status()?; + if !status.success() { anyhow::bail!("git add failed"); } + let status = crate::helpers::git_command(vault.root(), &["commit", "-m", message]).status()?; + if !status.success() { anyhow::bail!("git commit failed"); } + Ok(()) +} + fn cmd_get(_query: String, _show: bool, _copy: bool) -> Result<()> { bail!("not yet implemented"); } fn cmd_list(_t: Option, _g: Option, _tag: Option, _trashed: bool) -> Result<()> { bail!("not yet implemented"); } fn cmd_edit(_query: String) -> Result<()> { bail!("not yet implemented"); }