(** Command-line interface for cache management *) open Cmdliner open Eio let list_cmd ~app_name fs _stdin = let list cache_dir stale expired pinned temporary older_than = let base_dir = Path.(fs / cache_dir) in let cache = Cacheio.create ~base_dir in let entries = Cacheio.scan cache in (* Filter entries based on flags *) let filtered = List.filter (fun e -> let open Cacheio.Entry in (not stale || is_stale e) && (not expired || is_expired e) && (not pinned || is_pinned e) && (not temporary || is_temporary e) && (match older_than with | None -> true | Some days -> let age = (Unix.time () -. mtime e) /. 86400. in age > float_of_int days) ) entries in (* Print entries *) List.iter (fun e -> let open Cacheio.Entry in let flags_str = Cacheio.Flags.pp Format.str_formatter (flags e); Format.flush_str_formatter () in let ttl_str = match ttl e with | None -> "never" | Some t -> Printf.sprintf "%.0f" t in let size = size e in let size_str = if size < 1024L then Printf.sprintf "%Ld B" size else if size < 1048576L then Printf.sprintf "%.1f KB" (Int64.to_float size /. 1024.) else Printf.sprintf "%.1f MB" (Int64.to_float size /. 1048576.) in Printf.printf "%s %s %s %s %s\n" (String.sub (Cacheio.Entry.key e) 0 16) size_str ttl_str flags_str (if Cacheio.Entry.is_expired e then "[EXPIRED]" else "") ) filtered; Printf.printf "\nTotal: %d entries\n" (List.length filtered) in let stale = Arg.(value & flag & info ["stale"; "s"] ~doc:"Show only stale entries") in let expired = Arg.(value & flag & info ["expired"; "e"] ~doc:"Show only expired entries") in let pinned = Arg.(value & flag & info ["pinned"; "p"] ~doc:"Show only pinned entries") in let temporary = Arg.(value & flag & info ["temporary"; "t"] ~doc:"Show only temporary entries") in let older_than = let doc = "Show entries older than N days" in Arg.(value & opt (some int) None & info ["older-than"] ~docv:"DAYS" ~doc) in let doc = "List cache entries" in let info = Cmd.info "list" ~doc in let cache_dir_term = Xdge.Cmd.cache_term app_name in Cmd.v info Term.(const list $ cache_dir_term $ stale $ expired $ pinned $ temporary $ older_than) let expire_cmd ~app_name fs _stdin = let expire cache_dir = let base_dir = Path.(fs / cache_dir) in let cache = Cacheio.create ~base_dir in let count = Cacheio.expire cache in Printf.printf "Expired %d entries\n" count in let doc = "Remove expired entries from cache" in let info = Cmd.info "expire" ~doc in let cache_dir_term = Xdge.Cmd.cache_term app_name in Cmd.v info Term.(const expire $ cache_dir_term) let clear_cmd ~app_name fs _stdin = let clear cache_dir all temporary = let base_dir = Path.(fs / cache_dir) in let cache = Cacheio.create ~base_dir in if all then begin Cacheio.clear cache; Printf.printf "Cleared all entries (except pinned)\n" end else if temporary then begin Cacheio.clear_temporary cache; Printf.printf "Cleared temporary entries\n" end else begin Printf.printf "Specify --all or --temporary to clear cache\n"; exit 1 end in let all = Arg.(value & flag & info ["all"; "a"] ~doc:"Clear all entries (except pinned)") in let temporary = Arg.(value & flag & info ["temporary"; "t"] ~doc:"Clear only temporary entries") in let doc = "Clear cache entries" in let info = Cmd.info "clear" ~doc in let cache_dir_term = Xdge.Cmd.cache_term app_name in Cmd.v info Term.(const clear $ cache_dir_term $ all $ temporary) let flag_cmd ~app_name fs _stdin = let flag cache_dir key add remove set_flags = let base_dir = Path.(fs / cache_dir) in let cache = Cacheio.create ~base_dir in (* Parse flags *) let parse_flag_str s = match String.lowercase_ascii s with | "pinned" | "p" -> Some `Pinned | "stale" | "s" -> Some `Stale | "temporary" | "t" -> Some `Temporary | _ -> None in (* Handle operations *) match key with | None -> Printf.printf "Error: key required\n"; exit 1 | Some k -> (* Add flags *) List.iter (fun flag_str -> match parse_flag_str flag_str with | Some flag -> Cacheio.add_flag cache ~key:k flag; Printf.printf "Added flag %s to %s\n" flag_str k | None -> Printf.printf "Unknown flag: %s\n" flag_str ) add; (* Remove flags *) List.iter (fun flag_str -> match parse_flag_str flag_str with | Some flag -> Cacheio.remove_flag cache ~key:k flag; Printf.printf "Removed flag %s from %s\n" flag_str k | None -> Printf.printf "Unknown flag: %s\n" flag_str ) remove; (* Set flags (replace all) *) (match set_flags with | [] -> () | flags -> let parsed = List.filter_map parse_flag_str flags in Cacheio.set_flags cache ~key:k (Cacheio.Flags.of_list parsed); Printf.printf "Set flags for %s\n" k) in let key = Arg.(value & pos 0 (some string) None & info [] ~docv:"KEY" ~doc:"Cache key to modify") in let add = let doc = "Add flag (pinned|stale|temporary)" in Arg.(value & opt_all string [] & info ["add"; "a"] ~doc) in let remove = let doc = "Remove flag (pinned|stale|temporary)" in Arg.(value & opt_all string [] & info ["remove"; "r"] ~doc) in let set_flags = let doc = "Set flags (replaces all existing)" in Arg.(value & opt_all string [] & info ["set"] ~doc) in let doc = "Manage cache entry flags" in let info = Cmd.info "flag" ~doc in let cache_dir_term = Xdge.Cmd.cache_term app_name in Cmd.v info Term.(const flag $ cache_dir_term $ key $ add $ remove $ set_flags) let stats_cmd ~app_name fs _stdin = let stats cache_dir = let base_dir = Path.(fs / cache_dir) in let cache = Cacheio.create ~base_dir in let stats = Cacheio.stats cache in let size_str size = if size < 1024L then Printf.sprintf "%Ld B" size else if size < 1048576L then Printf.sprintf "%.1f KB" (Int64.to_float size /. 1024.) else if size < 1073741824L then Printf.sprintf "%.1f MB" (Int64.to_float size /. 1048576.) else Printf.sprintf "%.1f GB" (Int64.to_float size /. 1073741824.) in let open Cacheio.Stats in Printf.printf "Cache Statistics:\n"; Printf.printf " Total entries: %d\n" (entry_count stats); Printf.printf " Total size: %s\n" (size_str (total_size stats)); Printf.printf " Expired entries: %d\n" (expired_count stats); Printf.printf " Pinned entries: %d\n" (pinned_count stats); Printf.printf " Stale entries: %d\n" (stale_count stats); Printf.printf " Temporary entries: %d\n" (temporary_count stats) in let doc = "Show cache statistics" in let info = Cmd.info "stats" ~doc in let cache_dir_term = Xdge.Cmd.cache_term app_name in Cmd.v info Term.(const stats $ cache_dir_term) let put_cmd ~app_name fs stdin = let put cache_dir key file ttl pinned stale temporary = let base_dir = Path.(fs / cache_dir) in let cache = Cacheio.create ~base_dir in (* Build flags from options *) let flags = let open Cacheio.Flags in let f = empty in let f = if pinned then add `Pinned f else f in let f = if stale then add `Stale f else f in let f = if temporary then add `Temporary f else f in f in (* Read from file or stdin *) Switch.run @@ fun sw -> let source = match file with | Some path -> let file_path = Path.(fs / path) in (Path.open_in ~sw file_path :> Eio.Flow.source_ty Eio.Resource.t) | None -> (* Use stdin passed from the CLI *) stdin in (* Put into cache *) Cacheio.put cache ~key ~source ?ttl:(Some ttl) ~flags (); Printf.printf "Cached entry with key: %s\n" key; (* Show info about what was cached *) match Cacheio.size cache ~key with | Some size -> Printf.printf "Size: %Ld bytes\n" size | None -> () in let key = Arg.(required & pos 0 (some string) None & info [] ~docv:"KEY" ~doc:"Cache key for the entry") in let file = Arg.(value & opt (some string) None & info ["file"; "f"] ~docv:"FILE" ~doc:"File to cache (default: read from stdin)") in let ttl = Arg.(value & opt (some float) None & info ["ttl"] ~docv:"SECONDS" ~doc:"Time-to-live in seconds") in let pinned = Arg.(value & flag & info ["pinned"; "p"] ~doc:"Mark entry as pinned") in let stale = Arg.(value & flag & info ["stale"; "s"] ~doc:"Mark entry as stale") in let temporary = Arg.(value & flag & info ["temporary"; "t"] ~doc:"Mark entry as temporary") in let doc = "Put data into cache" in let man = [ `S "DESCRIPTION"; `P "Store data in the cache with the specified key. Data can be read from a file or stdin."; `P "Optionally set TTL (time-to-live) and flags (pinned, stale, temporary)."; `S "EXAMPLES"; `P "Cache a file: $(b,cache put mykey -f data.txt)"; `P "Cache from stdin: $(b,echo 'Hello' | cache put mykey)"; `P "Cache with TTL: $(b,cache put mykey -f data.txt --ttl 3600)"; `P "Cache as pinned: $(b,cache put mykey -f data.txt --pinned)"; ] in let info = Cmd.info "put" ~doc ~man in let cache_dir_term = Xdge.Cmd.cache_term app_name in Cmd.v info Term.(const put $ cache_dir_term $ key $ file $ ttl $ pinned $ stale $ temporary) let get_cmd ~app_name fs _stdin = let get cache_dir key output = let base_dir = Path.(fs / cache_dir) in let cache = Cacheio.create ~base_dir in (* Get from cache *) Switch.run @@ fun sw -> match Cacheio.get cache ~key ~sw with | None -> Printf.eprintf "Key not found: %s\n" key; exit 1 | Some source -> (* Write to file or stdout *) match output with | Some path -> let file_path = Path.(fs / path) in let sink = Path.open_out ~sw ~create:(`Or_truncate 0o644) file_path in Eio.Flow.copy source sink; Printf.eprintf "Wrote cached data to: %s\n" path | None -> (* For stdout, we need to read the content and print it *) let buf = Buffer.create 4096 in Eio.Flow.copy source (Eio.Flow.buffer_sink buf); print_string (Buffer.contents buf) in let key = Arg.(required & pos 0 (some string) None & info [] ~docv:"KEY" ~doc:"Cache key to retrieve") in let output = Arg.(value & opt (some string) None & info ["output"; "o"] ~docv:"FILE" ~doc:"Output file (default: write to stdout)") in let doc = "Get data from cache" in let man = [ `S "DESCRIPTION"; `P "Retrieve data from the cache using the specified key. Data can be written to a file or stdout."; `S "EXAMPLES"; `P "Get to stdout: $(b,cache get mykey)"; `P "Get to file: $(b,cache get mykey -o output.txt)"; `P "Use in pipeline: $(b,cache get mykey | grep pattern)"; ] in let info = Cmd.info "get" ~doc ~man in let cache_dir_term = Xdge.Cmd.cache_term app_name in Cmd.v info Term.(const get $ cache_dir_term $ key $ output) (** Delete command - remove a specific cache entry *) let delete_cmd ~app_name fs _stdin = let delete cache_dir key = let base_dir = Path.(fs / cache_dir) in let cache = Cacheio.create ~base_dir in if Cacheio.exists cache ~key then begin Cacheio.delete cache ~key; Printf.printf "Deleted cache entry: %s\n" key end else begin Printf.eprintf "Key not found: %s\n" key; exit 1 end in let key = Arg.(required & pos 0 (some string) None & info [] ~docv:"KEY" ~doc:"Cache key to delete") in let doc = "Delete a cache entry" in let man = [ `S "DESCRIPTION"; `P "Remove a specific cache entry by key."; `S "EXAMPLES"; `P "Delete entry: $(b,cache delete mykey)"; ] in let info = Cmd.info "delete" ~doc ~man in let cache_dir_term = Xdge.Cmd.cache_term app_name in Cmd.v info Term.(const delete $ cache_dir_term $ key) (** Exists command - check if a key exists *) let exists_cmd ~app_name fs _stdin = let exists cache_dir key quiet = let base_dir = Path.(fs / cache_dir) in let cache = Cacheio.create ~base_dir in if Cacheio.exists cache ~key then begin if not quiet then Printf.printf "Key exists: %s\n" key; exit 0 end else begin if not quiet then Printf.printf "Key does not exist: %s\n" key; exit 1 end in let key = Arg.(required & pos 0 (some string) None & info [] ~docv:"KEY" ~doc:"Cache key to check") in let quiet = Arg.(value & flag & info ["quiet"; "q"] ~doc:"Suppress output, use exit code only") in let doc = "Check if a cache entry exists" in let man = [ `S "DESCRIPTION"; `P "Check if a cache entry exists. Returns exit code 0 if exists, 1 if not."; `S "EXAMPLES"; `P "Check existence: $(b,cache exists mykey)"; `P "Use in scripts: $(b,if cache exists mykey -q; then echo 'Found'; fi)"; ] in let info = Cmd.info "exists" ~doc ~man in let cache_dir_term = Xdge.Cmd.cache_term app_name in Cmd.v info Term.(const exists $ cache_dir_term $ key $ quiet) (** {1 Command Groups} *) let make_commands ~app_name fs stdin = [ get_cmd ~app_name fs stdin; put_cmd ~app_name fs stdin; delete_cmd ~app_name fs stdin; exists_cmd ~app_name fs stdin; list_cmd ~app_name fs stdin; expire_cmd ~app_name fs stdin; clear_cmd ~app_name fs stdin; flag_cmd ~app_name fs stdin; stats_cmd ~app_name fs stdin ] let make_cmd ~app_name fs stdin = let doc = "Cache management commands" in let info = Cmd.info "cache" ~doc in Cmd.group info (make_commands ~app_name fs stdin)