diff --git a/docs/tasks.md b/docs/tasks.md index 3c47be0..d0cf2de 100644 --- a/docs/tasks.md +++ b/docs/tasks.md @@ -48,7 +48,7 @@ 3. [x] add an actual config file. 4. [x] set up the cli. 9. [x] fix ui overlap, and consistency issue. -10. [ ] `clean_cache` to cleanup the cache. +10. [x] `purge-cache` to clear the cache. 11. [ ] intensive integration test. ## Publish diff --git a/src/bin/vibebox-cli.rs b/src/bin/vibebox-cli.rs index d49d00f..70264a8 100644 --- a/src/bin/vibebox-cli.rs +++ b/src/bin/vibebox-cli.rs @@ -1,6 +1,7 @@ use std::{ env, ffi::OsString, + fs, io::{self, IsTerminal, Write}, path::{Path, PathBuf}, sync::{Arc, Mutex}, @@ -34,8 +35,10 @@ struct Cli { enum Command { /// List all sessions List, - /// Delete the current project's .vibebox directory - Clean, + /// Reset the current project's .vibebox directory + Reset, + /// Purge the global cache directory + PurgeCache, /// Explain mounts and mappings Explain, } @@ -147,7 +150,7 @@ fn handle_command(command: Command, cwd: &PathBuf, config_override: Option<&Path tui::render_sessions_table(&rows)?; Ok(()) } - Command::Clean => { + Command::Reset => { let instance_dir = cwd.join(session_manager::INSTANCE_DIR_NAME); if !instance_dir.exists() { println!("No .vibebox directory found at {}", instance_dir.display()); @@ -174,6 +177,34 @@ fn handle_command(command: Command, cwd: &PathBuf, config_override: Option<&Path ); Ok(()) } + Command::PurgeCache => { + let cache_dir = cache_dir()?; + if !cache_dir.exists() { + println!("No cache directory found at {}", cache_dir.display()); + return Ok(()); + } + let (file_count, total_bytes) = measure_dir(&cache_dir)?; + let confirmed = Confirm::new() + .with_prompt(format!( + "Delete cache directory {} and all its contents?", + cache_dir.display() + )) + .default(false) + .interact()?; + if !confirmed { + println!("Cancelled."); + return Ok(()); + } + fs::remove_dir_all(&cache_dir)?; + println!( + "Purged {} file{} totaling {} from {}", + file_count, + if file_count == 1 { "" } else { "s" }, + format_bytes(total_bytes), + cache_dir.display() + ); + Ok(()) + } Command::Explain => { let config = config::load_config_with_path(cwd, config_override); let mounts = explain::build_mount_rows(cwd, &config) @@ -212,6 +243,71 @@ fn relative_to_home(directory: &PathBuf) -> String { directory.display().to_string() } +fn cache_dir() -> Result { + let home = env::var("HOME").map(PathBuf::from)?; + let cache_home = env::var("XDG_CACHE_HOME") + .map(PathBuf::from) + .unwrap_or_else(|_| home.join(".cache")); + Ok(cache_home.join(session_manager::GLOBAL_CACHE_DIR_NAME)) +} + +fn measure_dir(path: &Path) -> Result<(u64, u64)> { + let mut total_bytes = 0u64; + let mut file_count = 0u64; + let mut stack = vec![path.to_path_buf()]; + while let Some(current) = stack.pop() { + let entries = match fs::read_dir(¤t) { + Ok(entries) => entries, + Err(err) => { + tracing::warn!(path = %current.display(), error = %err, "failed to read directory"); + continue; + } + }; + for entry in entries { + let entry = match entry { + Ok(entry) => entry, + Err(err) => { + tracing::warn!(path = %current.display(), error = %err, "failed to read directory entry"); + continue; + } + }; + let path = entry.path(); + let metadata = match fs::symlink_metadata(&path) { + Ok(metadata) => metadata, + Err(err) => { + tracing::warn!(path = %path.display(), error = %err, "failed to stat path"); + continue; + } + }; + let file_type = metadata.file_type(); + if file_type.is_dir() { + stack.push(path); + } else { + file_count += 1; + total_bytes = total_bytes.saturating_add(metadata.len()); + } + } + } + Ok((file_count, total_bytes)) +} + +fn format_bytes(bytes: u64) -> String { + const KB: f64 = 1024.0; + const MB: f64 = KB * 1024.0; + const GB: f64 = MB * 1024.0; + + let b = bytes as f64; + if b >= GB { + format!("{:.2} GB", b / GB) + } else if b >= MB { + format!("{:.1} MB", b / MB) + } else if b >= KB { + format!("{:.1} KB", b / KB) + } else { + format!("{} B", bytes) + } +} + fn format_last_active(value: Option<&str>) -> String { let Some(raw) = value else { return "-".to_string();