@@ -2706,9 +2706,9 @@ async fn handle_vigil_command(action: &crate::cli::VigilAction) -> anyhow::Resul
27062706
27072707 match action {
27082708 crate :: cli:: VigilAction :: List => {
2709- let vigils = config:: load ( ) . vigils . unwrap_or_default ( ) ;
2710- println ! ( "Vigils (from config) :" ) ;
2711- for v in & vigils {
2709+ let vigils = collect_vigils_for_list ( & paths , config:: load ( ) . vigils . unwrap_or_default ( ) ) ;
2710+ println ! ( "Vigils:" ) ;
2711+ for ( v , status ) in & vigils {
27122712 let trigger = match & v. trigger {
27132713 crate :: config:: VigilTrigger :: Toll { interval_secs } => {
27142714 format ! ( "toll every {interval_secs}s" )
@@ -2733,8 +2733,10 @@ async fn handle_vigil_command(action: &crate::cli::VigilAction) -> anyhow::Resul
27332733 v. prompt . clone ( )
27342734 } ;
27352735 println ! (
2736- " {} - {trigger} - reap every {}s - prompt: {prompt}" ,
2737- v. name, v. reap_interval_secs
2736+ " {} - {trigger} - reap every {}s - {} - prompt: {prompt}" ,
2737+ v. name,
2738+ v. reap_interval_secs,
2739+ status. as_str( )
27382740 ) ;
27392741 }
27402742 if vigils. is_empty ( ) {
@@ -2759,31 +2761,29 @@ async fn handle_vigil_command(action: &crate::cli::VigilAction) -> anyhow::Resul
27592761 }
27602762 crate :: cli:: VigilAction :: Remove { name } => {
27612763 let store = VigilStore :: open ( & paths) . map_err ( |e| anyhow:: anyhow!( "{e}" ) ) ?;
2762- match store. remove ( name) {
2763- Ok ( ( ) ) => println ! ( "Removed vigil '{name}'." ) ,
2764- Err ( e) => eprintln ! ( "{e}" ) ,
2765- }
2764+ store. remove ( name) . map_err ( |e| anyhow:: anyhow!( "{e}" ) ) ?;
2765+ println ! ( "Removed vigil '{name}'." ) ;
27662766 }
27672767 crate :: cli:: VigilAction :: Pause { name } => {
27682768 let store = VigilStore :: open ( & paths) . map_err ( |e| anyhow:: anyhow!( "{e}" ) ) ?;
2769- match store. set_status ( name , VigilStatus :: Paused ) {
2770- Ok ( ( ) ) => println ! ( " Paused vigil '{name}'." ) ,
2771- Err ( e ) => eprintln ! ( "{e}" ) ,
2772- }
2769+ store
2770+ . set_status ( name , VigilStatus :: Paused )
2771+ . map_err ( |e| anyhow :: anyhow !( "{e}" ) ) ? ;
2772+ println ! ( "Paused vigil '{name}'." ) ;
27732773 }
27742774 crate :: cli:: VigilAction :: Resume { name } => {
27752775 let store = VigilStore :: open ( & paths) . map_err ( |e| anyhow:: anyhow!( "{e}" ) ) ?;
2776- match store. set_status ( name , VigilStatus :: Active ) {
2777- Ok ( ( ) ) => println ! ( "Resumed vigil '{ name}'." ) ,
2778- Err ( e ) => eprintln ! ( "{e}" ) ,
2779- }
2776+ store
2777+ . set_status ( name, VigilStatus :: Active )
2778+ . map_err ( |e| anyhow :: anyhow !( "{e}" ) ) ? ;
2779+ println ! ( "Resumed vigil '{name}'." ) ;
27802780 }
27812781 crate :: cli:: VigilAction :: Rest { name } => {
27822782 let store = VigilStore :: open ( & paths) . map_err ( |e| anyhow:: anyhow!( "{e}" ) ) ?;
2783- match store. set_status ( name , VigilStatus :: Resting ) {
2784- Ok ( ( ) ) => println ! ( "vigil '{ name}' resting (will sleep until next trigger)." ) ,
2785- Err ( e ) => eprintln ! ( "{e}" ) ,
2786- }
2783+ store
2784+ . set_status ( name, VigilStatus :: Resting )
2785+ . map_err ( |e| anyhow :: anyhow !( "{e}" ) ) ? ;
2786+ println ! ( "vigil '{name}' resting (will sleep until next trigger)." ) ;
27872787 }
27882788 }
27892789 Ok ( ( ) )
@@ -2796,23 +2796,38 @@ fn build_vigil_entry(
27962796 trigger : & crate :: cli:: VigilAddTrigger ,
27972797 args : & [ String ] ,
27982798) -> anyhow:: Result < crate :: config:: VigilEntry > {
2799- use crate :: config:: { VigilEntry , VigilRite , VigilTrigger } ;
2800-
2801- let parsed: std:: collections:: HashMap < String , String > = args
2802- . iter ( )
2803- . filter_map ( |a| a. split_once ( '=' ) )
2804- . map ( |( k, v) | ( k. to_string ( ) , v. to_string ( ) ) )
2805- . collect ( ) ;
2799+ use crate :: config:: { SocketMode , VigilEntry , VigilRite , VigilTrigger } ;
2800+
2801+ // Parse key=value args strictly: a keyless arg is a typo, not a value.
2802+ let mut parsed: std:: collections:: HashMap < String , String > = std:: collections:: HashMap :: new ( ) ;
2803+ for arg in args {
2804+ let ( key, value) = arg
2805+ . split_once ( '=' )
2806+ . ok_or_else ( || anyhow:: anyhow!( "invalid vigil arg '{arg}': expected key=value" ) ) ?;
2807+ parsed. insert ( key. to_string ( ) , value. to_string ( ) ) ;
2808+ }
2809+
2810+ let known: & [ & str ] = match trigger {
2811+ crate :: cli:: VigilAddTrigger :: Toll => & [ "interval_secs" , "reap_interval_secs" , "prompt" ] ,
2812+ crate :: cli:: VigilAddTrigger :: Watcher => & [ "path" , "reap_interval_secs" , "prompt" ] ,
2813+ crate :: cli:: VigilAddTrigger :: Harbinger => & [
2814+ "address" ,
2815+ "protocol" ,
2816+ "socket_mode" ,
2817+ "reap_interval_secs" ,
2818+ "prompt" ,
2819+ ] ,
2820+ } ;
2821+ for key in parsed. keys ( ) {
2822+ if !known. contains ( & key. as_str ( ) ) {
2823+ anyhow:: bail!( "unknown vigil arg '{key}' for {trigger:?}" ) ;
2824+ }
2825+ }
28062826
28072827 let trigger = match trigger {
28082828 crate :: cli:: VigilAddTrigger :: Toll => {
2809- let secs = parsed
2810- . get ( "interval_secs" )
2811- . and_then ( |v| v. parse ( ) . ok ( ) )
2812- . unwrap_or ( 30 ) ;
2813- VigilTrigger :: Toll {
2814- interval_secs : secs,
2815- }
2829+ let interval_secs = parse_positive_secs ( & parsed, "interval_secs" , 30 ) ?;
2830+ VigilTrigger :: Toll { interval_secs }
28162831 }
28172832 crate :: cli:: VigilAddTrigger :: Watcher => {
28182833 let path = parsed
@@ -2827,19 +2842,28 @@ fn build_vigil_entry(
28272842 . cloned ( )
28282843 . unwrap_or_else ( || "127.0.0.1:9000" . to_string ( ) ) ;
28292844 let protocol = parsed. get ( "protocol" ) . cloned ( ) . unwrap_or_default ( ) ;
2845+ // The flat key=value CLI cannot express a commands map, so a
2846+ // CLI-added harbinger is template mode. Commands-mode harbingers
2847+ // must come from config or a .dirge/vigils/*.json file.
2848+ let socket_mode = match parsed. get ( "socket_mode" ) . map ( String :: as_str) {
2849+ None | Some ( "template" ) => SocketMode :: Template ,
2850+ Some ( "commands" ) => anyhow:: bail!(
2851+ "commands-mode harbingers need a commands map; define '{name}' in config or a vigil JSON file instead"
2852+ ) ,
2853+ Some ( other) => {
2854+ anyhow:: bail!( "invalid socket_mode '{other}': expected template or commands" )
2855+ }
2856+ } ;
28302857 VigilTrigger :: Harbinger {
28312858 address,
28322859 protocol,
2833- socket_mode : crate :: config :: SocketMode :: Commands ,
2860+ socket_mode,
28342861 commands : std:: collections:: HashMap :: new ( ) ,
28352862 }
28362863 }
28372864 } ;
28382865
2839- let reap_interval_secs = parsed
2840- . get ( "reap_interval_secs" )
2841- . and_then ( |v| v. parse ( ) . ok ( ) )
2842- . unwrap_or ( 30 ) ;
2866+ let reap_interval_secs = parse_positive_secs ( & parsed, "reap_interval_secs" , 30 ) ?;
28432867
28442868 let prompt = parsed. get ( "prompt" ) . cloned ( ) . unwrap_or_default ( ) ;
28452869
@@ -2855,3 +2879,75 @@ fn build_vigil_entry(
28552879 } ) ,
28562880 } )
28572881}
2882+
2883+ /// Parse a positive-integer seconds arg, rejecting zero and non-numeric
2884+ /// values. Zero trips tokio's non-zero interval assert (the trigger dies
2885+ /// silently) and a zero reap interval tight-loops the reaper.
2886+ #[ cfg( feature = "vigil" ) ]
2887+ fn parse_positive_secs (
2888+ parsed : & std:: collections:: HashMap < String , String > ,
2889+ key : & str ,
2890+ default : u64 ,
2891+ ) -> anyhow:: Result < u64 > {
2892+ match parsed. get ( key) {
2893+ None => Ok ( default) ,
2894+ Some ( raw) => {
2895+ let secs: u64 = raw
2896+ . parse ( )
2897+ . map_err ( |_| anyhow:: anyhow!( "{key} must be a positive integer, got '{raw}'" ) ) ?;
2898+ if secs == 0 {
2899+ anyhow:: bail!( "{key} must be greater than zero" ) ;
2900+ }
2901+ Ok ( secs)
2902+ }
2903+ }
2904+ }
2905+
2906+ /// Enumerate vigils for `dirge vigil list`, merged from config and the SQLite
2907+ /// store by name. Config entries win on name collision; the store is
2908+ /// authoritative for status, so a config vigil paused or rested via the CLI
2909+ /// still shows its state. Entries only in the store (added via
2910+ /// `dirge vigil add`) show up too.
2911+ #[ cfg( feature = "vigil" ) ]
2912+ fn collect_vigils_for_list (
2913+ paths : & crate :: extras:: dirge_paths:: ProjectPaths ,
2914+ config_vigils : Vec < crate :: config:: VigilEntry > ,
2915+ ) -> Vec < (
2916+ crate :: config:: VigilEntry ,
2917+ crate :: extras:: vigil_db:: VigilStatus ,
2918+ ) > {
2919+ use crate :: config:: VigilEntry ;
2920+ use crate :: extras:: vigil_db:: { VigilStatus , VigilStore } ;
2921+ use std:: collections:: HashMap ;
2922+
2923+ let mut status_by_name: HashMap < String , VigilStatus > = HashMap :: new ( ) ;
2924+ let mut entry_by_name: HashMap < String , VigilEntry > = HashMap :: new ( ) ;
2925+
2926+ // Store first (lowest entry precedence; authoritative for status).
2927+ if let Ok ( store) = VigilStore :: open ( paths) {
2928+ for row in store. list_all ( ) . unwrap_or_default ( ) {
2929+ status_by_name. insert ( row. name . clone ( ) , row. status ) ;
2930+ if let Ok ( entry) = serde_json:: from_str :: < VigilEntry > ( & row. payload_json ) {
2931+ entry_by_name. insert ( row. name . clone ( ) , entry) ;
2932+ }
2933+ }
2934+ }
2935+
2936+ // Config wins over store on name collision.
2937+ for entry in config_vigils {
2938+ entry_by_name. insert ( entry. name . clone ( ) , entry) ;
2939+ }
2940+
2941+ let mut out: Vec < ( VigilEntry , VigilStatus ) > = entry_by_name
2942+ . into_iter ( )
2943+ . map ( |( name, entry) | {
2944+ let status = status_by_name
2945+ . get ( & name)
2946+ . copied ( )
2947+ . unwrap_or ( VigilStatus :: Active ) ;
2948+ ( entry, status)
2949+ } )
2950+ . collect ( ) ;
2951+ out. sort_by ( |a, b| a. 0 . name . cmp ( & b. 0 . name ) ) ;
2952+ out
2953+ }
0 commit comments