feat(cli/org): org rm/restore/purge trash lifecycle (collection-scoped)

This commit is contained in:
adlee-was-taken
2026-06-20 14:39:18 -04:00
parent 057a7defe5
commit 6123d8b033
4 changed files with 350 additions and 4 deletions

View File

@@ -1044,6 +1044,94 @@ pub fn run_edit(
Ok(())
}
/// Resolve a query to (collection, item) with grant enforcement. Used by the
/// trash-lifecycle commands.
fn open_org_item(
vault: &crate::org_session::UnlockedOrgVault,
caller: &relicario_core::OrgMember,
query: &str,
) -> Result<(String, relicario_core::Item)> {
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 item = vault.load_item(&collection, &id)?;
Ok((collection, item))
}
pub fn run_rm(dir: &Path, query: &str) -> Result<()> {
let vault = crate::org_session::open_org_vault(Some(dir))?;
let caller = vault.current_member()?;
let (collection, mut item) = open_org_item(&vault, &caller, query)?;
item.soft_delete();
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 commit_msg = format!(
"org trash: {} ({})\n\nRelicario-Actor: {} {}\nRelicario-Action: item-delete\nRelicario-Collection: {}\nRelicario-Item: {}",
crate::helpers::sanitize_for_commit(&item.title), item.id.as_str(),
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 rm: git add")?;
crate::org_session::org_git_run(&vault.root, &["commit", "-m", &commit_msg], "org rm: git commit")?;
println!("Moved to trash: {}", item.title);
Ok(())
}
pub fn run_restore(dir: &Path, query: &str) -> Result<()> {
let vault = crate::org_session::open_org_vault(Some(dir))?;
let caller = vault.current_member()?;
let (collection, mut item) = open_org_item(&vault, &caller, query)?;
item.restore();
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 commit_msg = format!(
"org restore: {} ({})\n\nRelicario-Actor: {} {}\nRelicario-Action: item-restore\nRelicario-Collection: {}\nRelicario-Item: {}",
crate::helpers::sanitize_for_commit(&item.title), item.id.as_str(),
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 restore: git add")?;
crate::org_session::org_git_run(&vault.root, &["commit", "-m", &commit_msg], "org restore: git commit")?;
println!("Restored: {}", item.title);
Ok(())
}
pub fn run_purge(dir: &Path, query: &str) -> Result<()> {
let vault = crate::org_session::open_org_vault(Some(dir))?;
let caller = vault.current_member()?;
let (collection, item) = open_org_item(&vault, &caller, query)?;
let title = item.title.clone();
let id = item.id.clone();
// Remove the blob from disk, drop the manifest entry, stage with git rm.
vault.remove_item(&collection, &id)?;
let mut manifest = vault.load_manifest()?;
manifest.entries.retain(|e| e.id != id);
vault.save_manifest(&manifest)?;
let item_rel = format!("items/{}/{}.enc", collection, id.as_str());
crate::helpers::git_rm(&vault.root, &[item_rel], "org purge: git rm")?;
crate::org_session::org_git_run(&vault.root, &["add", "manifest.enc"], "org purge: git add manifest")?;
let commit_msg = format!(
"org purge: {} ({})\n\nRelicario-Actor: {} {}\nRelicario-Action: item-purge\nRelicario-Collection: {}\nRelicario-Item: {}",
crate::helpers::sanitize_for_commit(&title), id.as_str(),
caller.display_name, caller.member_id.as_str(), collection, id.as_str()
);
crate::org_session::org_git_run(&vault.root, &["commit", "-m", &commit_msg], "org purge: git commit")?;
println!("Purged: {title}");
Ok(())
}
/// Insert-or-replace an `OrgManifestEntry` mirroring the personal-vault
/// `Manifest::upsert`. Keyed by item id.
fn upsert_org_entry(

View File

@@ -535,8 +535,12 @@ pub(crate) enum OrgCommands {
#[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.
/// Soft-delete an org item (reversible via `org restore`).
Rm { query: String },
/// Restore a soft-deleted org item.
Restore { query: String },
/// Permanently purge an org item (deletes the encrypted blob).
Purge { query: String },
}
#[derive(clap::Subcommand)]
@@ -689,8 +693,18 @@ fn main() -> Result<()> {
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.
OrgCommands::Rm { query } => {
let d = crate::org_session::org_dir(dir_path)?;
commands::org::run_rm(&d, &query)?;
}
OrgCommands::Restore { query } => {
let d = crate::org_session::org_dir(dir_path)?;
commands::org::run_restore(&d, &query)?;
}
OrgCommands::Purge { query } => {
let d = crate::org_session::org_dir(dir_path)?;
commands::org::run_purge(&d, &query)?;
}
}
Ok(())
}