@@ -47,6 +47,7 @@ func helpPrinter(options kong.HelpOptions, ctx *kong.Context) error {
4747
4848 out := rewriteCommandSummaries (buf .String (), ctx .Selected ())
4949 out = injectBuildLine (out )
50+ out = injectExternalPlugins (out , ctx .Selected ())
5051 out = colorizeHelp (out , helpProfile (origStdout , helpColorMode (ctx .Args )))
5152 _ , err := io .WriteString (origStdout , out )
5253 return err
@@ -138,10 +139,16 @@ func colorizeHelp(out string, profile termenv.Profile) string {
138139 }
139140
140141 inCommands := false
142+ inPlugins := false
141143 lines := strings .Split (out , "\n " )
142144 for i , line := range lines {
143145 if line == "Commands:" {
144146 inCommands = true
147+ inPlugins = false
148+ }
149+ if line == "Plugins:" {
150+ inPlugins = true
151+ inCommands = false
145152 }
146153 switch {
147154 case strings .HasPrefix (line , "Usage:" ):
@@ -150,6 +157,8 @@ func colorizeHelp(out string, profile termenv.Profile) string {
150157 lines [i ] = section (line )
151158 case line == "Commands:" :
152159 lines [i ] = section (line )
160+ case line == "Plugins:" :
161+ lines [i ] = section (line )
153162 case line == "Arguments:" :
154163 lines [i ] = section (line )
155164 case strings .HasPrefix (line , "Build:" ) || line == "Config:" :
@@ -160,6 +169,8 @@ func colorizeHelp(out string, profile termenv.Profile) string {
160169 lines [i ] = colorizeCommandSummaryLine (line , cmdName , dim )
161170 case inCommands && strings .HasPrefix (line , " " ) && strings .TrimSpace (line ) != "" :
162171 lines [i ] = " " + dim (strings .TrimPrefix (line , " " ))
172+ case inPlugins && strings .HasPrefix (line , " " ) && strings .TrimSpace (line ) != "" :
173+ lines [i ] = colorizePluginLine (line , cmdName , dim )
163174 }
164175 }
165176 return strings .Join (lines , "\n " )
@@ -190,6 +201,33 @@ func colorizeCommandSummaryLine(line string, cmdName func(string) string, dim fu
190201 return " " + styled + " " + tail
191202}
192203
204+ // colorizePluginLine colorizes a plugin line in the Plugins: section.
205+ // Plugin lines look like: " docs headings List document headings"
206+ func colorizePluginLine (line string , cmdName func (string ) string , dim func (string ) string ) string {
207+ if ! strings .HasPrefix (line , " " ) {
208+ return line
209+ }
210+ rest := strings .TrimPrefix (line , " " )
211+ if rest == "" {
212+ return line
213+ }
214+
215+ // Find where the command name ends (two or more spaces indicate description start)
216+ parts := strings .SplitN (rest , " " , 2 )
217+ name := parts [0 ]
218+ if name == "" {
219+ return line
220+ }
221+
222+ styled := cmdName (name )
223+ if len (parts ) == 1 {
224+ return " " + styled
225+ }
226+
227+ // Description is dimmed
228+ return " " + styled + " " + dim (parts [1 ])
229+ }
230+
193231func rewriteCommandSummaries (out string , selected * kong.Node ) string {
194232 if selected == nil {
195233 return out
@@ -223,3 +261,59 @@ func guessColumns(w io.Writer) int {
223261 }
224262 return 80
225263}
264+
265+ // injectExternalPlugins adds discovered external plugins to help output.
266+ // Plugins are displayed in a separate "Plugins:" section after "Commands:".
267+ //
268+ // Discovery is lazy: only scans PATH when help is requested.
269+ // The --help-oneliner protocol is used to get plugin descriptions.
270+ // Plugins that don't respond within 100ms get no description.
271+ func injectExternalPlugins (out string , selected * kong.Node ) string {
272+ // Only show plugins in root help (not subcommand help)
273+ if selected != nil && selected .Parent != nil {
274+ return out
275+ }
276+
277+ plugins := DiscoverExternalPlugins ()
278+ if len (plugins ) == 0 {
279+ return out
280+ }
281+
282+ // Fetch oneliners (with timeout)
283+ plugins = FetchOneliners (plugins )
284+
285+ // Build plugins section
286+ var sb strings.Builder
287+ sb .WriteString ("\n Plugins:\n " )
288+
289+ // Find max command name length for alignment
290+ maxLen := 0
291+ for _ , p := range plugins {
292+ if len (p .CommandName ()) > maxLen {
293+ maxLen = len (p .CommandName ())
294+ }
295+ }
296+
297+ for _ , p := range plugins {
298+ cmdName := p .CommandName ()
299+ padding := strings .Repeat (" " , maxLen - len (cmdName )+ 2 )
300+ if p .Oneliner != "" {
301+ sb .WriteString (fmt .Sprintf (" %s%s%s\n " , cmdName , padding , p .Oneliner ))
302+ } else {
303+ sb .WriteString (fmt .Sprintf (" %s\n " , cmdName ))
304+ }
305+ }
306+
307+ // Insert before "Run ... --help" line or at end
308+ lines := strings .Split (out , "\n " )
309+ for i , line := range lines {
310+ if strings .HasPrefix (line , "Run \" " ) && strings .HasSuffix (line , " --help\" for more information on a command." ) {
311+ before := strings .Join (lines [:i ], "\n " )
312+ after := strings .Join (lines [i :], "\n " )
313+ return before + sb .String () + after
314+ }
315+ }
316+
317+ // Append at end if no "Run ... --help" line found
318+ return out + sb .String ()
319+ }
0 commit comments