11use std:: io:: Write ;
22
3+ use clap:: {
4+ Parser ,
5+ Subcommand ,
6+ } ;
37use crossterm:: style:: Color ;
48use crossterm:: {
59 queue,
@@ -87,6 +91,57 @@ Profiles allow you to organize and manage different sets of context files for di
8791 }
8892}
8993
94+ #[ derive( Parser , Debug , Clone ) ]
95+ #[ command( name = "hooks" , disable_help_flag = true , disable_help_subcommand = true ) ]
96+ struct HooksCommand {
97+ #[ command( subcommand) ]
98+ command : HooksSubcommand ,
99+ }
100+
101+ #[ derive( Subcommand , Debug , Clone , Eq , PartialEq ) ]
102+ pub enum HooksSubcommand {
103+ Add {
104+ name : String ,
105+
106+ #[ arg( long, value_parser = [ "per_prompt" , "conversation_start" ] ) ]
107+ r#type : String ,
108+
109+ #[ arg( long, value_parser = clap:: value_parser!( String ) ) ]
110+ command : String ,
111+
112+ #[ arg( long) ]
113+ global : bool ,
114+ } ,
115+ #[ command( name = "rm" ) ]
116+ Remove {
117+ name : String ,
118+
119+ #[ arg( long) ]
120+ global : bool ,
121+ } ,
122+ Enable {
123+ name : String ,
124+
125+ #[ arg( long) ]
126+ global : bool ,
127+ } ,
128+ Disable {
129+ name : String ,
130+
131+ #[ arg( long) ]
132+ global : bool ,
133+ } ,
134+ EnableAll {
135+ #[ arg( long) ]
136+ global : bool ,
137+ } ,
138+ DisableAll {
139+ #[ arg( long) ]
140+ global : bool ,
141+ } ,
142+ Help ,
143+ }
144+
90145#[ derive( Debug , Clone , PartialEq , Eq ) ]
91146pub enum ContextSubcommand {
92147 Show {
@@ -104,6 +159,9 @@ pub enum ContextSubcommand {
104159 Clear {
105160 global : bool ,
106161 } ,
162+ Hooks {
163+ subcommand : Option < HooksSubcommand > ,
164+ } ,
107165 Help ,
108166}
109167
@@ -124,15 +182,43 @@ impl ContextSubcommand {
124182 <black!>--global: Remove specified rules globally</black!>
125183
126184 <em>clear [--global]</em> <black!>Remove all rules from current profile</black!>
127- <black!>--global: Remove global rules</black!>" } ;
185+ <black!>--global: Remove global rules</black!>
186+
187+ <em>hooks</em> <black!>View and manage context hooks</black!>" } ;
128188 const CLEAR_USAGE : & str = "/context clear [--global]" ;
189+ const HOOKS_AVAILABLE_COMMANDS : & str = color_print:: cstr! { "<cyan!>Available subcommands</cyan!>
190+ <em>hooks help</em> <black!>Show an explanation for context hooks commands</black!>
191+
192+ <em>hooks add [--global] <<name>></em> <black!>Add a new command context hook</black!>
193+ <black!>--global: Add to global hooks</black!>
194+ <em>--type <<type>></em> <black!>Type of hook, valid options: `per_prompt` or `conversation_start`</black!>
195+ <em>--command <<command>></em> <black!>Shell command to execute</black!>
196+
197+ <em>hooks rm [--global] <<name>></em> <black!>Remove an existing context hook</black!>
198+ <black!>--global: Remove from global hooks</black!>
199+
200+ <em>hooks enable [--global] <<name>></em> <black!>Enable an existing context hook</black!>
201+ <black!>--global: Enable in global hooks</black!>
202+
203+ <em>hooks disable [--global] <<name>></em> <black!>Disable an existing context hook</black!>
204+ <black!>--global: Disable in global hooks</black!>
205+
206+ <em>hooks enable-all [--global]</em> <black!>Enable all existing context hooks</black!>
207+ <black!>--global: Enable all in global hooks</black!>
208+
209+ <em>hooks disable-all [--global]</em> <black!>Disable all existing context hooks</black!>
210+ <black!>--global: Disable all in global hooks</black!>" } ;
129211 const REMOVE_USAGE : & str = "/context rm [--global] <path1> [path2...]" ;
130212 const SHOW_USAGE : & str = "/context show [--expand]" ;
131213
132214 fn usage_msg ( header : impl AsRef < str > ) -> String {
133215 format ! ( "{}\n \n {}" , header. as_ref( ) , Self :: AVAILABLE_COMMANDS )
134216 }
135217
218+ fn hooks_usage_msg ( header : impl AsRef < str > ) -> String {
219+ format ! ( "{}\n \n {}" , header. as_ref( ) , Self :: HOOKS_AVAILABLE_COMMANDS )
220+ }
221+
136222 pub fn help_text ( ) -> String {
137223 color_print:: cformat!(
138224 r#"
@@ -143,6 +229,9 @@ The files matched by these rules provide Amazon Q with additional information
143229about your project or environment. Adding relevant files helps Q generate
144230more accurate and helpful responses.
145231
232+ In addition to files, you can specify hooks that will run commands and return
233+ the output as context to Amazon Q.
234+
146235{}
147236
148237<cyan!>Notes</cyan!>
@@ -154,6 +243,32 @@ more accurate and helpful responses.
154243 Self :: AVAILABLE_COMMANDS
155244 )
156245 }
246+
247+ pub fn hooks_help_text ( ) -> String {
248+ color_print:: cformat!(
249+ r#"
250+ <magenta,em>(Beta) Context Hooks</magenta,em>
251+
252+ Use context hooks to specify shell commands to run. The output from these
253+ commands will be appended to the prompt to Amazon Q. Hooks can be defined
254+ in global or local profiles.
255+
256+ <cyan!>Usage: /context hooks [SUBCOMMAND]</cyan!>
257+
258+ <cyan!>Description</cyan!>
259+ Show existing global or profile-specific hooks.
260+ Alternatively, specify a subcommand to modify the hooks.
261+
262+ {}
263+
264+ <cyan!>Notes</cyan!>
265+ • Hooks are executed in parallel
266+ • 'conversation_start' hooks run on the first user prompt and are attached once to the conversation history sent to Amazon Q
267+ • 'per_prompt' hooks run on each user prompt and are attached to the prompt
268+ "# ,
269+ Self :: HOOKS_AVAILABLE_COMMANDS
270+ )
271+ }
157272}
158273
159274#[ derive( Debug , Clone , PartialEq , Eq ) ]
@@ -493,6 +608,18 @@ impl Command {
493608 "help" => Self :: Context {
494609 subcommand : ContextSubcommand :: Help ,
495610 } ,
611+ "hooks" => {
612+ if parts. get ( 2 ) . is_none ( ) {
613+ return Ok ( Self :: Context {
614+ subcommand : ContextSubcommand :: Hooks { subcommand : None } ,
615+ } ) ;
616+ } ;
617+
618+ match Self :: parse_hooks ( & parts) {
619+ Ok ( command) => command,
620+ Err ( err) => return Err ( ContextSubcommand :: hooks_usage_msg ( err) ) ,
621+ }
622+ } ,
496623 other => {
497624 return Err ( ContextSubcommand :: usage_msg ( format ! ( "Unknown subcommand '{}'." , other) ) ) ;
498625 } ,
@@ -567,6 +694,27 @@ impl Command {
567694 prompt : input. to_string ( ) ,
568695 } )
569696 }
697+
698+ // NOTE: Here we use clap to parse the hooks subcommand instead of parsing manually
699+ // like the rest of the file.
700+ // Since the hooks subcommand has a lot of options, this makes more sense.
701+ // Ideally, we parse everything with clap instead of trying to do it manually.
702+ fn parse_hooks ( parts : & [ & str ] ) -> Result < Self , String > {
703+ // Skip the first two parts ("/context" and "hooks")
704+ let args = match shlex:: split ( & parts[ 1 ..] . join ( " " ) ) {
705+ Some ( args) => args,
706+ None => return Err ( "Failed to parse arguments" . to_string ( ) ) ,
707+ } ;
708+
709+ // Parse with Clap
710+ HooksCommand :: try_parse_from ( args)
711+ . map ( |hooks_command| Self :: Context {
712+ subcommand : ContextSubcommand :: Hooks {
713+ subcommand : Some ( hooks_command. command ) ,
714+ } ,
715+ } )
716+ . map_err ( |e| e. to_string ( ) )
717+ }
570718}
571719
572720#[ cfg( test) ]
@@ -688,6 +836,66 @@ mod tests {
688836 ( "/issue \" there was an error in the chat\" " , Command :: Issue {
689837 prompt : Some ( "\" there was an error in the chat\" " . to_string ( ) ) ,
690838 } ) ,
839+ (
840+ "/context hooks" ,
841+ context ! ( ContextSubcommand :: Hooks { subcommand: None } ) ,
842+ ) ,
843+ (
844+ "/context hooks add test --type per_prompt --command 'echo 1' --global" ,
845+ context ! ( ContextSubcommand :: Hooks {
846+ subcommand: Some ( HooksSubcommand :: Add {
847+ name: "test" . to_string( ) ,
848+ global: true ,
849+ r#type: "per_prompt" . to_string( ) ,
850+ command: "echo 1" . to_string( )
851+ } )
852+ } ) ,
853+ ) ,
854+ (
855+ "/context hooks rm test --global" ,
856+ context ! ( ContextSubcommand :: Hooks {
857+ subcommand: Some ( HooksSubcommand :: Remove {
858+ name: "test" . to_string( ) ,
859+ global: true
860+ } )
861+ } ) ,
862+ ) ,
863+ (
864+ "/context hooks enable test --global" ,
865+ context ! ( ContextSubcommand :: Hooks {
866+ subcommand: Some ( HooksSubcommand :: Enable {
867+ name: "test" . to_string( ) ,
868+ global: true
869+ } )
870+ } ) ,
871+ ) ,
872+ (
873+ "/context hooks disable test" ,
874+ context ! ( ContextSubcommand :: Hooks {
875+ subcommand: Some ( HooksSubcommand :: Disable {
876+ name: "test" . to_string( ) ,
877+ global: false
878+ } )
879+ } ) ,
880+ ) ,
881+ (
882+ "/context hooks enable-all --global" ,
883+ context ! ( ContextSubcommand :: Hooks {
884+ subcommand: Some ( HooksSubcommand :: EnableAll { global: true } )
885+ } ) ,
886+ ) ,
887+ (
888+ "/context hooks disable-all" ,
889+ context ! ( ContextSubcommand :: Hooks {
890+ subcommand: Some ( HooksSubcommand :: DisableAll { global: false } )
891+ } ) ,
892+ ) ,
893+ (
894+ "/context hooks help" ,
895+ context ! ( ContextSubcommand :: Hooks {
896+ subcommand: Some ( HooksSubcommand :: Help )
897+ } ) ,
898+ ) ,
691899 ] ;
692900
693901 for ( input, parsed) in tests {
0 commit comments