@@ -2719,9 +2719,9 @@ async fn handle_vigil_command(action: &crate::cli::VigilAction) -> anyhow::Resul
27192719
27202720 match action {
27212721 crate :: cli:: VigilAction :: List => {
2722- let vigils = config:: load ( ) . vigils . unwrap_or_default ( ) ;
2723- println ! ( "Vigils (from config) :" ) ;
2724- for v in & vigils {
2722+ let vigils = collect_vigils_for_list ( & paths , config:: load ( ) . vigils . unwrap_or_default ( ) ) ;
2723+ println ! ( "Vigils:" ) ;
2724+ for ( v , status ) in & vigils {
27252725 let trigger = match & v. trigger {
27262726 crate :: config:: VigilTrigger :: Toll { interval_secs } => {
27272727 format ! ( "toll every {interval_secs}s" )
@@ -2746,8 +2746,10 @@ async fn handle_vigil_command(action: &crate::cli::VigilAction) -> anyhow::Resul
27462746 v. prompt . clone ( )
27472747 } ;
27482748 println ! (
2749- " {} - {trigger} - reap every {}s - prompt: {prompt}" ,
2750- v. name, v. reap_interval_secs
2749+ " {} - {trigger} - reap every {}s - {} - prompt: {prompt}" ,
2750+ v. name,
2751+ v. reap_interval_secs,
2752+ status. as_str( )
27512753 ) ;
27522754 }
27532755 if vigils. is_empty ( ) {
@@ -2772,31 +2774,29 @@ async fn handle_vigil_command(action: &crate::cli::VigilAction) -> anyhow::Resul
27722774 }
27732775 crate :: cli:: VigilAction :: Remove { name } => {
27742776 let store = VigilStore :: open ( & paths) . map_err ( |e| anyhow:: anyhow!( "{e}" ) ) ?;
2775- match store. remove ( name) {
2776- Ok ( ( ) ) => println ! ( "Removed vigil '{name}'." ) ,
2777- Err ( e) => eprintln ! ( "{e}" ) ,
2778- }
2777+ store. remove ( name) . map_err ( |e| anyhow:: anyhow!( "{e}" ) ) ?;
2778+ println ! ( "Removed vigil '{name}'." ) ;
27792779 }
27802780 crate :: cli:: VigilAction :: Pause { name } => {
27812781 let store = VigilStore :: open ( & paths) . map_err ( |e| anyhow:: anyhow!( "{e}" ) ) ?;
2782- match store. set_status ( name , VigilStatus :: Paused ) {
2783- Ok ( ( ) ) => println ! ( " Paused vigil '{name}'." ) ,
2784- Err ( e ) => eprintln ! ( "{e}" ) ,
2785- }
2782+ store
2783+ . set_status ( name , VigilStatus :: Paused )
2784+ . map_err ( |e| anyhow :: anyhow !( "{e}" ) ) ? ;
2785+ println ! ( "Paused vigil '{name}'." ) ;
27862786 }
27872787 crate :: cli:: VigilAction :: Resume { name } => {
27882788 let store = VigilStore :: open ( & paths) . map_err ( |e| anyhow:: anyhow!( "{e}" ) ) ?;
2789- match store. set_status ( name , VigilStatus :: Active ) {
2790- Ok ( ( ) ) => println ! ( "Resumed vigil '{ name}'." ) ,
2791- Err ( e ) => eprintln ! ( "{e}" ) ,
2792- }
2789+ store
2790+ . set_status ( name, VigilStatus :: Active )
2791+ . map_err ( |e| anyhow :: anyhow !( "{e}" ) ) ? ;
2792+ println ! ( "Resumed vigil '{name}'." ) ;
27932793 }
27942794 crate :: cli:: VigilAction :: Rest { name } => {
27952795 let store = VigilStore :: open ( & paths) . map_err ( |e| anyhow:: anyhow!( "{e}" ) ) ?;
2796- match store. set_status ( name , VigilStatus :: Resting ) {
2797- Ok ( ( ) ) => println ! ( "vigil '{ name}' resting (will sleep until next trigger)." ) ,
2798- Err ( e ) => eprintln ! ( "{e}" ) ,
2799- }
2796+ store
2797+ . set_status ( name, VigilStatus :: Resting )
2798+ . map_err ( |e| anyhow :: anyhow !( "{e}" ) ) ? ;
2799+ println ! ( "vigil '{name}' resting (will sleep until next trigger)." ) ;
28002800 }
28012801 }
28022802 Ok ( ( ) )
@@ -2809,23 +2809,38 @@ fn build_vigil_entry(
28092809 trigger : & crate :: cli:: VigilAddTrigger ,
28102810 args : & [ String ] ,
28112811) -> anyhow:: Result < crate :: config:: VigilEntry > {
2812- use crate :: config:: { VigilEntry , VigilRite , VigilTrigger } ;
2813-
2814- let parsed: std:: collections:: HashMap < String , String > = args
2815- . iter ( )
2816- . filter_map ( |a| a. split_once ( '=' ) )
2817- . map ( |( k, v) | ( k. to_string ( ) , v. to_string ( ) ) )
2818- . collect ( ) ;
2812+ use crate :: config:: { SocketMode , VigilEntry , VigilRite , VigilTrigger } ;
2813+
2814+ // Parse key=value args strictly: a keyless arg is a typo, not a value.
2815+ let mut parsed: std:: collections:: HashMap < String , String > = std:: collections:: HashMap :: new ( ) ;
2816+ for arg in args {
2817+ let ( key, value) = arg
2818+ . split_once ( '=' )
2819+ . ok_or_else ( || anyhow:: anyhow!( "invalid vigil arg '{arg}': expected key=value" ) ) ?;
2820+ parsed. insert ( key. to_string ( ) , value. to_string ( ) ) ;
2821+ }
2822+
2823+ let known: & [ & str ] = match trigger {
2824+ crate :: cli:: VigilAddTrigger :: Toll => & [ "interval_secs" , "reap_interval_secs" , "prompt" ] ,
2825+ crate :: cli:: VigilAddTrigger :: Watcher => & [ "path" , "reap_interval_secs" , "prompt" ] ,
2826+ crate :: cli:: VigilAddTrigger :: Harbinger => & [
2827+ "address" ,
2828+ "protocol" ,
2829+ "socket_mode" ,
2830+ "reap_interval_secs" ,
2831+ "prompt" ,
2832+ ] ,
2833+ } ;
2834+ for key in parsed. keys ( ) {
2835+ if !known. contains ( & key. as_str ( ) ) {
2836+ anyhow:: bail!( "unknown vigil arg '{key}' for {trigger:?}" ) ;
2837+ }
2838+ }
28192839
28202840 let trigger = match trigger {
28212841 crate :: cli:: VigilAddTrigger :: Toll => {
2822- let secs = parsed
2823- . get ( "interval_secs" )
2824- . and_then ( |v| v. parse ( ) . ok ( ) )
2825- . unwrap_or ( 30 ) ;
2826- VigilTrigger :: Toll {
2827- interval_secs : secs,
2828- }
2842+ let interval_secs = parse_positive_secs ( & parsed, "interval_secs" , 30 ) ?;
2843+ VigilTrigger :: Toll { interval_secs }
28292844 }
28302845 crate :: cli:: VigilAddTrigger :: Watcher => {
28312846 let path = parsed
@@ -2840,19 +2855,28 @@ fn build_vigil_entry(
28402855 . cloned ( )
28412856 . unwrap_or_else ( || "127.0.0.1:9000" . to_string ( ) ) ;
28422857 let protocol = parsed. get ( "protocol" ) . cloned ( ) . unwrap_or_default ( ) ;
2858+ // The flat key=value CLI cannot express a commands map, so a
2859+ // CLI-added harbinger is template mode. Commands-mode harbingers
2860+ // must come from config or a .dirge/vigils/*.json file.
2861+ let socket_mode = match parsed. get ( "socket_mode" ) . map ( String :: as_str) {
2862+ None | Some ( "template" ) => SocketMode :: Template ,
2863+ Some ( "commands" ) => anyhow:: bail!(
2864+ "commands-mode harbingers need a commands map; define '{name}' in config or a vigil JSON file instead"
2865+ ) ,
2866+ Some ( other) => {
2867+ anyhow:: bail!( "invalid socket_mode '{other}': expected template or commands" )
2868+ }
2869+ } ;
28432870 VigilTrigger :: Harbinger {
28442871 address,
28452872 protocol,
2846- socket_mode : crate :: config :: SocketMode :: Commands ,
2873+ socket_mode,
28472874 commands : std:: collections:: HashMap :: new ( ) ,
28482875 }
28492876 }
28502877 } ;
28512878
2852- let reap_interval_secs = parsed
2853- . get ( "reap_interval_secs" )
2854- . and_then ( |v| v. parse ( ) . ok ( ) )
2855- . unwrap_or ( 30 ) ;
2879+ let reap_interval_secs = parse_positive_secs ( & parsed, "reap_interval_secs" , 30 ) ?;
28562880
28572881 let prompt = parsed. get ( "prompt" ) . cloned ( ) . unwrap_or_default ( ) ;
28582882
@@ -2868,3 +2892,75 @@ fn build_vigil_entry(
28682892 } ) ,
28692893 } )
28702894}
2895+
2896+ /// Parse a positive-integer seconds arg, rejecting zero and non-numeric
2897+ /// values. Zero trips tokio's non-zero interval assert (the trigger dies
2898+ /// silently) and a zero reap interval tight-loops the reaper.
2899+ #[ cfg( feature = "vigil" ) ]
2900+ fn parse_positive_secs (
2901+ parsed : & std:: collections:: HashMap < String , String > ,
2902+ key : & str ,
2903+ default : u64 ,
2904+ ) -> anyhow:: Result < u64 > {
2905+ match parsed. get ( key) {
2906+ None => Ok ( default) ,
2907+ Some ( raw) => {
2908+ let secs: u64 = raw
2909+ . parse ( )
2910+ . map_err ( |_| anyhow:: anyhow!( "{key} must be a positive integer, got '{raw}'" ) ) ?;
2911+ if secs == 0 {
2912+ anyhow:: bail!( "{key} must be greater than zero" ) ;
2913+ }
2914+ Ok ( secs)
2915+ }
2916+ }
2917+ }
2918+
2919+ /// Enumerate vigils for `dirge vigil list`, merged from config and the SQLite
2920+ /// store by name. Config entries win on name collision; the store is
2921+ /// authoritative for status, so a config vigil paused or rested via the CLI
2922+ /// still shows its state. Entries only in the store (added via
2923+ /// `dirge vigil add`) show up too.
2924+ #[ cfg( feature = "vigil" ) ]
2925+ fn collect_vigils_for_list (
2926+ paths : & crate :: extras:: dirge_paths:: ProjectPaths ,
2927+ config_vigils : Vec < crate :: config:: VigilEntry > ,
2928+ ) -> Vec < (
2929+ crate :: config:: VigilEntry ,
2930+ crate :: extras:: vigil_db:: VigilStatus ,
2931+ ) > {
2932+ use crate :: config:: VigilEntry ;
2933+ use crate :: extras:: vigil_db:: { VigilStatus , VigilStore } ;
2934+ use std:: collections:: HashMap ;
2935+
2936+ let mut status_by_name: HashMap < String , VigilStatus > = HashMap :: new ( ) ;
2937+ let mut entry_by_name: HashMap < String , VigilEntry > = HashMap :: new ( ) ;
2938+
2939+ // Store first (lowest entry precedence; authoritative for status).
2940+ if let Ok ( store) = VigilStore :: open ( paths) {
2941+ for row in store. list_all ( ) . unwrap_or_default ( ) {
2942+ status_by_name. insert ( row. name . clone ( ) , row. status ) ;
2943+ if let Ok ( entry) = serde_json:: from_str :: < VigilEntry > ( & row. payload_json ) {
2944+ entry_by_name. insert ( row. name . clone ( ) , entry) ;
2945+ }
2946+ }
2947+ }
2948+
2949+ // Config wins over store on name collision.
2950+ for entry in config_vigils {
2951+ entry_by_name. insert ( entry. name . clone ( ) , entry) ;
2952+ }
2953+
2954+ let mut out: Vec < ( VigilEntry , VigilStatus ) > = entry_by_name
2955+ . into_iter ( )
2956+ . map ( |( name, entry) | {
2957+ let status = status_by_name
2958+ . get ( & name)
2959+ . copied ( )
2960+ . unwrap_or ( VigilStatus :: Active ) ;
2961+ ( entry, status)
2962+ } )
2963+ . collect ( ) ;
2964+ out. sort_by ( |a, b| a. 0 . name . cmp ( & b. 0 . name ) ) ;
2965+ out
2966+ }
0 commit comments