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 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,7 @@ rand = "0.8"
|
|||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
zeroize = "1"
|
zeroize = "1"
|
||||||
|
url = "2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assert_cmd = "2"
|
assert_cmd = "2"
|
||||||
|
|||||||
@@ -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.");
|
eprintln!(" \u{2192} back this file up somewhere safe; it is your second factor.");
|
||||||
Ok(())
|
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<String> {
|
||||||
|
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<Option<String>> {
|
||||||
|
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_get(_query: String, _show: bool, _copy: bool) -> Result<()> { bail!("not yet implemented"); }
|
||||||
fn cmd_list(_t: Option<String>, _g: Option<String>, _tag: Option<String>, _trashed: bool) -> Result<()> { bail!("not yet implemented"); }
|
fn cmd_list(_t: Option<String>, _g: Option<String>, _tag: Option<String>, _trashed: bool) -> Result<()> { bail!("not yet implemented"); }
|
||||||
fn cmd_edit(_query: String) -> Result<()> { bail!("not yet implemented"); }
|
fn cmd_edit(_query: String) -> Result<()> { bail!("not yet implemented"); }
|
||||||
|
|||||||
Reference in New Issue
Block a user