1use anyhow::Result;
4use std::path::{Path, PathBuf};
5
6struct ResetTarget {
8 label: String,
9 path: PathBuf,
10 description: String,
11}
12
13pub fn handle_reset(yes: bool, include_project: bool) -> Result<()> {
18 use std::io::{self, Write};
19
20 let oxicode_dir = oxicode_catalog::oxi_home::oxicode_home()
25 .ok_or_else(|| anyhow::anyhow!("Cannot determine oxicode home directory"))?;
26 let home =
27 dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Cannot determine home directory"))?;
28
29 let config_oxicode_dir = dirs::config_dir()
30 .unwrap_or_else(|| home.join(".config"))
31 .join("oxicode");
32 let cache_oxicode_dir = dirs::cache_dir()
33 .unwrap_or_else(|| home.join(".cache"))
34 .join("oxicode");
35
36 let project_oxicode = std::env::current_dir().unwrap_or_default().join(".oxicode");
37 let (targets, project_target) = collect_reset_targets(
38 &oxicode_dir,
39 oxicode_catalog::oxi_home::legacy_home_dir().as_deref(),
40 oxicode_catalog::oxi_home::migration_journal_path().as_deref(),
41 &config_oxicode_dir,
42 &cache_oxicode_dir,
43 &project_oxicode,
44 include_project,
45 );
46
47 let total_count = targets.len() + usize::from(project_target.is_some());
48 if total_count == 0 {
49 println!("Nothing to reset — no oxicode data found.");
50 return Ok(());
51 }
52
53 let mut total_bytes: u64 = 0;
55 for t in &targets {
56 total_bytes += dir_size_bytes(&t.path);
57 }
58 if let Some(ref pt) = project_target {
59 total_bytes += dir_size_bytes(&pt.path);
60 }
61
62 eprintln!();
64 eprintln!(" ⚠ Warning: The following will be permanently deleted:");
65 eprintln!();
66 for (i, t) in targets.iter().enumerate() {
67 eprintln!(
68 " {}. {} ({})",
69 i + 1,
70 display_path(&t.path),
71 dir_size_human(&t.path)
72 );
73 eprintln!(" {}", t.description);
74 }
75 if let Some(ref pt) = project_target {
76 eprintln!(
77 " {}. {} ({})",
78 total_count,
79 display_path(&pt.path),
80 dir_size_human(&pt.path)
81 );
82 eprintln!(" {}", pt.description);
83 }
84 eprintln!();
85 eprintln!(
86 " Total: {} item(s), {}",
87 total_count,
88 bytes_human(total_bytes)
89 );
90 eprintln!();
91 eprintln!(
92 " This cannot be undone. All sessions, skills, extensions, and settings will be deleted."
93 );
94 eprintln!();
95
96 if !yes {
97 eprint!(" Type RESET to continue: ");
98 io::stdout().flush()?;
99 let mut input = String::new();
100 io::stdin().read_line(&mut input)?;
101 if input.trim() != "RESET" {
102 eprintln!();
103 eprintln!();
104 eprintln!(" Cancelled.");
105 return Ok(());
106 }
107 }
108
109 eprintln!();
111 let mut errors = Vec::new();
112
113 for t in &targets {
114 eprint!(" ● Deleting {}...", t.label);
115 io::stdout().flush()?;
116 match remove_path(&t.path) {
117 Ok(()) => eprintln!(" done"),
118 Err(e) => {
119 eprintln!(" failed");
120 eprintln!(" ✗ {}: {}", t.label, e);
121 errors.push(format!("{}: {}", t.label, e));
122 }
123 }
124 }
125 if let Some(ref pt) = project_target {
126 eprint!(" ● Deleting {}...", pt.label);
127 io::stdout().flush()?;
128 match remove_path(&pt.path) {
129 Ok(()) => eprintln!(" done"),
130 Err(e) => {
131 eprintln!(" failed");
132 eprintln!(" ✗ {}: {}", pt.label, e);
133 errors.push(format!("{}: {}", pt.label, e));
134 }
135 }
136 }
137
138 eprintln!();
139 if errors.is_empty() {
140 eprintln!(" ✓ All oxicode data has been reset.");
141 eprintln!(" → Run 'oxicode setup' to reconfigure.");
142 } else {
143 eprintln!(" ⚠ {} item(s) failed to delete:", errors.len());
144 for err in &errors {
145 eprintln!(" • {}", err);
146 }
147 eprintln!(" Some data may need manual cleanup.");
148 }
149
150 Ok(())
151}
152
153fn collect_reset_targets(
164 oxicode_dir: &Path,
165 legacy_dir: Option<&Path>,
166 journal_path: Option<&Path>,
167 config_dir: &Path,
168 cache_dir: &Path,
169 project_dir: &Path,
170 include_project: bool,
171) -> (Vec<ResetTarget>, Option<ResetTarget>) {
172 let mut targets: Vec<ResetTarget> = vec![];
173
174 let home_dir_to_reset: Option<&Path> = if oxicode_dir.exists() {
177 Some(oxicode_dir)
178 } else {
179 legacy_dir
180 };
181 if let Some(reset_dir) = home_dir_to_reset {
182 let sub_items = [
183 ("settings.toml", "global settings"),
184 ("settings.json", "global settings (JSON)"),
185 ("auth.json", "credentials (API keys, OAuth tokens)"),
186 ("sessions", "session history"),
187 ("skills", "skills"),
188 ("extensions", "extensions"),
189 ("packages", "packages"),
190 ];
191 let mut has_sub = false;
192 for (name, desc) in &sub_items {
193 let p = reset_dir.join(name);
194 if p.exists() {
195 has_sub = true;
196 targets.push(ResetTarget {
197 label: display_path(&p),
198 path: p,
199 description: desc.to_string(),
200 });
201 }
202 }
203 if !has_sub {
205 targets.push(ResetTarget {
206 label: display_path(reset_dir),
207 path: reset_dir.to_path_buf(),
208 description: "oxicode home (settings, sessions, skills, extensions, packages)"
209 .to_string(),
210 });
211 }
212 }
213
214 if let Some(journal) = journal_path
216 && journal.exists()
217 {
218 targets.push(ResetTarget {
219 label: display_path(journal),
220 path: journal.to_path_buf(),
221 description: "home-layout migration journal".to_string(),
222 });
223 }
224
225 if config_dir.exists() {
227 targets.push(ResetTarget {
228 label: display_path(config_dir),
229 path: config_dir.to_path_buf(),
230 description: "MCP config, credentials".to_string(),
231 });
232 }
233
234 if cache_dir.exists() {
236 targets.push(ResetTarget {
237 label: display_path(cache_dir),
238 path: cache_dir.to_path_buf(),
239 description: "logs, cache".to_string(),
240 });
241 }
242
243 let mut project_target: Option<ResetTarget> = None;
244 if include_project && project_dir.exists() {
245 project_target = Some(ResetTarget {
246 label: display_path(project_dir),
247 path: project_dir.to_path_buf(),
248 description: "project settings".to_string(),
249 });
250 }
251
252 (targets, project_target)
253}
254
255pub fn remove_path(path: &Path) -> Result<()> {
257 if path.is_dir() {
258 std::fs::remove_dir_all(path)?;
259 } else {
260 std::fs::remove_file(path)?;
261 }
262 Ok(())
263}
264
265pub fn display_path(path: &Path) -> String {
267 if let Some(home) = dirs::home_dir() {
268 let home_str = home.to_string_lossy();
269 let path_str = path.to_string_lossy();
270 if let Some(rest) = path_str.strip_prefix(home_str.as_ref()) {
271 return format!("~{}", rest);
272 }
273 }
274 path.display().to_string()
275}
276
277pub fn dir_size_bytes(path: &Path) -> u64 {
279 let mut total: u64 = 0;
280 if path.is_dir() {
281 if let Ok(entries) = walkdir_recursive(path) {
282 for entry in entries {
283 if let Ok(meta) = std::fs::metadata(&entry)
284 && meta.is_file()
285 {
286 total += meta.len();
287 }
288 }
289 }
290 } else if let Ok(meta) = std::fs::metadata(path) {
291 total = meta.len();
292 }
293 total
294}
295
296pub fn dir_size_human(path: &Path) -> String {
298 bytes_human(dir_size_bytes(path))
299}
300
301pub fn bytes_human(bytes: u64) -> String {
303 if bytes == 0 {
304 return "0 B".to_string();
305 }
306 const KB: u64 = 1024;
307 const MB: u64 = 1024 * KB;
308 const GB: u64 = 1024 * MB;
309 if bytes >= GB {
310 format!("{:.1} GB", bytes as f64 / GB as f64)
311 } else if bytes >= MB {
312 format!("{:.1} MB", bytes as f64 / MB as f64)
313 } else if bytes >= KB {
314 format!("{:.1} KB", bytes as f64 / KB as f64)
315 } else {
316 format!("{} B", bytes)
317 }
318}
319
320pub fn walkdir_recursive(dir: &Path) -> Result<Vec<PathBuf>> {
322 let mut result = Vec::new();
323 if dir.is_dir() {
324 for entry in std::fs::read_dir(dir)? {
325 let entry = entry?;
326 let path = entry.path();
327 if path.is_dir() {
328 result.extend(walkdir_recursive(&path)?);
329 } else {
330 result.push(path);
331 }
332 }
333 }
334 Ok(result)
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340
341 #[test]
343 fn canonical_home_wins_over_legacy() {
344 let tmp = tempfile::tempdir().unwrap();
345 let canonical = tmp.path().join("canonical-home");
346 let legacy = tmp.path().join("legacy-home");
347 let config = tmp.path().join("config");
348 let cache = tmp.path().join("cache");
349 let project = tmp.path().join("project").join(".oxicode");
350 std::fs::create_dir_all(&canonical).unwrap();
351 std::fs::create_dir_all(&legacy).unwrap();
352 std::fs::write(canonical.join("auth.json"), "{}").unwrap();
353 std::fs::write(legacy.join("auth.json"), "{}").unwrap();
354
355 let (targets, _) = collect_reset_targets(
356 &canonical,
357 Some(&legacy),
358 None,
359 &config,
360 &cache,
361 &project,
362 false,
363 );
364
365 let paths: Vec<&Path> = targets.iter().map(|t| t.path.as_path()).collect();
366 assert!(paths.contains(&canonical.join("auth.json").as_path()));
367 assert!(
368 !paths.iter().any(|p| p.starts_with(&legacy)),
369 "legacy home must not be reset while the canonical home exists"
370 );
371 }
372
373 #[test]
375 fn legacy_home_targeted_when_canonical_absent() {
376 let tmp = tempfile::tempdir().unwrap();
377 let canonical = tmp.path().join("canonical-home");
378 let legacy = tmp.path().join("legacy-home");
379 let config = tmp.path().join("config");
380 let cache = tmp.path().join("cache");
381 let project = tmp.path().join("project").join(".oxicode");
382 std::fs::create_dir_all(&legacy).unwrap();
383 std::fs::write(legacy.join("auth.json"), "{}").unwrap();
384
385 let (targets, _) = collect_reset_targets(
386 &canonical,
387 Some(&legacy),
388 None,
389 &config,
390 &cache,
391 &project,
392 false,
393 );
394
395 let paths: Vec<&Path> = targets.iter().map(|t| t.path.as_path()).collect();
396 assert!(paths.contains(&legacy.join("auth.json").as_path()));
397 assert!(!paths.iter().any(|p| p.starts_with(&canonical)));
398 }
399
400 #[test]
402 fn journal_and_project_targeting() {
403 let tmp = tempfile::tempdir().unwrap();
404 let canonical = tmp.path().join("canonical-home");
405 let config = tmp.path().join("config");
406 let cache = tmp.path().join("cache");
407 let project = tmp.path().join("project").join(".oxicode");
408 std::fs::create_dir_all(&canonical).unwrap();
409 let journal = tmp.path().join("oxicode.migration-journal.json");
410 std::fs::write(&journal, "{}").unwrap();
411
412 let (targets, project_target) = collect_reset_targets(
413 &canonical,
414 None,
415 Some(&journal),
416 &config,
417 &cache,
418 &project,
419 false,
420 );
421 assert!(targets.iter().any(|t| t.path == journal));
422 assert!(project_target.is_none());
423
424 std::fs::create_dir_all(&project).unwrap();
425 let (_, project_target) = collect_reset_targets(
426 &canonical,
427 None,
428 Some(&journal),
429 &config,
430 &cache,
431 &project,
432 true,
433 );
434 assert!(project_target.is_some());
435 }
436}