Skip to main content

oxi/cli/commands/
reset.rs

1//! Reset subcommand handler and helper utilities.
2
3use anyhow::Result;
4use std::path::{Path, PathBuf};
5
6/// Target descriptor for the reset command.
7struct ResetTarget {
8    label: String,
9    path: PathBuf,
10    description: String,
11}
12
13/// Handle `oxi reset [--yes] [--include-project]`
14///
15/// Factory-reset: deletes ALL oxi data.
16/// Optionally also deletes the project-local `.oxi/` directory.
17pub fn handle_reset(yes: bool, include_project: bool) -> Result<()> {
18    use std::io::{self, Write};
19
20    // ── Collect targets ──────────────────────────────────────────
21    let home =
22        dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Cannot determine home directory"))?;
23
24    let oxi_dir = home.join(".oxi");
25    let config_oxi_dir = dirs::config_dir()
26        .unwrap_or_else(|| home.join(".config"))
27        .join("oxi");
28    let cache_oxi_dir = dirs::cache_dir()
29        .unwrap_or_else(|| home.join(".cache"))
30        .join("oxi");
31
32    let mut targets: Vec<ResetTarget> = vec![];
33
34    // ~/.oxi/ — split into sub-items for clarity
35    if oxi_dir.exists() {
36        let sub_items = [
37            ("settings.toml", "global settings"),
38            ("settings.json", "global settings (JSON)"),
39            ("auth.json", "credentials (API keys, OAuth tokens)"),
40            ("sessions", "session history"),
41            ("skills", "skills"),
42            ("extensions", "extensions"),
43            ("packages", "packages"),
44        ];
45        let mut has_sub = false;
46        for (name, desc) in &sub_items {
47            let p = oxi_dir.join(name);
48            if p.exists() {
49                has_sub = true;
50                targets.push(ResetTarget {
51                    label: format!("~/.oxi/{}", name),
52                    path: p,
53                    description: desc.to_string(),
54                });
55            }
56        }
57        // If no known sub-items found, target the whole directory
58        if !has_sub {
59            targets.push(ResetTarget {
60                label: "~/.oxi".to_string(),
61                path: oxi_dir.clone(),
62                description: "oxi home (settings, sessions, skills, extensions, packages)"
63                    .to_string(),
64            });
65        }
66    }
67
68    // ~/.config/oxi/ — MCP config, alternative auth location
69    if config_oxi_dir.exists() {
70        targets.push(ResetTarget {
71            label: display_path(&config_oxi_dir),
72            path: config_oxi_dir,
73            description: "MCP config, credentials".to_string(),
74        });
75    }
76
77    // ~/.cache/oxi/ — logs
78    if cache_oxi_dir.exists() {
79        targets.push(ResetTarget {
80            label: display_path(&cache_oxi_dir),
81            path: cache_oxi_dir,
82            description: "logs, cache".to_string(),
83        });
84    }
85
86    // Project-local .oxi/
87    let project_oxi = std::env::current_dir().unwrap_or_default().join(".oxi");
88    let mut project_target: Option<ResetTarget> = None;
89    if include_project && project_oxi.exists() {
90        project_target = Some(ResetTarget {
91            label: display_path(&project_oxi),
92            path: project_oxi.clone(),
93            description: "project settings".to_string(),
94        });
95    }
96
97    let total_count = targets.len() + usize::from(project_target.is_some());
98    if total_count == 0 {
99        println!("Nothing to reset — no oxi data found.");
100        return Ok(());
101    }
102
103    // ── Calculate total size ─────────────────────────────────────
104    let mut total_bytes: u64 = 0;
105    for t in &targets {
106        total_bytes += dir_size_bytes(&t.path);
107    }
108    if let Some(ref pt) = project_target {
109        total_bytes += dir_size_bytes(&pt.path);
110    }
111
112    // ── Show what will be deleted ────────────────────────────────
113    eprintln!();
114    eprintln!("     ⚠ Warning: The following will be permanently deleted:");
115    eprintln!();
116    for (i, t) in targets.iter().enumerate() {
117        eprintln!(
118            "       {}. {} ({})",
119            i + 1,
120            display_path(&t.path),
121            dir_size_human(&t.path)
122        );
123        eprintln!("          {}", t.description);
124    }
125    if let Some(ref pt) = project_target {
126        eprintln!(
127            "       {}. {} ({})",
128            total_count,
129            display_path(&pt.path),
130            dir_size_human(&pt.path)
131        );
132        eprintln!("          {}", pt.description);
133    }
134    eprintln!();
135    eprintln!(
136        "     Total: {} item(s), {}",
137        total_count,
138        bytes_human(total_bytes)
139    );
140    eprintln!();
141    eprintln!(
142        "     This cannot be undone. All sessions, skills, extensions, and settings will be deleted."
143    );
144    eprintln!();
145
146    if !yes {
147        eprint!("     Type RESET to continue: ");
148        io::stdout().flush()?;
149        let mut input = String::new();
150        io::stdin().read_line(&mut input)?;
151        if input.trim() != "RESET" {
152            eprintln!();
153            eprintln!();
154            eprintln!("     Cancelled.");
155            return Ok(());
156        }
157    }
158
159    // ── Delete ───────────────────────────────────────────────────
160    eprintln!();
161    let mut errors = Vec::new();
162
163    for t in &targets {
164        eprint!("     ● Deleting {}...", t.label);
165        io::stdout().flush()?;
166        match remove_path(&t.path) {
167            Ok(()) => eprintln!(" done"),
168            Err(e) => {
169                eprintln!(" failed");
170                eprintln!("       ✗ {}: {}", t.label, e);
171                errors.push(format!("{}: {}", t.label, e));
172            }
173        }
174    }
175    if let Some(ref pt) = project_target {
176        eprint!("     ● Deleting {}...", pt.label);
177        io::stdout().flush()?;
178        match remove_path(&pt.path) {
179            Ok(()) => eprintln!(" done"),
180            Err(e) => {
181                eprintln!(" failed");
182                eprintln!("       ✗ {}: {}", pt.label, e);
183                errors.push(format!("{}: {}", pt.label, e));
184            }
185        }
186    }
187
188    eprintln!();
189    if errors.is_empty() {
190        eprintln!("     ✓ All oxi data has been reset.");
191        eprintln!("     → Run 'oxi setup' to reconfigure.");
192    } else {
193        eprintln!("     ⚠ {} item(s) failed to delete:", errors.len());
194        for err in &errors {
195            eprintln!("       • {}", err);
196        }
197        eprintln!("     Some data may need manual cleanup.");
198    }
199
200    Ok(())
201}
202
203/// Remove a file or directory (including all contents).
204pub fn remove_path(path: &Path) -> Result<()> {
205    if path.is_dir() {
206        std::fs::remove_dir_all(path)?;
207    } else {
208        std::fs::remove_file(path)?;
209    }
210    Ok(())
211}
212
213/// Display path with ~/ abbreviation for home directory.
214pub fn display_path(path: &Path) -> String {
215    if let Some(home) = dirs::home_dir() {
216        let home_str = home.to_string_lossy();
217        let path_str = path.to_string_lossy();
218        if let Some(rest) = path_str.strip_prefix(home_str.as_ref()) {
219            return format!("~{}", rest);
220        }
221    }
222    path.display().to_string()
223}
224
225/// Calculate total bytes in a directory or file.
226pub fn dir_size_bytes(path: &Path) -> u64 {
227    let mut total: u64 = 0;
228    if path.is_dir() {
229        if let Ok(entries) = walkdir_recursive(path) {
230            for entry in entries {
231                if let Ok(meta) = std::fs::metadata(&entry)
232                    && meta.is_file()
233                {
234                    total += meta.len();
235                }
236            }
237        }
238    } else if let Ok(meta) = std::fs::metadata(path) {
239        total = meta.len();
240    }
241    total
242}
243
244/// Calculate a human-readable directory or file size.
245pub fn dir_size_human(path: &Path) -> String {
246    bytes_human(dir_size_bytes(path))
247}
248
249/// Format bytes as a human-readable string.
250pub fn bytes_human(bytes: u64) -> String {
251    if bytes == 0 {
252        return "0 B".to_string();
253    }
254    const KB: u64 = 1024;
255    const MB: u64 = 1024 * KB;
256    const GB: u64 = 1024 * MB;
257    if bytes >= GB {
258        format!("{:.1} GB", bytes as f64 / GB as f64)
259    } else if bytes >= MB {
260        format!("{:.1} MB", bytes as f64 / MB as f64)
261    } else if bytes >= KB {
262        format!("{:.1} KB", bytes as f64 / KB as f64)
263    } else {
264        format!("{} B", bytes)
265    }
266}
267
268/// Walk a directory recursively, collecting all file paths.
269pub fn walkdir_recursive(dir: &Path) -> Result<Vec<PathBuf>> {
270    let mut result = Vec::new();
271    if dir.is_dir() {
272        for entry in std::fs::read_dir(dir)? {
273            let entry = entry?;
274            let path = entry.path();
275            if path.is_dir() {
276                result.extend(walkdir_recursive(&path)?);
277            } else {
278                result.push(path);
279            }
280        }
281    }
282    Ok(result)
283}