use crate::cli::CliOutput;
use crate::cli::helpers::id_short;
use crate::{db, validate};
use anyhow::Result;
use clap::{Args, Subcommand};
use std::path::Path;
#[derive(Args)]
pub struct ArchiveArgs {
#[command(subcommand)]
pub action: ArchiveAction,
}
#[derive(Subcommand)]
pub enum ArchiveAction {
List {
#[arg(long, short)]
namespace: Option<String>,
#[arg(long, default_value_t = 50)]
limit: usize,
#[arg(long, default_value_t = 0)]
offset: usize,
},
Restore { id: String },
Purge {
#[arg(long)]
older_than_days: Option<i64>,
},
Stats,
}
pub fn run(
db_path: &Path,
args: ArchiveArgs,
json_out: bool,
out: &mut CliOutput<'_>,
) -> Result<()> {
let conn = db::open(db_path)?;
match args.action {
ArchiveAction::List {
namespace,
limit,
offset,
} => {
let items = db::list_archived(&conn, namespace.as_deref(), limit, offset)?;
if json_out {
writeln!(
out.stdout,
"{}",
serde_json::json!({"archived": items, "count": items.len()})
)?;
} else if items.is_empty() {
writeln!(out.stdout, "no archived memories")?;
} else {
for item in &items {
writeln!(
out.stdout,
"[{}] {} (archived: {})",
id_short(item["id"].as_str().unwrap_or("")),
item["title"].as_str().unwrap_or(""),
item["archived_at"].as_str().unwrap_or("")
)?;
}
writeln!(out.stdout, "{} archived memories", items.len())?;
}
}
ArchiveAction::Restore { id } => {
validate::validate_id(&id)?;
let restored = db::restore_archived(&conn, &id)?;
if json_out {
writeln!(
out.stdout,
"{}",
serde_json::json!({"restored": restored, "id": id})
)?;
} else if restored {
writeln!(out.stdout, "restored: {}", id_short(&id))?;
} else {
writeln!(out.stderr, "not found in archive: {id}")?;
std::process::exit(1);
}
}
ArchiveAction::Purge { older_than_days } => {
let purged = db::purge_archive(&conn, older_than_days)?;
if json_out {
writeln!(out.stdout, "{}", serde_json::json!({"purged": purged}))?;
} else {
writeln!(out.stdout, "purged {purged} archived memories")?;
}
}
ArchiveAction::Stats => {
let stats = db::archive_stats(&conn)?;
if json_out {
writeln!(out.stdout, "{stats}")?;
} else {
writeln!(out.stdout, "archived: {} total", stats["archived_total"])?;
if let Some(by_ns) = stats["by_namespace"].as_array() {
for ns in by_ns {
writeln!(
out.stdout,
" {}: {}",
ns["namespace"].as_str().unwrap_or(""),
ns["count"]
)?;
}
}
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::test_utils::{TestEnv, seed_memory};
#[test]
fn test_archive_list_empty() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let args = ArchiveArgs {
action: ArchiveAction::List {
namespace: None,
limit: 50,
offset: 0,
},
};
{
let mut out = env.output();
run(&db, args, false, &mut out).unwrap();
}
assert!(env.stdout_str().contains("no archived memories"));
}
#[test]
fn test_archive_list_empty_json() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let args = ArchiveArgs {
action: ArchiveAction::List {
namespace: None,
limit: 50,
offset: 0,
},
};
{
let mut out = env.output();
run(&db, args, true, &mut out).unwrap();
}
let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
assert_eq!(v["count"].as_u64().unwrap(), 0);
assert!(v["archived"].is_array());
}
#[test]
fn test_archive_list_with_namespace_filter() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let args = ArchiveArgs {
action: ArchiveAction::List {
namespace: Some("nope".to_string()),
limit: 50,
offset: 0,
},
};
{
let mut out = env.output();
run(&db, args, false, &mut out).unwrap();
}
assert!(env.stdout_str().contains("no archived memories"));
}
#[test]
fn test_archive_restore_nonexistent_exits_via_stderr() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let id = seed_memory(&db, "ns", "t", "c");
let conn = db::open(&db).unwrap();
let _ = db::archive_memory(&conn, &id, None);
drop(conn);
let args = ArchiveArgs {
action: ArchiveAction::Restore { id: id.clone() },
};
{
let mut out = env.output();
run(&db, args, false, &mut out).unwrap();
}
assert!(env.stdout_str().contains("restored:"));
}
#[test]
fn test_archive_restore_json() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let id = seed_memory(&db, "ns", "t", "c");
let conn = db::open(&db).unwrap();
let _ = db::archive_memory(&conn, &id, None);
drop(conn);
let args = ArchiveArgs {
action: ArchiveAction::Restore { id: id.clone() },
};
{
let mut out = env.output();
run(&db, args, true, &mut out).unwrap();
}
let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
assert_eq!(v["restored"].as_bool().unwrap(), true);
}
#[test]
fn test_archive_purge_no_filter() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let args = ArchiveArgs {
action: ArchiveAction::Purge {
older_than_days: None,
},
};
{
let mut out = env.output();
run(&db, args, false, &mut out).unwrap();
}
assert!(env.stdout_str().contains("purged 0"));
}
#[test]
fn test_archive_purge_older_than_filter() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let args = ArchiveArgs {
action: ArchiveAction::Purge {
older_than_days: Some(30),
},
};
{
let mut out = env.output();
run(&db, args, true, &mut out).unwrap();
}
let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
assert_eq!(v["purged"].as_u64().unwrap(), 0);
}
#[test]
fn test_archive_stats() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let args = ArchiveArgs {
action: ArchiveAction::Stats,
};
{
let mut out = env.output();
run(&db, args, false, &mut out).unwrap();
}
assert!(env.stdout_str().contains("archived:"));
}
#[test]
fn test_archive_stats_json() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let args = ArchiveArgs {
action: ArchiveAction::Stats,
};
{
let mut out = env.output();
run(&db, args, true, &mut out).unwrap();
}
let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
assert!(v["archived_total"].is_number());
}
}