@@ -1,6 +1,7 @@
3 ffi::OsString,
3 ffi::OsString,
4 io::{self, IsTerminal, Write},
5 io::{self, IsTerminal, Write},
5 path::{Path, PathBuf},
6 path::{Path, PathBuf},
6 sync::{Arc, Mutex},
7 sync::{Arc, Mutex},
@@ -34,8 +35,10 @@ struct Cli {
34enum Command {
35enum Command {
35 /// List all sessions
36 /// List all sessions
37− /// Delete the current project's .vibebox directory
38+ /// Reset the current project's .vibebox directory
40+ /// Purge the global cache directory
39 /// Explain mounts and mappings
42 /// Explain mounts and mappings
@@ -147,7 +150,7 @@ fn handle_command(command: Command, cwd: &PathBuf, config_override: Option<&Path
147 tui::render_sessions_table(&rows)?;
150 tui::render_sessions_table(&rows)?;
150− Command::Clean => {
153+ Command::Reset => {
151 let instance_dir = cwd.join(session_manager::INSTANCE_DIR_NAME);
154 let instance_dir = cwd.join(session_manager::INSTANCE_DIR_NAME);
152 if !instance_dir.exists() {
155 if !instance_dir.exists() {
153 println!("No .vibebox directory found at {}", instance_dir.display());
156 println!("No .vibebox directory found at {}", instance_dir.display());
@@ -174,6 +177,34 @@ fn handle_command(command: Command, cwd: &PathBuf, config_override: Option<&Path
180+ Command::PurgeCache => {
181+ let cache_dir = cache_dir()?;
182+ if !cache_dir.exists() {
183+ println!("No cache directory found at {}", cache_dir.display());
186+ let (file_count, total_bytes) = measure_dir(&cache_dir)?;
187+ let confirmed = Confirm::new()
188+ .with_prompt(format!(
189+ "Delete cache directory {} and all its contents?",
195+ println!("Cancelled.");
198+ fs::remove_dir_all(&cache_dir)?;
200+ "Purged {} file{} totaling {} from {}",
202+ if file_count == 1 { "" } else { "s" },
203+ format_bytes(total_bytes),
177 Command::Explain => {
208 Command::Explain => {
178 let config = config::load_config_with_path(cwd, config_override);
209 let config = config::load_config_with_path(cwd, config_override);
179 let mounts = explain::build_mount_rows(cwd, &config)
210 let mounts = explain::build_mount_rows(cwd, &config)
@@ -212,6 +243,71 @@ fn relative_to_home(directory: &PathBuf) -> String {
212 directory.display().to_string()
243 directory.display().to_string()
246+fn cache_dir() -> Result<PathBuf> {
247+ let home = env::var("HOME").map(PathBuf::from)?;
248+ let cache_home = env::var("XDG_CACHE_HOME")
250+ .unwrap_or_else(|_| home.join(".cache"));
251+ Ok(cache_home.join(session_manager::GLOBAL_CACHE_DIR_NAME))
254+fn measure_dir(path: &Path) -> Result<(u64, u64)> {
255+ let mut total_bytes = 0u64;
256+ let mut file_count = 0u64;
257+ let mut stack = vec![path.to_path_buf()];
258+ while let Some(current) = stack.pop() {
259+ let entries = match fs::read_dir(¤t) {
260+ Ok(entries) => entries,
262+ tracing::warn!(path = %current.display(), error = %err, "failed to read directory");
266+ for entry in entries {
267+ let entry = match entry {
270+ tracing::warn!(path = %current.display(), error = %err, "failed to read directory entry");
274+ let path = entry.path();
275+ let metadata = match fs::symlink_metadata(&path) {
276+ Ok(metadata) => metadata,
278+ tracing::warn!(path = %path.display(), error = %err, "failed to stat path");
282+ let file_type = metadata.file_type();
283+ if file_type.is_dir() {
287+ total_bytes = total_bytes.saturating_add(metadata.len());
291+ Ok((file_count, total_bytes))
294+fn format_bytes(bytes: u64) -> String {
295+ const KB: f64 = 1024.0;
296+ const MB: f64 = KB * 1024.0;
297+ const GB: f64 = MB * 1024.0;
299+ let b = bytes as f64;
301+ format!("{:.2} GB", b / GB)
303+ format!("{:.1} MB", b / MB)
305+ format!("{:.1} KB", b / KB)
307+ format!("{} B", bytes)
215fn format_last_active(value: Option<&str>) -> String {
311fn format_last_active(value: Option<&str>) -> String {
216 let Some(raw) = value else {
312 let Some(raw) = value else {
217 return "-".to_string();
313 return "-".to_string();