From cc279bac0b168751b9608a91ef699397c1c21aab Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sun, 19 Apr 2026 22:24:32 -0400 Subject: [PATCH] =?UTF-8?q?feat(cli):=20trash=20ops=20=E2=80=94=20rm=20/?= =?UTF-8?q?=20restore=20/=20purge=20/=20trash=20empty?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Soft-delete sets trashed_at via Item::soft_delete; restore clears it. Purge deletes item + attachment dir and removes manifest entry. Trash empty scans for items past settings.trash_retention. --- crates/relicario-cli/src/main.rs | 103 +++++++++++++++++++++++++++++-- 1 file changed, 99 insertions(+), 4 deletions(-) diff --git a/crates/relicario-cli/src/main.rs b/crates/relicario-cli/src/main.rs index ee70fa3..370c378 100644 --- a/crates/relicario-cli/src/main.rs +++ b/crates/relicario-cli/src/main.rs @@ -958,10 +958,105 @@ fn push_history( replaced_at: now_unix(), }); } -fn cmd_rm(_query: String) -> Result<()> { bail!("not yet implemented"); } -fn cmd_restore(_query: String) -> Result<()> { bail!("not yet implemented"); } -fn cmd_purge(_query: String) -> Result<()> { bail!("not yet implemented"); } -fn cmd_trash(_action: TrashAction) -> Result<()> { bail!("not yet implemented"); } +fn cmd_rm(query: String) -> Result<()> { + let vault = crate::session::UnlockedVault::unlock_interactive()?; + let mut manifest = vault.load_manifest()?; + let entry = resolve_query(&manifest, &query)?; + let id = entry.id.clone(); + let _ = entry; + let mut item = vault.load_item(&id)?; + item.soft_delete(); + vault.save_item(&item)?; + manifest.upsert(&item); + vault.save_manifest(&manifest)?; + commit_paths(&vault, &format!("trash: {} ({})", item.title, item.id.as_str()), + &[&format!("items/{}.enc", item.id.as_str()), "manifest.enc"])?; + eprintln!("Moved to trash: {}", item.title); + Ok(()) +} + +fn cmd_restore(query: String) -> Result<()> { + let vault = crate::session::UnlockedVault::unlock_interactive()?; + let mut manifest = vault.load_manifest()?; + let entry = resolve_query(&manifest, &query)?; + let id = entry.id.clone(); + let _ = entry; + let mut item = vault.load_item(&id)?; + item.restore(); + vault.save_item(&item)?; + manifest.upsert(&item); + vault.save_manifest(&manifest)?; + commit_paths(&vault, &format!("restore: {} ({})", item.title, item.id.as_str()), + &[&format!("items/{}.enc", item.id.as_str()), "manifest.enc"])?; + eprintln!("Restored: {}", item.title); + Ok(()) +} + +fn cmd_purge(query: String) -> Result<()> { + use std::fs; + + let vault = crate::session::UnlockedVault::unlock_interactive()?; + let mut manifest = vault.load_manifest()?; + let entry = resolve_query(&manifest, &query)?; + let id = entry.id.clone(); + let title = entry.title.clone(); + let _ = entry; + + // Remove the item file, its attachments directory, and drop the manifest entry. + let item_path = vault.item_path(&id); + if item_path.exists() { fs::remove_file(&item_path)?; } + let att_dir = vault.root().join("attachments").join(id.as_str()); + if att_dir.exists() { fs::remove_dir_all(&att_dir)?; } + manifest.remove(&id); + vault.save_manifest(&manifest)?; + + // `git rm -rf --ignore-unmatch` stages the deletions. Then add manifest and commit. + let _ = crate::helpers::git_command(vault.root(), &["rm", "-rf", "--ignore-unmatch", + &format!("items/{}.enc", id.as_str()), + &format!("attachments/{}", id.as_str()), + ]).status()?; + let status = crate::helpers::git_command(vault.root(), &["add", "manifest.enc"]).status()?; + if !status.success() { anyhow::bail!("git add manifest.enc failed"); } + let status = crate::helpers::git_command(vault.root(), + &["commit", "-m", &format!("purge: {} ({})", title, id.as_str())]).status()?; + if !status.success() { anyhow::bail!("git commit failed"); } + eprintln!("Purged: {title}"); + Ok(()) +} + +fn cmd_trash(action: TrashAction) -> Result<()> { + match action { + TrashAction::List => cmd_list(None, None, None, true), + TrashAction::Empty => cmd_trash_empty(), + } +} + +fn cmd_trash_empty() -> Result<()> { + use relicario_core::time::now_unix; + let vault = crate::session::UnlockedVault::unlock_interactive()?; + let manifest = vault.load_manifest()?; + let settings = vault.load_settings()?; + let now = now_unix(); + + let purgeable: Vec<_> = manifest.items.values() + .filter(|e| match e.trashed_at { + Some(t) => settings.trash_retention.should_purge(t, now), + None => false, + }) + .map(|e| (e.id.clone(), e.title.clone())) + .collect(); + + if purgeable.is_empty() { + eprintln!("nothing past retention window"); + return Ok(()); + } + + for (id, title) in purgeable { + cmd_purge(id.as_str().to_string())?; + eprintln!(" purged {title}"); + } + Ok(()) +} fn cmd_attach(_q: String, _file: PathBuf) -> Result<()> { bail!("not yet implemented"); } fn cmd_attachments(_q: String) -> Result<()> { bail!("not yet implemented"); } fn cmd_extract(_q: String, _aid: String, _out: Option) -> Result<()> { bail!("not yet implemented"); }