···
let src = Logs.Src.create "river-cli" ~doc:"River CLI application"
module Log = (val Logs.src_log src : Logs.LOG)
13
-
(* User management commands *)
13
+
(* User display formatting *)
14
+
module User_fmt = struct
15
+
let pp_user_with_handle ppf (handle, fullname) =
16
+
Fmt.pf ppf "%a (%a)"
17
+
Fmt.(styled (`Fg `Cyan) string) handle
18
+
Fmt.(styled `Green string) fullname
21
+
(* User management commands - read-only, users managed in Sortal *)
15
-
let add state ~username ~fullname ~email =
16
-
let user = River.User.make ~username ~fullname ?email () in
17
-
match River.State.create_user state user with
19
-
Fmt.pr "@.%a %a %a@.@."
20
-
Fmt.(styled (`Fg `Green) string) "✓"
21
-
Fmt.(styled `Bold string) "User created:"
22
-
Fmt.(styled (`Fg `Cyan) string) username;
25
-
Fmt.pr "@.%a %s@.@."
26
-
Fmt.(styled (`Fg `Red) string) "✗ Error:"
30
-
let remove state ~username =
31
-
match River.State.delete_user state ~username with
33
-
Fmt.pr "@.%a %a %a@.@."
34
-
Fmt.(styled (`Fg `Green) string) "✓"
35
-
Fmt.(styled `Bold string) "User removed:"
36
-
Fmt.(styled (`Fg `Cyan) string) username;
39
-
Fmt.pr "@.%a %s@.@."
40
-
Fmt.(styled (`Fg `Red) string) "✗ Error:"
let users = River.State.list_users state in
Fmt.(styled `Yellow string)
49
-
"No users found. Use 'river-cli user add' to create one."
28
+
"No users found. Add contacts with feeds to Sortal to see them here."
Fmt.(styled `Bold (styled (`Fg `Cyan) string))
···
76
-
let add_feed state ~username ~name ~url =
77
-
match River.State.get_user state ~username with
79
-
Fmt.pr "@.%a User %a not found@.@."
80
-
Fmt.(styled (`Fg `Red) string) "✗ Error:"
81
-
Fmt.(styled `Bold string) username;
84
-
let source = River.Source.make ~name ~url in
85
-
let user = River.User.add_feed user source in
86
-
(match River.State.update_user state user with
88
-
Fmt.pr "@.%a Feed added to %a@."
89
-
Fmt.(styled (`Fg `Green) string) "✓"
90
-
Fmt.(styled (`Fg `Cyan) string) username;
92
-
Fmt.(styled `Faint string) "Name:"
93
-
Fmt.(styled `Bold string) name;
95
-
Fmt.(styled `Faint string) "URL: "
96
-
Fmt.(styled (`Fg `Blue) string) url;
99
-
Fmt.pr "@.%a %s@.@."
100
-
Fmt.(styled (`Fg `Red) string) "✗ Error:"
104
-
let remove_feed state ~username ~url =
105
-
match River.State.get_user state ~username with
107
-
Fmt.pr "@.%a User %a not found@.@."
108
-
Fmt.(styled (`Fg `Red) string) "✗ Error:"
109
-
Fmt.(styled `Bold string) username;
112
-
let user = River.User.remove_feed user ~url in
113
-
(match River.State.update_user state user with
115
-
Fmt.pr "@.%a Feed removed from %a@.@."
116
-
Fmt.(styled (`Fg `Green) string) "✓"
117
-
Fmt.(styled (`Fg `Cyan) string) username;
120
-
Fmt.pr "@.%a %s@.@."
121
-
Fmt.(styled (`Fg `Red) string) "✗ Error:"
let show state ~username =
match River.State.get_user state ~username with
···
Fmt.(styled `Yellow string)
(Option.value (River.User.last_synced user) ~default:"never");
81
+
(* Quality analysis *)
82
+
(match River.State.analyze_user_quality state ~username with
84
+
let score = River.Quality.quality_score metrics in
85
+
let total = River.Quality.total_entries metrics in
86
+
let score_color, score_label = match score with
87
+
| s when s >= 80.0 -> `Green, "Excellent"
88
+
| s when s >= 60.0 -> `Yellow, "Good"
89
+
| s when s >= 40.0 -> `Magenta, "Fair"
92
+
Fmt.pr "%a %a %.1f/100 %a - %d posts@.@."
93
+
Fmt.(styled `Faint string) "Quality: "
94
+
Fmt.(styled (`Fg score_color) string) "●"
96
+
Fmt.(styled (`Fg score_color) string) (Printf.sprintf "(%s)" score_label)
100
+
Fmt.(styled `Faint string) "Quality: "
101
+
Fmt.(styled `Faint string) "(not synced yet)");
let feeds = River.User.feeds user in
Fmt.(styled `Bold string)
···
Fmt.(styled `Faint string)
160
-
" No feeds configured. Use 'river-cli user add-feed' to add one."
112
+
" No feeds configured for this contact."
···
Fmt.(styled (`Fg `Red) string) "✗"
(* Post listing commands *)
···
(format_text_construct entry.title);
Fmt.pr "%a@.@." Fmt.(styled `Bold string) (String.make 70 '=');
437
-
(* Author and date *)
439
-
match River.State.get_user state ~username with
440
-
| Some user -> River.User.fullname user
390
+
(* Author and date - show handle and full name *)
391
+
(match River.State.get_user state ~username with
393
+
Fmt.pr "%a %a@." Fmt.(styled `Cyan string) "Author:"
394
+
User_fmt.pp_user_with_handle (username, River.User.fullname user)
let (author, _) = entry.authors in
443
-
String.trim author.name
445
-
Fmt.pr "%a %a@." Fmt.(styled `Cyan string) "Author:"
446
-
Fmt.(styled `Green string) author_name;
397
+
Fmt.pr "%a %a@." Fmt.(styled `Cyan string) "Author:"
398
+
Fmt.(styled `Green string) (String.trim author.name));
Fmt.pr "%a %a@." Fmt.(styled `Cyan string) "Published:"
Fmt.(styled `Magenta string) (format_date entry.updated);
Fmt.pr "%a %a@.@." Fmt.(styled `Cyan string) "ID:"
···
Arg.(required & pos 0 (some string) None & info [] ~docv:"USERNAME" ~doc)
520
-
let doc = "Full name of the user" in
521
-
Arg.(required & opt (some string) None & info ["name"; "n"] ~doc)
524
-
let doc = "Email address of the user (optional)" in
525
-
Arg.(value & opt (some string) None & info ["email"; "e"] ~doc)
527
-
let feed_name_arg =
528
-
let doc = "Feed name/label" in
529
-
Arg.(required & opt (some string) None & info ["name"; "n"] ~doc)
532
-
let doc = "Feed URL" in
533
-
Arg.(required & opt (some string) None & info ["url"; "u"] ~doc)
535
-
(* User commands - these don't need network, just filesystem access *)
537
-
Term.(const (fun username fullname email env _xdg _profile ->
538
-
let state = River.State.create env ~app_name:"river" in
539
-
User.add state ~username ~fullname ~email
540
-
) $ username_arg $ fullname_arg $ email_arg)
543
-
Term.(const (fun username env _xdg _profile ->
544
-
let state = River.State.create env ~app_name:"river" in
545
-
User.remove state ~username
471
+
(* User commands - read-only, users managed in Sortal *)
Term.(const (fun env _xdg _profile ->
let state = River.State.create env ~app_name:"river" in
···
User.show state ~username
560
-
let user_add_feed =
561
-
Term.(const (fun username name url env _xdg _profile ->
562
-
let state = River.State.create env ~app_name:"river" in
563
-
User.add_feed state ~username ~name ~url
564
-
) $ username_arg $ feed_name_arg $ feed_url_arg)
566
-
let user_remove_feed =
567
-
Term.(const (fun username url env _xdg _profile ->
568
-
let state = River.State.create env ~app_name:"river" in
569
-
User.remove_feed state ~username ~url
570
-
) $ username_arg $ feed_url_arg)
573
-
let doc = "Manage users" in
485
+
let doc = "View users from Sortal" in
let info = Cmd.info "user" ~doc in
578
-
~info:(Cmd.info "add" ~doc:"Add a new user")
583
-
let user_remove_cmd =
586
-
~info:(Cmd.info "remove" ~doc:"Remove a user")
594
-
~info:(Cmd.info "list" ~doc:"List all users")
490
+
~info:(Cmd.info "list" ~doc:"List all users from Sortal")
···
607
-
let user_add_feed_cmd =
610
-
~info:(Cmd.info "add-feed" ~doc:"Add a feed to a user")
615
-
let user_remove_feed_cmd =
618
-
~info:(Cmd.info "remove-feed" ~doc:"Remove a feed from a user")
629
-
user_remove_feed_cmd;
(* Sync command - needs Eio environment for HTTP requests *)
···
Log.err (fun m -> m "Failed to export merged feed: %s" err);
) $ format_arg $ title_arg $ limit_arg)
708
-
(* Quality command - analyze feed quality *)
711
-
let doc = "Username to analyze" in
712
-
Arg.(required & pos 0 (some string) None & info [] ~docv:"USERNAME" ~doc)
714
-
Term.(const (fun username env _xdg _profile ->
715
-
let state = River.State.create env ~app_name:"river" in
716
-
match River.State.analyze_user_quality state ~username with
718
-
Log.err (fun m -> m "%s" err);
721
-
(* Display quality metrics *)
723
-
Fmt.(styled `Bold (styled (`Fg `Cyan) string))
724
-
(Printf.sprintf "Feed Quality Analysis: %s" username);
725
-
Fmt.pr "%a@.@." Fmt.(styled `Faint string) (String.make 70 '=');
727
-
(* Overall quality score with visual indicator *)
728
-
let score = River.Quality.quality_score metrics in
729
-
let score_color, score_label = match score with
730
-
| s when s >= 80.0 -> `Green, "Excellent"
731
-
| s when s >= 60.0 -> `Yellow, "Good"
732
-
| s when s >= 40.0 -> `Magenta, "Fair"
733
-
| _ -> `Red, "Poor"
735
-
let bar_width = 40 in
736
-
let filled = int_of_float (score /. 100.0 *. float_of_int bar_width) in
737
-
let bar = String.make filled '#' ^ String.make (bar_width - filled) '-' in
739
-
Fmt.(styled `Bold string) "Overall Quality Score";
740
-
Fmt.pr " %a %.1f/100 %a@.@."
741
-
Fmt.(styled (`Fg score_color) string) bar
743
-
Fmt.(styled (`Fg score_color) (styled `Bold string)) (Printf.sprintf "(%s)" score_label);
745
-
(* Entry statistics *)
747
-
Fmt.(styled `Bold string) "📊 Entries:"
748
-
Fmt.(styled (`Fg `Yellow) (styled `Bold string))
749
-
(string_of_int (River.Quality.total_entries metrics));
752
-
(* Completeness metrics with visual indicators *)
753
-
Fmt.pr "%a@." Fmt.(styled `Bold string) "Completeness";
754
-
let total = River.Quality.total_entries metrics in
756
-
float_of_int entries /. float_of_int total *. 100.0
758
-
let show_metric label count =
759
-
let p = pct count in
760
-
let icon, color = match p with
761
-
| p when p >= 90.0 -> "✓", `Green
762
-
| p when p >= 50.0 -> "○", `Yellow
765
-
Fmt.pr " %a %s %3d/%d %a@."
766
-
Fmt.(styled (`Fg color) string) icon
769
-
Fmt.(styled `Faint string) (Printf.sprintf "(%.1f%%)" p)
771
-
show_metric "Content: " (River.Quality.entries_with_content metrics);
772
-
show_metric "Dates: " (River.Quality.entries_with_date metrics);
773
-
show_metric "Authors: " (River.Quality.entries_with_author metrics);
774
-
show_metric "Summaries:" (River.Quality.entries_with_summary metrics);
775
-
show_metric "Tags: " (River.Quality.entries_with_tags metrics);
778
-
(* Content statistics *)
779
-
if River.Quality.entries_with_content metrics > 0 then begin
780
-
Fmt.pr "%a@." Fmt.(styled `Bold string) "Content Statistics";
781
-
Fmt.pr " %a %.0f chars@."
782
-
Fmt.(styled `Faint string) "Average:"
783
-
(River.Quality.avg_content_length metrics);
784
-
Fmt.pr " %a %a ... %a@.@."
785
-
Fmt.(styled `Faint string) "Range: "
786
-
Fmt.(styled (`Fg `Cyan) string) (string_of_int (River.Quality.min_content_length metrics))
787
-
Fmt.(styled (`Fg `Cyan) string) (string_of_int (River.Quality.max_content_length metrics))
790
-
(* Posting frequency *)
791
-
(match River.Quality.posting_frequency_days metrics with
793
-
Fmt.pr "%a@." Fmt.(styled `Bold string) "Posting Frequency";
794
-
let posts_per_week = 7.0 /. freq in
795
-
Fmt.pr " %a %.1f days between posts@."
796
-
Fmt.(styled `Faint string) "Average:"
798
-
Fmt.pr " %a ~%.1f posts/week@.@."
799
-
Fmt.(styled `Faint string) " "
802
-
Fmt.pr "%a@.@." Fmt.(styled `Faint string)
803
-
"Not enough data to calculate posting frequency");
let doc = "River feed management CLI" in
let main_info = Cmd.info "river-cli" ~version:"1.0" ~doc in
···
847
-
~info:(Cmd.info "quality" ~doc:"Analyze feed quality metrics for a user")
852
-
Cmd.group main_info [user_cmd; sync_cmd; list_cmd; info_cmd; merge_cmd; quality_cmd]
618
+
Cmd.group main_info [user_cmd; sync_cmd; list_cmd; info_cmd; merge_cmd]