diff --git a/doc/runtime-completions.md b/doc/runtime-completions.md index 56585e4..0bee386 100644 --- a/doc/runtime-completions.md +++ b/doc/runtime-completions.md @@ -92,8 +92,8 @@ inshellah dump # view stored data for a command inshellah query docker -# clear cache -rm -rf ~/.cache/inshellah/ +# clear the on-the-fly user cache (.json/.nu files; system dirs untouched) +inshellah purge # re-index from a prefix inshellah index /usr --dir ~/.cache/inshellah diff --git a/nix/module.nix b/nix/module.nix index f28de8a..f8311cd 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -241,7 +241,7 @@ in dirPaths = lib.concatStringsSep ":" ([ systemDir ] ++ cfg.extraDirs); wrapped = pkgs.writeShellScriptBin "inshellah" '' case "''${1:-}" in - complete|query|dump) + complete|query|dump|purge) exec ${cfg.package}/bin/inshellah "$@" --dir "''${XDG_CACHE_HOME:-$HOME/.cache}/inshellah:${dirPaths}" ;; *) diff --git a/src/main.rs b/src/main.rs index 99322c0..9253f78 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,7 +31,7 @@ use inshellah::parsers::nushell::{generate_extern, generate_module, is_nushell_b use inshellah::pool::{ScrapePool, Submitter}; use inshellah::store::{ all_commands, default_store_path, ensure_dir, file_type_of, filename_of_command, lookup, - lookup_raw, parse_nu_completions, subcommands_of, write_native, write_result, + lookup_raw, parse_nu_completions, purge_dir, subcommands_of, write_native, write_result, }; const COMMAND_SECTIONS: &[u8] = &[1, 8]; @@ -61,6 +61,10 @@ Usage: Print stored completion data for CMD. inshellah dump [--dir PATH[:PATH...]] List indexed commands. + inshellah purge [--dir PATH[:PATH...]] + Delete the on-the-fly user cache (.json/.nu files). Only the first + --dir (the writable user cache) is cleared; system dirs are untouched. + Default dir: $XDG_CACHE_HOME/inshellah inshellah manpage FILE Parse a manpage and emit nushell extern inshellah manpage-dir DIR Batch-process manpages under DIR inshellah completions Generate nushell completions for inshellah @@ -1291,6 +1295,18 @@ fn cmd_dump(dirs: &[PathBuf]) { } } +/// purge the on-the-fly user cache. only the writable user dir is cleared; +/// read-only system overlays are never touched. +fn cmd_purge(user_dir: &Path) { + match purge_dir(user_dir) { + Ok(n) => println!("purged {n} cached entries from {}", user_dir.display()), + Err(e) => { + eprintln!("purge failed: {e}"); + std::process::exit(1); + } + } +} + /// look up a command's path in $PATH. fn find_in_path(name: &str) -> Option { let path_var = std::env::var("PATH").ok()?; @@ -2185,6 +2201,7 @@ fn cmd_completions() { "complete", "query", "dump", + "purge", "completions", ]; let mut subcommands = Vec::new(); @@ -2442,6 +2459,13 @@ fn main() { let (_, dirs, _timeout_ms) = parse_dir_args(&args[2..]); cmd_dump(&dirs); } + "purge" => { + let (_, dirs, _timeout_ms) = parse_dir_args(&args[2..]); + // only the first (writable user) dir is purged; the rest are + // read-only system overlays we must never delete from. + let user_dir = dirs.first().cloned().unwrap_or_else(default_store_path); + cmd_purge(&user_dir); + } "completions" => cmd_completions(), "--help" | "-h" | "help" => usage(), other => { diff --git a/src/store.rs b/src/store.rs index 053f631..8a99bea 100644 --- a/src/store.rs +++ b/src/store.rs @@ -579,6 +579,33 @@ pub fn all_commands(dirs: &[PathBuf]) -> Vec { out.into_iter().collect() } +/// remove every inshellah cache file (`.json` / `.nu`) from a single store +/// directory. only those extensions are touched, so even a misaimed dir +/// won't wipe unrelated files, and the directory itself is left in place. +/// a missing directory is treated as already empty. returns how many files +/// were removed. +pub fn purge_dir(dir: &Path) -> io::Result { + let entries = match fs::read_dir(dir) { + Ok(entries) => entries, + Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(0), + Err(e) => return Err(e), + }; + let mut removed = 0; + for entry in entries.flatten() { + let path = entry.path(); + let is_cache_file = path + .file_name() + .and_then(|n| n.to_str()) + .and_then(chop_extension) + .is_some(); + if is_cache_file && path.is_file() { + fs::remove_file(&path)?; + removed += 1; + } + } + Ok(removed) +} + /// discover subcommands of a command by scanning filenames in the store /// (e.g. for "git", finds "git_add.json", "git_log.json"). pub fn subcommands_of(dirs: &[PathBuf], command: &str) -> Vec { diff --git a/tests/runtime_complete.rs b/tests/runtime_complete.rs index 47b08eb..effb81e 100644 --- a/tests/runtime_complete.rs +++ b/tests/runtime_complete.rs @@ -665,6 +665,47 @@ fn flag_demo_cache(name: &str, flags: &[&str]) -> std::path::PathBuf { cache_dir } +#[test] +fn purge_clears_user_cache_but_not_system_dirs() { + let root = unique_temp_dir("inshellah-purge"); + let user_dir = root.join("cache"); + let system_dir = root.join("system"); + fs::create_dir_all(&user_dir).expect("user dir"); + fs::create_dir_all(&system_dir).expect("system dir"); + + let result = ManpageResult { + entries: Vec::new(), + subcommands: Vec::new(), + positionals: Vec::new(), + description: String::new(), + }; + write_result(&user_dir, "usercmd", "help", &result).expect("user cache"); + write_result(&system_dir, "syscmd", "manpage", &result).expect("system cache"); + // a non-cache file in the user dir must survive the purge. + fs::write(user_dir.join("keep.txt"), "keep me").expect("sentinel"); + + let dir_arg = format!("{}:{}", user_dir.display(), system_dir.display()); + let output = Command::new(env!("CARGO_BIN_EXE_inshellah")) + .args(["purge", "--dir", &dir_arg]) + .output() + .expect("run inshellah purge"); + assert!( + output.status.success(), + "stderr = {}", + String::from_utf8_lossy(&output.stderr) + ); + + // user cache entry gone, non-cache file kept, system dir untouched. + assert!(!user_dir.join("usercmd.json").exists(), "user entry not purged"); + assert!(user_dir.join("keep.txt").exists(), "non-cache file removed"); + assert!( + system_dir.join("syscmd.json").exists(), + "system dir must not be purged" + ); + + let _ = fs::remove_dir_all(root); +} + #[test] fn complete_flag_on_empty_env_surfaces_flags_after_space() { let cache_dir = flag_demo_cache("inshellah-flag-on-empty", &["verbose"]); diff --git a/tests/self_completions.rs b/tests/self_completions.rs index 14b8667..e125d89 100644 --- a/tests/self_completions.rs +++ b/tests/self_completions.rs @@ -20,6 +20,7 @@ fn inshellah_completions_include_all_subcommands() { "complete", "query", "dump", + "purge", "completions", ] { let extern_name = format!("export extern \"inshellah {subcommand}\"");