My agentic slop goes here. Not intended for anyone else!
at main 23 kB view raw
1(** Toru Cache Management CLI Tool *) 2 3open Cmdliner 4 5module File_info = struct 6 type t = { 7 name : string; 8 size : int64; 9 mtime : Ptime.t; 10 path : string; 11 } 12 13 let create ~name ~size ~mtime ~path = 14 { name; size; mtime; path } 15 16 let compare_by_size a b = Int64.compare b.size a.size (* Largest first *) 17 let compare_by_age a b = Ptime.compare a.mtime b.mtime (* Oldest first *) 18 let compare_by_name a b = String.compare a.name b.name 19end 20 21module Utils = struct 22 let human_readable_bytes bytes = 23 let units = [|"B"; "KB"; "MB"; "GB"; "TB"|] in 24 let rec loop bytes unit_index = 25 if bytes < 1024.0 || unit_index >= Array.length units - 1 then 26 Printf.sprintf "%.1f %s" bytes units.(unit_index) 27 else 28 loop (bytes /. 1024.0) (unit_index + 1) 29 in 30 loop (Int64.to_float bytes) 0 31 32 let format_time_ago ptime = 33 let now = Ptime_clock.now () in 34 let span = Ptime.diff now ptime in 35 let days = Ptime.Span.to_d_ps span |> fst in 36 if days = 0 then "today" 37 else if days = 1 then "1 day ago" 38 else Printf.sprintf "%d days ago" days 39 40 41 let get_file_info cache_path filename = 42 let full_path = Eio.Path.(cache_path / filename) in 43 try 44 let stat = Eio.Path.stat ~follow:false full_path in 45 let mtime = Ptime.of_float_s stat.mtime |> Option.value ~default:(Ptime_clock.now ()) 46 in 47 Some (File_info.create 48 ~name:filename 49 ~size:(Optint.Int63.to_int64 stat.size) 50 ~mtime 51 ~path:(Eio.Path.native_exn full_path)) 52 with 53 | _ -> None 54 55 let collect_file_info cache = 56 let cache_path = match Toru.Cache.version cache with 57 | None -> Toru.Cache.base_path cache 58 | Some v -> Eio.Path.(Toru.Cache.base_path cache / v) 59 in 60 let filenames = Toru.Cache.list_files cache in 61 List.filter_map (get_file_info cache_path) filenames 62 63 let print_header title = 64 Fmt.(pf stdout "%a@." (styled `Bold (styled `Cyan string)) title); 65 Fmt.(pf stdout "%a@." (styled `Cyan string) (String.make (String.length title) '=')) 66 67 let print_success msg = 68 Fmt.(pf stdout "%a%s@." (styled `Green string) "[OK] " msg) 69 70 let print_warning msg = 71 Fmt.(pf stdout "%a%s@." (styled `Yellow string) "[WARN] " msg) 72 73 let print_error msg = 74 Fmt.(pf stdout "%a%s@." (styled `Red string) "[ERROR] " msg) 75end 76 77(* Global options *) 78type global_opts = { 79 cache_dir : string option; 80 app_name : string; 81 version : string option; 82} 83 84let global_opts_term = 85 let cache_dir = 86 let doc = "Override default cache location" in 87 Arg.(value & opt (some string) None & info ["cache-dir"; "c"] ~docv:"DIR" ~doc) 88 in 89 let app_name = 90 let doc = "Override application name (default: toru)" in 91 Arg.(value & opt string "toru" & info ["app-name"] ~docv:"NAME" ~doc) 92 in 93 let version = 94 let doc = "Target specific cache version" in 95 Arg.(value & opt (some string) None & info ["cache-version"; "v"] ~docv:"VER" ~doc) 96 in 97 Term.(const (fun cache_dir app_name version -> { cache_dir; app_name; version }) 98 $ cache_dir $ app_name $ version) 99 100(* Command implementations *) 101let info_cmd env sw global_opts = 102 let cache = match global_opts.cache_dir with 103 | Some path -> Toru.Cache.create ~sw ~fs:env#fs ?version:global_opts.version path 104 | None -> 105 let base_path = Toru.Cache.default_cache_path ~app_name:global_opts.app_name () in 106 Toru.Cache.create ~sw ~fs:env#fs ?version:global_opts.version base_path 107 in 108 109 let cache = match global_opts.version with 110 | Some v -> 111 (* Create new cache with version override *) 112 (match global_opts.cache_dir with 113 | Some path -> Toru.Cache.create ~sw ~fs:env#fs ~version:v path 114 | None -> 115 let base_path = Toru.Cache.default_cache_path ~app_name:global_opts.app_name () in 116 Toru.Cache.create ~sw ~fs:env#fs ~version:v base_path) 117 | None -> cache 118 in 119 120 Utils.print_header "Toru Cache Information"; 121 122 let cache_path = Toru.Cache.base_path cache in 123 let version_path = match Toru.Cache.version cache with 124 | Some v -> Eio.Path.(cache_path / v) 125 | None -> cache_path 126 in 127 128 Printf.printf "Location: %s\n" (Eio.Path.native_exn version_path); 129 130 (match Toru.Cache.version cache with 131 | Some v -> Printf.printf "Version: %s\n" v 132 | None -> Printf.printf "Version: none\n"); 133 134 let total_size = Toru.Cache.size_bytes cache in 135 Printf.printf "Total Size: %s (%Ld bytes)\n" 136 (Utils.human_readable_bytes total_size) total_size; 137 138 let file_count = List.length (Toru.Cache.list_files cache) in 139 Printf.printf "File Count: %d files\n" file_count; 140 141 if file_count > 0 then ( 142 let file_infos = Utils.collect_file_info cache in 143 match file_infos with 144 | [] -> Printf.printf "Age Range: No files found\n" 145 | files -> 146 let sorted_by_age = List.sort File_info.compare_by_age files in 147 let oldest = List.hd sorted_by_age in 148 let newest = List.hd (List.rev sorted_by_age) in 149 Printf.printf "Age Range: %s to %s\n" 150 (Utils.format_time_ago oldest.mtime) 151 (Utils.format_time_ago newest.mtime) 152 ); 153 154 Printf.printf "Free Space: Unable to determine\n"; 155 0 156 157let list_cmd env sw global_opts sort_by format limit = 158 let cache = match global_opts.cache_dir with 159 | Some path -> Toru.Cache.create ~sw ~fs:env#fs ?version:global_opts.version path 160 | None -> 161 let base_path = Toru.Cache.default_cache_path ~app_name:global_opts.app_name () in 162 Toru.Cache.create ~sw ~fs:env#fs ?version:global_opts.version base_path 163 in 164 165 let cache = match global_opts.version with 166 | Some v -> 167 (match global_opts.cache_dir with 168 | Some path -> Toru.Cache.create ~sw ~fs:env#fs ~version:v path 169 | None -> 170 let base_path = Toru.Cache.default_cache_path ~app_name:global_opts.app_name () in 171 Toru.Cache.create ~sw ~fs:env#fs ~version:v base_path) 172 | None -> cache 173 in 174 175 let file_infos = Utils.collect_file_info cache in 176 177 let sorted_files = match sort_by with 178 | `Size -> List.sort File_info.compare_by_size file_infos 179 | `Age -> List.sort File_info.compare_by_age file_infos 180 | `Name -> List.sort File_info.compare_by_name file_infos 181 in 182 183 let limited_files = match limit with 184 | Some n -> 185 let rec take n lst acc = 186 match n, lst with 187 | 0, _ | _, [] -> List.rev acc 188 | n, x :: xs -> take (n - 1) xs (x :: acc) 189 in 190 take n sorted_files [] 191 | None -> sorted_files 192 in 193 194 (match format with 195 | `Table -> 196 if limited_files = [] then 197 Printf.printf "No files found in cache.\n" 198 else ( 199 Printf.printf "%-50s %12s %12s %s\n" "Filename" "Size" "Age" "Hash (SHA256)"; 200 Fmt.(pf stdout "%a@." (styled `Cyan string) (String.make 90 '-')); 201 202 List.iter (fun file -> 203 let truncated_name = 204 if String.length file.File_info.name > 47 then 205 String.sub file.File_info.name 0 44 ^ "..." 206 else file.File_info.name 207 in 208 Printf.printf "%-50s %12s %12s %s\n" 209 truncated_name 210 (Utils.human_readable_bytes file.File_info.size) 211 (Utils.format_time_ago file.File_info.mtime) 212 "no hash" (* TODO: Add hash computation if needed *) 213 ) limited_files 214 ) 215 | `Json -> 216 let json_files = List.map (fun file -> 217 `Assoc [ 218 ("filename", `String file.File_info.name); 219 ("size", `Int (Int64.to_int file.File_info.size)); 220 ("path", `String file.File_info.path); 221 ("mtime", `String (Ptime.to_rfc3339 file.File_info.mtime)); 222 ("age_days", `Int ( 223 let span = Ptime.diff (Ptime_clock.now ()) file.File_info.mtime in 224 Ptime.Span.to_d_ps span |> fst 225 )); 226 ] 227 ) limited_files in 228 let json_output = `List json_files in 229 Printf.printf "%s\n" (Yojson.Safe.pretty_to_string json_output) 230 ); 231 0 232 233let size_cmd env sw global_opts breakdown human_readable = 234 let cache = match global_opts.cache_dir with 235 | Some path -> Toru.Cache.create ~sw ~fs:env#fs ?version:global_opts.version path 236 | None -> 237 let base_path = Toru.Cache.default_cache_path ~app_name:global_opts.app_name () in 238 Toru.Cache.create ~sw ~fs:env#fs ?version:global_opts.version base_path 239 in 240 241 let cache = match global_opts.version with 242 | Some v -> 243 (match global_opts.cache_dir with 244 | Some path -> Toru.Cache.create ~sw ~fs:env#fs ~version:v path 245 | None -> 246 let base_path = Toru.Cache.default_cache_path ~app_name:global_opts.app_name () in 247 Toru.Cache.create ~sw ~fs:env#fs ~version:v base_path) 248 | None -> cache 249 in 250 251 let total_size = Toru.Cache.size_bytes cache in 252 253 if human_readable then 254 Printf.printf "Total Size: %s\n" (Utils.human_readable_bytes total_size) 255 else 256 Printf.printf "Total Size: %Ld bytes\n" total_size; 257 258 if breakdown then ( 259 let file_infos = Utils.collect_file_info cache in 260 261 (* Breakdown by file extension *) 262 let ext_map = Hashtbl.create 16 in 263 List.iter (fun file -> 264 let ext = 265 try 266 let dot_idx = String.rindex file.File_info.name '.' in 267 String.sub file.File_info.name dot_idx (String.length file.File_info.name - dot_idx) 268 with 269 | Not_found -> "no extension" 270 in 271 let current = Hashtbl.find_opt ext_map ext |> Option.value ~default:0L in 272 Hashtbl.replace ext_map ext (Int64.add current file.File_info.size) 273 ) file_infos; 274 275 Printf.printf "\nBreakdown by file type:\n"; 276 Fmt.(pf stdout "%a@." (styled `Cyan string) (String.make 40 '-')); 277 278 let ext_list = Hashtbl.fold (fun ext size acc -> (ext, size) :: acc) ext_map [] in 279 let sorted_ext = List.sort (fun (_, a) (_, b) -> Int64.compare b a) ext_list in 280 281 List.iter (fun (ext, size) -> 282 let percentage = if total_size > 0L then 283 Int64.to_float size /. Int64.to_float total_size *. 100.0 284 else 0.0 in 285 if human_readable then 286 Printf.printf "%-20s %12s (%5.1f%%)\n" ext (Utils.human_readable_bytes size) percentage 287 else 288 Printf.printf "%-20s %12Ld (%5.1f%%)\n" ext size percentage 289 ) sorted_ext; 290 291 (* Breakdown by age *) 292 let now = Ptime_clock.now () in 293 let age_buckets = [ 294 ("< 1 day", 1); 295 ("1-7 days", 7); 296 ("1-4 weeks", 28); 297 ("1-12 months", 365); 298 ("> 1 year", max_int); 299 ] in 300 301 Printf.printf "\nBreakdown by age:\n"; 302 Fmt.(pf stdout "%a@." (styled `Cyan string) (String.make 40 '-')); 303 304 List.iter (fun (label, max_days) -> 305 let bucket_size = List.fold_left (fun acc file -> 306 let span = Ptime.diff now file.File_info.mtime in 307 let days = Ptime.Span.to_d_ps span |> fst in 308 if days <= max_days then Int64.add acc file.File_info.size else acc 309 ) 0L file_infos in 310 311 let percentage = if total_size > 0L then 312 Int64.to_float bucket_size /. Int64.to_float total_size *. 100.0 313 else 0.0 in 314 315 if human_readable then 316 Printf.printf "%-20s %12s (%5.1f%%)\n" label (Utils.human_readable_bytes bucket_size) percentage 317 else 318 Printf.printf "%-20s %12Ld (%5.1f%%)\n" label bucket_size percentage 319 ) age_buckets 320 ); 321 (); 322 0 323 324let clean_cmd env sw global_opts max_size max_age dry_run = 325 let cache = match global_opts.cache_dir with 326 | Some path -> Toru.Cache.create ~sw ~fs:env#fs ?version:global_opts.version path 327 | None -> 328 let base_path = Toru.Cache.default_cache_path ~app_name:global_opts.app_name () in 329 Toru.Cache.create ~sw ~fs:env#fs ?version:global_opts.version base_path 330 in 331 332 let cache = match global_opts.version with 333 | Some v -> 334 (match global_opts.cache_dir with 335 | Some path -> Toru.Cache.create ~sw ~fs:env#fs ~version:v path 336 | None -> 337 let base_path = Toru.Cache.default_cache_path ~app_name:global_opts.app_name () in 338 Toru.Cache.create ~sw ~fs:env#fs ~version:v base_path) 339 | None -> cache 340 in 341 342 let file_infos = Utils.collect_file_info cache in 343 let now = Ptime_clock.now () in 344 345 let files_to_remove = List.filter (fun file -> 346 (* Check age constraint *) 347 let age_match = match max_age with 348 | Some max_days -> 349 let span = Ptime.diff now file.File_info.mtime in 350 let days = Ptime.Span.to_d_ps span |> fst in 351 days > max_days 352 | None -> true 353 in 354 age_match 355 ) file_infos in 356 357 (* If max_size is specified, sort by age and remove oldest until under limit *) 358 let files_to_remove = match max_size with 359 | Some max_bytes -> 360 let current_size = List.fold_left (fun acc file -> Int64.add acc file.File_info.size) 0L file_infos in 361 if current_size <= max_bytes then 362 [] 363 else 364 let sorted_by_age = List.sort File_info.compare_by_age file_infos in 365 let rec select_for_removal remaining_files target_reduction acc_size acc_files = 366 match remaining_files with 367 | [] -> acc_files 368 | file :: rest -> 369 if acc_size >= target_reduction then acc_files 370 else select_for_removal rest target_reduction 371 (Int64.add acc_size file.File_info.size) (file :: acc_files) 372 in 373 select_for_removal sorted_by_age (Int64.sub current_size max_bytes) 0L [] 374 | None -> files_to_remove 375 in 376 377 if files_to_remove = [] then ( 378 Utils.print_success "No files need to be removed."; 379 () 380 ) else ( 381 let total_size_to_remove = List.fold_left (fun acc file -> Int64.add acc file.File_info.size) 0L files_to_remove in 382 let file_count = List.length files_to_remove in 383 384 if dry_run then ( 385 Utils.print_header "Dry Run: Cache Cleanup"; 386 Printf.printf "Would remove %d files (%s)\n\n" 387 file_count (Utils.human_readable_bytes total_size_to_remove); 388 389 Printf.printf "Files to be removed:\n"; 390 List.iter (fun file -> 391 Printf.printf "- %s (%s, %s)\n" 392 file.File_info.name 393 (Utils.human_readable_bytes file.File_info.size) 394 (Utils.format_time_ago file.File_info.mtime) 395 ) files_to_remove; 396 397 print_endline ""; 398 Utils.print_warning "Use without --dry-run to proceed with cleanup." 399 ) else ( 400 Utils.print_header "Cache Cleanup"; 401 Printf.printf "Removing %d files (%s)...\n\n" 402 file_count (Utils.human_readable_bytes total_size_to_remove); 403 404 let removed_count = ref 0 in 405 List.iter (fun file -> 406 try 407 Unix.unlink file.File_info.path; 408 incr removed_count; 409 Printf.printf "Removed: %s\n" file.File_info.name 410 with 411 | exn -> 412 Utils.print_error (Printf.sprintf "Failed to remove %s: %s" 413 file.File_info.name (Printexc.to_string exn)) 414 ) files_to_remove; 415 416 Printf.printf "\nRemoved %d files successfully.\n" !removed_count 417 ); 418 () 419 ); 420 0 421 422let vacuum_cmd env sw global_opts dry_run = 423 let cache = match global_opts.cache_dir with 424 | Some path -> Toru.Cache.create ~sw ~fs:env#fs ?version:global_opts.version path 425 | None -> 426 let base_path = Toru.Cache.default_cache_path ~app_name:global_opts.app_name () in 427 Toru.Cache.create ~sw ~fs:env#fs ?version:global_opts.version base_path 428 in 429 430 let cache = match global_opts.version with 431 | Some v -> 432 (match global_opts.cache_dir with 433 | Some path -> Toru.Cache.create ~sw ~fs:env#fs ~version:v path 434 | None -> 435 let base_path = Toru.Cache.default_cache_path ~app_name:global_opts.app_name () in 436 Toru.Cache.create ~sw ~fs:env#fs ~version:v base_path) 437 | None -> cache 438 in 439 440 let cache_path = match Toru.Cache.version cache with 441 | None -> Toru.Cache.base_path cache 442 | Some v -> Eio.Path.(Toru.Cache.base_path cache / v) 443 in 444 445 let rec find_empty_dirs path = 446 try 447 let entries = Eio.Path.read_dir path in 448 if entries = [] then 449 [path] 450 else 451 List.fold_left (fun acc entry -> 452 let entry_path = Eio.Path.(path / entry) in 453 let stat = Eio.Path.stat ~follow:false entry_path in 454 match stat.kind with 455 | `Directory -> (find_empty_dirs entry_path) @ acc 456 | _ -> acc 457 ) [] entries 458 with 459 | _ -> [] 460 in 461 462 let empty_dirs = find_empty_dirs cache_path in 463 let cache_path_str = Eio.Path.native_exn cache_path in 464 let empty_dirs = List.filter (fun dir -> not (String.equal (Eio.Path.native_exn dir) cache_path_str)) empty_dirs in 465 466 if empty_dirs = [] then ( 467 Utils.print_success "No empty directories found."; 468 () 469 ) else ( 470 if dry_run then ( 471 Utils.print_header "Dry Run: Vacuum Cache"; 472 Printf.printf "Would remove %d empty directories:\n\n" (List.length empty_dirs); 473 List.iter (fun dir -> 474 Printf.printf "- %s\n" (Eio.Path.native_exn dir) 475 ) empty_dirs; 476 print_endline ""; 477 Utils.print_warning "Use without --dry-run to proceed with vacuum." 478 ) else ( 479 Utils.print_header "Vacuum Cache"; 480 Printf.printf "Removing %d empty directories...\n\n" (List.length empty_dirs); 481 482 let removed_count = ref 0 in 483 List.iter (fun dir -> 484 try 485 Eio.Path.rmdir dir; 486 incr removed_count; 487 Printf.printf "Removed: %s\n" (Eio.Path.native_exn dir) 488 with 489 | exn -> 490 Utils.print_error (Printf.sprintf "Failed to remove %s: %s" 491 (Eio.Path.native_exn dir) (Printexc.to_string exn)) 492 ) empty_dirs; 493 494 Printf.printf "\nRemoved %d directories successfully.\n" !removed_count 495 ); 496 () 497 ); 498 0 499 500(* Command definitions *) 501let info_cmd_def eio_env sw = 502 let doc = "Show cache statistics and location" in 503 let term = Term.(const (info_cmd eio_env sw) $ global_opts_term) in 504 Cmd.v (Cmd.info "info" ~doc) term 505 506let list_cmd_def eio_env sw = 507 let sort_by = 508 let doc = "Sort files by size, age, or name" in 509 Arg.(value & opt (enum [("size", `Size); ("age", `Age); ("name", `Name)]) `Name & 510 info ["sort"] ~docv:"FIELD" ~doc) 511 in 512 let format = 513 let doc = "Output format: table or json" in 514 Arg.(value & opt (enum [("table", `Table); ("json", `Json)]) `Table & 515 info ["format"; "f"] ~docv:"FORMAT" ~doc) 516 in 517 let limit = 518 let doc = "Limit number of files shown" in 519 Arg.(value & opt (some int) None & info ["limit"; "n"] ~docv:"N" ~doc) 520 in 521 let doc = "List cached files with details" in 522 let term = Term.(const (list_cmd eio_env sw) $ global_opts_term $ sort_by $ format $ limit) in 523 Cmd.v (Cmd.info "list" ~doc) term 524 525let size_cmd_def eio_env sw = 526 let breakdown = 527 let doc = "Show size breakdown by file type and age" in 528 Arg.(value & flag & info ["breakdown"; "b"] ~doc) 529 in 530 let human_readable = 531 let doc = "Display sizes in human readable format" in 532 Arg.(value & flag & info ["human-readable"; "h"] ~doc) 533 in 534 let doc = "Show cache size information" in 535 let term = Term.(const (size_cmd eio_env sw) $ global_opts_term $ breakdown $ human_readable) in 536 Cmd.v (Cmd.info "size" ~doc) term 537 538let clean_cmd_def eio_env sw = 539 let max_size = 540 let doc = "Remove files to get cache under this size (e.g., 1GB, 500MB)" in 541 let parse_size s = 542 let s = String.uppercase_ascii s in 543 let len = String.length s in 544 if len < 2 then Error "Invalid size format" 545 else 546 let (num_str, unit) = 547 if String.sub s (len-2) 2 = "GB" then 548 (String.sub s 0 (len-2), Int64.(mul 1024L (mul 1024L 1024L))) 549 else if String.sub s (len-2) 2 = "MB" then 550 (String.sub s 0 (len-2), Int64.(mul 1024L 1024L)) 551 else if String.sub s (len-2) 2 = "KB" then 552 (String.sub s 0 (len-2), 1024L) 553 else if String.sub s (len-1) 1 = "B" then 554 (String.sub s 0 (len-1), 1L) 555 else 556 (s, 1L) 557 in 558 try 559 let num = Float.of_string num_str in 560 Ok (Int64.of_float (num *. Int64.to_float unit)) 561 with 562 | _ -> Error "Invalid number in size" 563 in 564 let size_conv = Arg.conv' (parse_size, fun fmt size -> 565 Format.fprintf fmt "%Ld" size) in 566 Arg.(value & opt (some size_conv) None & info ["max-size"] ~docv:"SIZE" ~doc) 567 in 568 let max_age = 569 let doc = "Remove files older than this many days" in 570 Arg.(value & opt (some int) None & info ["max-age"] ~docv:"DAYS" ~doc) 571 in 572 let dry_run = 573 let doc = "Show what would be removed without actually removing" in 574 Arg.(value & flag & info ["dry-run"; "n"] ~doc) 575 in 576 let doc = "Clean cache with various options" in 577 let term = Term.(const (clean_cmd eio_env sw) $ global_opts_term $ max_size $ max_age $ dry_run) in 578 Cmd.v (Cmd.info "clean" ~doc) term 579 580let vacuum_cmd_def eio_env sw = 581 let dry_run = 582 let doc = "Show what would be removed without actually removing" in 583 Arg.(value & flag & info ["dry-run"; "n"] ~doc) 584 in 585 let doc = "Remove empty directories and broken links" in 586 let term = Term.(const (vacuum_cmd eio_env sw) $ global_opts_term $ dry_run) in 587 Cmd.v (Cmd.info "vacuum" ~doc) term 588 589let main_cmd eio_env sw = 590 let doc = "Toru cache management tool" in 591 let sdocs = Manpage.s_common_options in 592 let man = [ 593 `S Manpage.s_description; 594 `P "$(tname) manages the Toru data cache, providing commands to inspect, clean, and maintain cached data files."; 595 `P "The cache follows XDG Base Directory specifications on Unix systems and uses appropriate locations on other platforms."; 596 `S Manpage.s_commands; 597 `P "Use $(b,$(tname) COMMAND --help) for command-specific help."; 598 `S "ENVIRONMENT VARIABLES"; 599 `P "$(b,XDG_CACHE_HOME) - Override default cache location on Unix systems"; 600 `P "$(b,TORU_CACHE_DIR) - Override cache location (takes precedence)"; 601 `S "EXAMPLES"; 602 `P "$(b,toru-cache info) - Show cache information"; 603 `P "$(b,toru-cache list --sort=size --limit=10) - Show 10 largest files"; 604 `P "$(b,toru-cache clean --max-size=1GB --dry-run) - Preview cleanup to 1GB limit"; 605 `P "$(b,toru-cache size --breakdown -h) - Show human-readable size breakdown"; 606 ] in 607 let default = Term.(const 0) in 608 Cmd.group ~default (Cmd.info "toru-cache" ~version:"0.1.0" ~doc ~sdocs ~man) 609 [info_cmd_def eio_env sw; list_cmd_def eio_env sw; size_cmd_def eio_env sw; clean_cmd_def eio_env sw; vacuum_cmd_def eio_env sw] 610 611let () = 612 Eio_main.run @@ fun env -> 613 Eio.Switch.run @@ fun sw -> 614 exit (Cmd.eval' (main_cmd env sw))