My agentic slop goes here. Not intended for anyone else!

river

Changed files
+280 -1
stack
+222 -1
stack/river/cmd/river_cmd.ml
···
1
) $ output_dir_arg $ title_arg $ posts_per_page_arg)
let main_cmd =
let doc = "River feed management CLI" in
let main_info = Cmd.info "river-cli" ~version:"1.0" ~doc in
···
~service:"river"
html
in
-
Cmd.group main_info [user_cmd; sync_cmd; list_cmd; info_cmd; merge_cmd; html_cmd]
···
1
) $ output_dir_arg $ title_arg $ posts_per_page_arg)
+
(* Category management commands *)
+
module Category = struct
+
let list state =
+
let categories = River.State.list_categories state in
+
if categories = [] then begin
+
Fmt.pr "@.%a@.@."
+
Fmt.(styled `Yellow string)
+
"No categories defined yet. Use 'river category add' to create one."
+
end else begin
+
Fmt.pr "@.%a@."
+
Fmt.(styled `Bold (styled (`Fg `Cyan) string))
+
(Printf.sprintf "Categories (%d total)" (List.length categories));
+
Fmt.pr "%a@.@." Fmt.(styled `Faint string) (String.make 60 '-');
+
List.iter (fun cat ->
+
Fmt.pr "%a %a@."
+
Fmt.(styled `Bold (styled (`Fg `Blue) string)) (River.Category.id cat)
+
Fmt.(styled `Green string) (River.Category.name cat);
+
(match River.Category.description cat with
+
| Some desc ->
+
Fmt.pr " %a %a@."
+
Fmt.(styled `Faint string) "└─"
+
Fmt.(styled `Faint string) desc
+
| None -> ());
+
Fmt.pr "@."
+
) categories
+
end;
+
0
+
+
let add state ~id ~name ?description () =
+
let category = River.Category.create ~id ~name ?description () in
+
match River.State.add_category state category with
+
| Ok () ->
+
Fmt.pr "@.%a Category %a added successfully@.@."
+
Fmt.(styled (`Fg `Green) string) "✓"
+
Fmt.(styled `Bold string) id;
+
0
+
| Error err ->
+
Fmt.pr "@.%a Failed to add category: %s@.@."
+
Fmt.(styled (`Fg `Red) string) "✗"
+
err;
+
1
+
+
let remove state ~id =
+
match River.State.remove_category state ~id with
+
| Ok () ->
+
Fmt.pr "@.%a Category %a removed successfully@.@."
+
Fmt.(styled (`Fg `Green) string) "✓"
+
Fmt.(styled `Bold string) id;
+
0
+
| Error err ->
+
Fmt.pr "@.%a Failed to remove category: %s@.@."
+
Fmt.(styled (`Fg `Red) string) "✗"
+
err;
+
1
+
+
let show state ~id =
+
match River.State.get_category state ~id with
+
| None ->
+
Fmt.pr "@.%a Category %a not found@.@."
+
Fmt.(styled (`Fg `Red) string) "✗ Error:"
+
Fmt.(styled `Bold string) id;
+
1
+
| Some cat ->
+
Fmt.pr "@.%a@."
+
Fmt.(styled `Bold (styled (`Fg `Cyan) string))
+
(Printf.sprintf "Category: %s" (River.Category.id cat));
+
Fmt.pr "%a@.@." Fmt.(styled `Faint string) (String.make 60 '-');
+
Fmt.pr "%a %a@."
+
Fmt.(styled `Faint string) "Name: "
+
Fmt.(styled `Green string) (River.Category.name cat);
+
Fmt.pr "%a %a@.@."
+
Fmt.(styled `Faint string) "Description:"
+
Fmt.string (Option.value (River.Category.description cat) ~default:"(none)");
+
+
let post_ids = River.State.get_posts_by_category state ~category_id:id in
+
Fmt.pr "%a@."
+
Fmt.(styled `Bold string)
+
(Printf.sprintf "Posts (%d)" (List.length post_ids));
+
Fmt.pr "%a@." Fmt.(styled `Faint string) (String.make 60 '-');
+
if post_ids = [] then
+
Fmt.pr "%a@.@."
+
Fmt.(styled `Faint string)
+
" No posts tagged with this category."
+
else begin
+
List.iter (fun post_id ->
+
Fmt.pr " %a %a@."
+
Fmt.(styled `Faint string) "•"
+
Fmt.(styled (`Fg `Blue) string) post_id
+
) post_ids;
+
Fmt.pr "@."
+
end;
+
0
+
end
+
+
(* Post category tagging commands *)
+
module Post_category = struct
+
let add state ~post_id ~category_id =
+
match River.State.add_post_category state ~post_id ~category_id with
+
| Ok () ->
+
Fmt.pr "@.%a Post %a tagged with category %a@.@."
+
Fmt.(styled (`Fg `Green) string) "✓"
+
Fmt.(styled `Bold string) post_id
+
Fmt.(styled `Bold string) category_id;
+
0
+
| Error err ->
+
Fmt.pr "@.%a Failed to tag post: %s@.@."
+
Fmt.(styled (`Fg `Red) string) "✗"
+
err;
+
1
+
+
let remove state ~post_id ~category_id =
+
match River.State.remove_post_category state ~post_id ~category_id with
+
| Ok () ->
+
Fmt.pr "@.%a Category %a removed from post %a@.@."
+
Fmt.(styled (`Fg `Green) string) "✓"
+
Fmt.(styled `Bold string) category_id
+
Fmt.(styled `Bold string) post_id;
+
0
+
| Error err ->
+
Fmt.pr "@.%a Failed to remove category from post: %s@.@."
+
Fmt.(styled (`Fg `Red) string) "✗"
+
err;
+
1
+
+
let list state ~post_id =
+
let category_ids = River.State.get_post_categories state ~post_id in
+
if category_ids = [] then begin
+
Fmt.pr "@.%a@.@."
+
Fmt.(styled `Yellow string)
+
(Printf.sprintf "Post %s has no categories assigned." post_id)
+
end else begin
+
Fmt.pr "@.%a@."
+
Fmt.(styled `Bold (styled (`Fg `Cyan) string))
+
(Printf.sprintf "Categories for post: %s" post_id);
+
Fmt.pr "%a@.@." Fmt.(styled `Faint string) (String.make 60 '-');
+
List.iter (fun cat_id ->
+
match River.State.get_category state ~id:cat_id with
+
| Some cat ->
+
Fmt.pr "%a %a@.@."
+
Fmt.(styled `Bold (styled (`Fg `Blue) string)) cat_id
+
Fmt.(styled `Green string) (River.Category.name cat)
+
| None ->
+
Fmt.pr "%a %a@.@."
+
Fmt.(styled `Bold (styled (`Fg `Blue) string)) cat_id
+
Fmt.(styled `Faint string) "(category not found)"
+
) category_ids
+
end;
+
0
+
end
+
+
(* Cmdliner terms for category commands *)
+
let category_list =
+
Term.(const (fun env _xdg _profile ->
+
let state = River.State.create env ~app_name:"river" in
+
Category.list state
+
))
+
+
let category_add =
+
let id_arg = Arg.(required & pos 0 (some string) None & info [] ~docv:"ID" ~doc:"Category ID (e.g., 'ocaml-projects')") in
+
let name_arg = Arg.(required & pos 1 (some string) None & info [] ~docv:"NAME" ~doc:"Category display name") in
+
let description_arg = Arg.(value & opt (some string) None & info ["d"; "description"] ~docv:"DESC" ~doc:"Category description") in
+
Term.(const (fun id name description env _xdg _profile ->
+
let state = River.State.create env ~app_name:"river" in
+
Category.add state ~id ~name ?description ()
+
) $ id_arg $ name_arg $ description_arg)
+
+
let category_remove =
+
let id_arg = Arg.(required & pos 0 (some string) None & info [] ~docv:"ID" ~doc:"Category ID to remove") in
+
Term.(const (fun id env _xdg _profile ->
+
let state = River.State.create env ~app_name:"river" in
+
Category.remove state ~id
+
) $ id_arg)
+
+
let category_show =
+
let id_arg = Arg.(required & pos 0 (some string) None & info [] ~docv:"ID" ~doc:"Category ID to show") in
+
Term.(const (fun id env _xdg _profile ->
+
let state = River.State.create env ~app_name:"river" in
+
Category.show state ~id
+
) $ id_arg)
+
+
let category_cmd =
+
let doc = "Manage custom categories" in
+
let info = Cmd.info "category" ~doc in
+
let list_cmd = Eiocmd.run ~use_keyeio:false ~info:(Cmd.info "list" ~doc:"List all categories") ~app_name:"river" ~service:"river" category_list in
+
let add_cmd = Eiocmd.run ~use_keyeio:false ~info:(Cmd.info "add" ~doc:"Add a new category") ~app_name:"river" ~service:"river" category_add in
+
let remove_cmd = Eiocmd.run ~use_keyeio:false ~info:(Cmd.info "remove" ~doc:"Remove a category") ~app_name:"river" ~service:"river" category_remove in
+
let show_cmd = Eiocmd.run ~use_keyeio:false ~info:(Cmd.info "show" ~doc:"Show category details") ~app_name:"river" ~service:"river" category_show in
+
Cmd.group info [list_cmd; add_cmd; remove_cmd; show_cmd]
+
+
(* Cmdliner terms for post category tagging commands *)
+
let tag_add =
+
let post_id_arg = Arg.(required & pos 0 (some string) None & info [] ~docv:"POST_ID" ~doc:"Post ID to tag") in
+
let category_id_arg = Arg.(required & pos 1 (some string) None & info [] ~docv:"CATEGORY_ID" ~doc:"Category ID") in
+
Term.(const (fun post_id category_id env _xdg _profile ->
+
let state = River.State.create env ~app_name:"river" in
+
Post_category.add state ~post_id ~category_id
+
) $ post_id_arg $ category_id_arg)
+
+
let tag_remove =
+
let post_id_arg = Arg.(required & pos 0 (some string) None & info [] ~docv:"POST_ID" ~doc:"Post ID") in
+
let category_id_arg = Arg.(required & pos 1 (some string) None & info [] ~docv:"CATEGORY_ID" ~doc:"Category ID to remove") in
+
Term.(const (fun post_id category_id env _xdg _profile ->
+
let state = River.State.create env ~app_name:"river" in
+
Post_category.remove state ~post_id ~category_id
+
) $ post_id_arg $ category_id_arg)
+
+
let tag_list =
+
let post_id_arg = Arg.(required & pos 0 (some string) None & info [] ~docv:"POST_ID" ~doc:"Post ID") in
+
Term.(const (fun post_id env _xdg _profile ->
+
let state = River.State.create env ~app_name:"river" in
+
Post_category.list state ~post_id
+
) $ post_id_arg)
+
+
let tag_cmd =
+
let doc = "Manage post category tags" in
+
let info = Cmd.info "tag" ~doc in
+
let add_cmd = Eiocmd.run ~use_keyeio:false ~info:(Cmd.info "add" ~doc:"Tag a post with a category") ~app_name:"river" ~service:"river" tag_add in
+
let remove_cmd = Eiocmd.run ~use_keyeio:false ~info:(Cmd.info "remove" ~doc:"Remove a category from a post") ~app_name:"river" ~service:"river" tag_remove in
+
let list_cmd = Eiocmd.run ~use_keyeio:false ~info:(Cmd.info "list" ~doc:"List categories for a post") ~app_name:"river" ~service:"river" tag_list in
+
Cmd.group info [add_cmd; remove_cmd; list_cmd]
+
let main_cmd =
let doc = "River feed management CLI" in
let main_info = Cmd.info "river-cli" ~version:"1.0" ~doc in
···
~service:"river"
html
in
+
Cmd.group main_info [user_cmd; sync_cmd; list_cmd; info_cmd; merge_cmd; html_cmd; category_cmd; tag_cmd]
+58
stack/river/cmd/river_cmd.mli
···
Reads: format (atom|jsonfeed), title, limit from command-line arguments.
Calls: [River.State.export_merged_feed] *)
(** {2 Main Command} *)
val main_cmd : int Cmd.t
···
Reads: format (atom|jsonfeed), title, limit from command-line arguments.
Calls: [River.State.export_merged_feed] *)
+
(** {2 Category Management Commands} *)
+
+
val category_list :
+
(Eio_unix.Stdenv.base -> Xdge.t -> Keyeio.Profile.t -> int) Term.t
+
(** [category_list] command term for listing all categories.
+
+
Calls: [River.State.list_categories] *)
+
+
val category_add :
+
(Eio_unix.Stdenv.base -> Xdge.t -> Keyeio.Profile.t -> int) Term.t
+
(** [category_add] command term for adding a category.
+
+
Reads: id, name, optional description from command-line arguments.
+
Calls: [River.State.add_category] *)
+
+
val category_remove :
+
(Eio_unix.Stdenv.base -> Xdge.t -> Keyeio.Profile.t -> int) Term.t
+
(** [category_remove] command term for removing a category.
+
+
Reads: id from command-line arguments.
+
Calls: [River.State.remove_category] *)
+
+
val category_show :
+
(Eio_unix.Stdenv.base -> Xdge.t -> Keyeio.Profile.t -> int) Term.t
+
(** [category_show] command term for showing category details.
+
+
Reads: id from command-line arguments.
+
Calls: [River.State.get_category], [River.State.get_posts_by_category] *)
+
+
val category_cmd : int Cmd.t
+
(** [category_cmd] is the category management command group. *)
+
+
(** {2 Post Tagging Commands} *)
+
+
val tag_add :
+
(Eio_unix.Stdenv.base -> Xdge.t -> Keyeio.Profile.t -> int) Term.t
+
(** [tag_add] command term for tagging a post with a category.
+
+
Reads: post_id, category_id from command-line arguments.
+
Calls: [River.State.add_post_category] *)
+
+
val tag_remove :
+
(Eio_unix.Stdenv.base -> Xdge.t -> Keyeio.Profile.t -> int) Term.t
+
(** [tag_remove] command term for removing a category from a post.
+
+
Reads: post_id, category_id from command-line arguments.
+
Calls: [River.State.remove_post_category] *)
+
+
val tag_list :
+
(Eio_unix.Stdenv.base -> Xdge.t -> Keyeio.Profile.t -> int) Term.t
+
(** [tag_list] command term for listing categories assigned to a post.
+
+
Reads: post_id from command-line arguments.
+
Calls: [River.State.get_post_categories] *)
+
+
val tag_cmd : int Cmd.t
+
(** [tag_cmd] is the post tagging command group. *)
+
(** {2 Main Command} *)
val main_cmd : int Cmd.t