oxi/cli/commands/
reset.rs1use 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 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 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 !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 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 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 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 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 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 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
203pub 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
213pub 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
225pub 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
244pub fn dir_size_human(path: &Path) -> String {
246 bytes_human(dir_size_bytes(path))
247}
248
249pub 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
268pub 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}