feat(cli/org): org edit — flag-driven field update for login/note/identity
This commit is contained in:
@@ -973,6 +973,77 @@ fn resolve_org_query<'a>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn run_edit(
|
||||||
|
dir: &Path,
|
||||||
|
query: &str,
|
||||||
|
title: Option<String>,
|
||||||
|
username: Option<String>,
|
||||||
|
url: Option<String>,
|
||||||
|
password: Option<String>,
|
||||||
|
body: Option<String>,
|
||||||
|
email: Option<String>,
|
||||||
|
phone: Option<String>,
|
||||||
|
full_name: Option<String>,
|
||||||
|
) -> Result<()> {
|
||||||
|
use relicario_core::time::now_unix;
|
||||||
|
use relicario_core::ItemCore;
|
||||||
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
|
let vault = crate::org_session::open_org_vault(Some(dir))?;
|
||||||
|
let caller = vault.current_member()?;
|
||||||
|
let manifest = vault.load_manifest()?;
|
||||||
|
let visible = manifest.filter_for_member(&caller);
|
||||||
|
let entry = resolve_org_query(&visible, query)?;
|
||||||
|
let collection = entry.collection.clone();
|
||||||
|
let id = entry.id.clone();
|
||||||
|
crate::org_session::UnlockedOrgVault::ensure_grant(&caller, &collection)?;
|
||||||
|
|
||||||
|
let mut item = vault.load_item(&collection, &id)?;
|
||||||
|
|
||||||
|
if let Some(t) = title { item.title = t; }
|
||||||
|
|
||||||
|
match &mut item.core {
|
||||||
|
ItemCore::Login(l) => {
|
||||||
|
if let Some(u) = username { l.username = Some(u); }
|
||||||
|
if let Some(u) = url {
|
||||||
|
l.url = Some(url::Url::parse(&u).with_context(|| format!("invalid URL: {u}"))?);
|
||||||
|
}
|
||||||
|
if let Some(p) = password { l.password = Some(Zeroizing::new(p)); }
|
||||||
|
}
|
||||||
|
ItemCore::SecureNote(n) => {
|
||||||
|
if let Some(b) = body { n.body = Zeroizing::new(b); }
|
||||||
|
}
|
||||||
|
ItemCore::Identity(i) => {
|
||||||
|
if let Some(v) = full_name { i.full_name = Some(v); }
|
||||||
|
if let Some(v) = email { i.email = Some(v); }
|
||||||
|
if let Some(v) = phone { i.phone = Some(v); }
|
||||||
|
}
|
||||||
|
_ => anyhow::bail!("org edit currently supports login, secure-note, and identity items"),
|
||||||
|
}
|
||||||
|
|
||||||
|
item.modified = now_unix();
|
||||||
|
let item_rel = vault.save_item(&collection, &item)?;
|
||||||
|
|
||||||
|
let mut manifest = vault.load_manifest()?;
|
||||||
|
upsert_org_entry(&mut manifest, &item, &collection);
|
||||||
|
vault.save_manifest(&manifest)?;
|
||||||
|
|
||||||
|
let subject = format!(
|
||||||
|
"org edit: {} ({})",
|
||||||
|
crate::helpers::sanitize_for_commit(&item.title),
|
||||||
|
item.id.as_str()
|
||||||
|
);
|
||||||
|
let commit_msg = format!(
|
||||||
|
"{subject}\n\nRelicario-Actor: {} {}\nRelicario-Action: item-update\nRelicario-Collection: {}\nRelicario-Item: {}",
|
||||||
|
caller.display_name, caller.member_id.as_str(), collection, item.id.as_str()
|
||||||
|
);
|
||||||
|
crate::org_session::org_git_run(&vault.root, &["add", &item_rel, "manifest.enc"], "org edit: git add")?;
|
||||||
|
crate::org_session::org_git_run(&vault.root, &["commit", "-m", &commit_msg], "org edit: git commit")?;
|
||||||
|
|
||||||
|
println!("Updated {}", item.id.as_str());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Insert-or-replace an `OrgManifestEntry` mirroring the personal-vault
|
/// Insert-or-replace an `OrgManifestEntry` mirroring the personal-vault
|
||||||
/// `Manifest::upsert`. Keyed by item id.
|
/// `Manifest::upsert`. Keyed by item id.
|
||||||
fn upsert_org_entry(
|
fn upsert_org_entry(
|
||||||
|
|||||||
@@ -522,8 +522,21 @@ pub(crate) enum OrgCommands {
|
|||||||
List {
|
List {
|
||||||
#[arg(long)] trashed: bool,
|
#[arg(long)] trashed: bool,
|
||||||
},
|
},
|
||||||
// Item subcommands (Edit/Rm/Restore/Purge) are added by
|
/// Edit an org item's fields (flag-driven; blank flags keep current values).
|
||||||
// Tasks B12–B13, which extend this enum.
|
Edit {
|
||||||
|
/// Item id or case-insensitive title substring.
|
||||||
|
query: String,
|
||||||
|
#[arg(long)] title: Option<String>,
|
||||||
|
#[arg(long)] username: Option<String>,
|
||||||
|
#[arg(long)] url: Option<String>,
|
||||||
|
#[arg(long)] password: Option<String>,
|
||||||
|
#[arg(long)] body: Option<String>,
|
||||||
|
#[arg(long)] email: Option<String>,
|
||||||
|
#[arg(long)] phone: Option<String>,
|
||||||
|
#[arg(long)] full_name: Option<String>,
|
||||||
|
},
|
||||||
|
// Item subcommands (Rm/Restore/Purge) are added by
|
||||||
|
// Task B13, which extends this enum.
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(clap::Subcommand)]
|
#[derive(clap::Subcommand)]
|
||||||
@@ -672,8 +685,12 @@ fn main() -> Result<()> {
|
|||||||
let d = crate::org_session::org_dir(dir_path)?;
|
let d = crate::org_session::org_dir(dir_path)?;
|
||||||
commands::org::run_list(&d, trashed)?;
|
commands::org::run_list(&d, trashed)?;
|
||||||
}
|
}
|
||||||
// Item dispatch arms (Edit/Rm/Restore/Purge) added by
|
OrgCommands::Edit { query, title, username, url, password, body, email, phone, full_name } => {
|
||||||
// Tasks B12–B13.
|
let d = crate::org_session::org_dir(dir_path)?;
|
||||||
|
commands::org::run_edit(&d, &query, title, username, url, password, body, email, phone, full_name)?;
|
||||||
|
}
|
||||||
|
// Item dispatch arms (Rm/Restore/Purge) added by
|
||||||
|
// Task B13.
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,3 +147,33 @@ fn org_add_rejects_unknown_collection() {
|
|||||||
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
|
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
|
||||||
assert!(stderr.contains("does not exist") || stderr.contains("ghost"), "unexpected error: {stderr}");
|
assert!(stderr.contains("does not exist") || stderr.contains("ghost"), "unexpected error: {stderr}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn org_edit_updates_fields_and_commits_update_trailer() {
|
||||||
|
let f = OrgFixture::new();
|
||||||
|
let owner = f.owner_member_id();
|
||||||
|
assert!(f.run(&["org", "create-collection", "prod", "--name", "Production"]).status.success());
|
||||||
|
assert!(f.run(&["org", "grant", &owner, "prod"]).status.success());
|
||||||
|
assert!(f.run(&[
|
||||||
|
"org", "add", "login", "--collection", "prod",
|
||||||
|
"--title", "Mail", "--username", "old", "--password", "pw",
|
||||||
|
]).status.success());
|
||||||
|
|
||||||
|
// Edit the username.
|
||||||
|
let out = f.run(&[
|
||||||
|
"org", "edit", "Mail", "--username", "new-user",
|
||||||
|
]);
|
||||||
|
assert!(out.status.success(), "org edit: {}", String::from_utf8_lossy(&out.stderr));
|
||||||
|
|
||||||
|
// get --show reflects the new username.
|
||||||
|
let out = f.run(&["org", "get", "Mail", "--show"]);
|
||||||
|
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
|
||||||
|
assert!(stdout.contains("new-user"), "edit did not take: {stdout}");
|
||||||
|
|
||||||
|
let log = Command::new("git")
|
||||||
|
.args(["-C", f.vault_str(), "log", "-1", "--format=%B"])
|
||||||
|
.output().unwrap();
|
||||||
|
let body = String::from_utf8_lossy(&log.stdout).to_string();
|
||||||
|
assert!(body.contains("Relicario-Action: item-update"), "missing update trailer: {body}");
|
||||||
|
assert!(body.contains("Relicario-Collection: prod"), "missing collection trailer: {body}");
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user