1use crate::cli::CliOutput;
7use crate::cli::helpers::id_short;
8use crate::{db, validate};
9use anyhow::Result;
10use clap::{Args, Subcommand};
11use std::path::Path;
12
13#[derive(Args)]
14pub struct ArchiveArgs {
15 #[command(subcommand)]
16 pub action: ArchiveAction,
17}
18
19#[derive(Subcommand)]
20pub enum ArchiveAction {
21 List {
23 #[arg(long, short)]
24 namespace: Option<String>,
25 #[arg(long, default_value_t = 50)]
26 limit: usize,
27 #[arg(long, default_value_t = 0)]
28 offset: usize,
29 },
30 Restore { id: String },
32 Purge {
34 #[arg(long)]
36 older_than_days: Option<i64>,
37 },
38 Stats,
40}
41
42pub fn run(
44 db_path: &Path,
45 args: ArchiveArgs,
46 json_out: bool,
47 out: &mut CliOutput<'_>,
48) -> Result<()> {
49 let conn = db::open(db_path)?;
50 match args.action {
51 ArchiveAction::List {
52 namespace,
53 limit,
54 offset,
55 } => {
56 let items = db::list_archived(&conn, namespace.as_deref(), limit, offset)?;
57 if json_out {
58 writeln!(
59 out.stdout,
60 "{}",
61 serde_json::json!({"archived": items, "count": items.len()})
62 )?;
63 } else if items.is_empty() {
64 writeln!(out.stdout, "no archived memories")?;
65 } else {
66 for item in &items {
67 writeln!(
68 out.stdout,
69 "[{}] {} (archived: {})",
70 id_short(item["id"].as_str().unwrap_or("")),
71 item["title"].as_str().unwrap_or(""),
72 item["archived_at"].as_str().unwrap_or("")
73 )?;
74 }
75 writeln!(out.stdout, "{} archived memories", items.len())?;
76 }
77 }
78 ArchiveAction::Restore { id } => {
79 validate::validate_id(&id)?;
80 let restored = db::restore_archived(&conn, &id)?;
81 if json_out {
82 writeln!(
83 out.stdout,
84 "{}",
85 serde_json::json!({"restored": restored, "id": id})
86 )?;
87 } else if restored {
88 writeln!(out.stdout, "restored: {}", id_short(&id))?;
89 } else {
90 writeln!(out.stderr, "not found in archive: {id}")?;
91 std::process::exit(1);
92 }
93 }
94 ArchiveAction::Purge { older_than_days } => {
95 let purged = db::purge_archive(&conn, older_than_days)?;
96 if json_out {
97 writeln!(out.stdout, "{}", serde_json::json!({"purged": purged}))?;
98 } else {
99 writeln!(out.stdout, "purged {purged} archived memories")?;
100 }
101 }
102 ArchiveAction::Stats => {
103 let stats = db::archive_stats(&conn)?;
104 if json_out {
105 writeln!(out.stdout, "{stats}")?;
106 } else {
107 writeln!(out.stdout, "archived: {} total", stats["archived_total"])?;
108 if let Some(by_ns) = stats["by_namespace"].as_array() {
109 for ns in by_ns {
110 writeln!(
111 out.stdout,
112 " {}: {}",
113 ns["namespace"].as_str().unwrap_or(""),
114 ns["count"]
115 )?;
116 }
117 }
118 }
119 }
120 }
121 Ok(())
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use crate::cli::test_utils::{TestEnv, seed_memory};
128
129 #[test]
130 fn test_archive_list_empty() {
131 let mut env = TestEnv::fresh();
132 let db = env.db_path.clone();
133 let args = ArchiveArgs {
134 action: ArchiveAction::List {
135 namespace: None,
136 limit: 50,
137 offset: 0,
138 },
139 };
140 {
141 let mut out = env.output();
142 run(&db, args, false, &mut out).unwrap();
143 }
144 assert!(env.stdout_str().contains("no archived memories"));
145 }
146
147 #[test]
148 fn test_archive_list_empty_json() {
149 let mut env = TestEnv::fresh();
150 let db = env.db_path.clone();
151 let args = ArchiveArgs {
152 action: ArchiveAction::List {
153 namespace: None,
154 limit: 50,
155 offset: 0,
156 },
157 };
158 {
159 let mut out = env.output();
160 run(&db, args, true, &mut out).unwrap();
161 }
162 let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
163 assert_eq!(v["count"].as_u64().unwrap(), 0);
164 assert!(v["archived"].is_array());
165 }
166
167 #[test]
168 fn test_archive_list_with_namespace_filter() {
169 let mut env = TestEnv::fresh();
170 let db = env.db_path.clone();
171 let args = ArchiveArgs {
172 action: ArchiveAction::List {
173 namespace: Some("nope".to_string()),
174 limit: 50,
175 offset: 0,
176 },
177 };
178 {
179 let mut out = env.output();
180 run(&db, args, false, &mut out).unwrap();
181 }
182 assert!(env.stdout_str().contains("no archived memories"));
184 }
185
186 #[test]
187 fn test_archive_restore_nonexistent_exits_via_stderr() {
188 let mut env = TestEnv::fresh();
192 let db = env.db_path.clone();
193 let id = seed_memory(&db, "ns", "t", "c");
195 let conn = db::open(&db).unwrap();
196 let _ = db::archive_memory(&conn, &id, None);
197 drop(conn);
198 let args = ArchiveArgs {
199 action: ArchiveAction::Restore { id: id.clone() },
200 };
201 {
202 let mut out = env.output();
203 run(&db, args, false, &mut out).unwrap();
204 }
205 assert!(env.stdout_str().contains("restored:"));
206 }
207
208 #[test]
209 fn test_archive_restore_json() {
210 let mut env = TestEnv::fresh();
211 let db = env.db_path.clone();
212 let id = seed_memory(&db, "ns", "t", "c");
213 let conn = db::open(&db).unwrap();
214 let _ = db::archive_memory(&conn, &id, None);
215 drop(conn);
216 let args = ArchiveArgs {
217 action: ArchiveAction::Restore { id: id.clone() },
218 };
219 {
220 let mut out = env.output();
221 run(&db, args, true, &mut out).unwrap();
222 }
223 let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
224 assert_eq!(v["restored"].as_bool().unwrap(), true);
225 }
226
227 #[test]
228 fn test_archive_purge_no_filter() {
229 let mut env = TestEnv::fresh();
230 let db = env.db_path.clone();
231 let args = ArchiveArgs {
232 action: ArchiveAction::Purge {
233 older_than_days: None,
234 },
235 };
236 {
237 let mut out = env.output();
238 run(&db, args, false, &mut out).unwrap();
239 }
240 assert!(env.stdout_str().contains("purged 0"));
241 }
242
243 #[test]
244 fn test_archive_purge_older_than_filter() {
245 let mut env = TestEnv::fresh();
246 let db = env.db_path.clone();
247 let args = ArchiveArgs {
248 action: ArchiveAction::Purge {
249 older_than_days: Some(30),
250 },
251 };
252 {
253 let mut out = env.output();
254 run(&db, args, true, &mut out).unwrap();
255 }
256 let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
257 assert_eq!(v["purged"].as_u64().unwrap(), 0);
258 }
259
260 #[test]
261 fn test_archive_stats() {
262 let mut env = TestEnv::fresh();
263 let db = env.db_path.clone();
264 let args = ArchiveArgs {
265 action: ArchiveAction::Stats,
266 };
267 {
268 let mut out = env.output();
269 run(&db, args, false, &mut out).unwrap();
270 }
271 assert!(env.stdout_str().contains("archived:"));
272 }
273
274 #[test]
275 fn test_archive_stats_json() {
276 let mut env = TestEnv::fresh();
277 let db = env.db_path.clone();
278 let args = ArchiveArgs {
279 action: ArchiveAction::Stats,
280 };
281 {
282 let mut out = env.output();
283 run(&db, args, true, &mut out).unwrap();
284 }
285 let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
287 assert!(v["archived_total"].is_number());
288 }
289}