cli/cmd/core/help/cmd.go
Virgil c3f2d6abb7 fix(help): add recovery hint for empty searches
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-02 10:15:13 +00:00

161 lines
3.9 KiB
Go

package help
import (
"bufio"
"fmt"
"strings"
"forge.lthn.ai/core/cli/pkg/cli"
gohelp "forge.lthn.ai/core/go-help"
"github.com/spf13/cobra"
)
var startHelpServer = func(catalog *gohelp.Catalog, addr string) error {
return gohelp.NewServer(catalog, addr).ListenAndServe()
}
func AddHelpCommands(root *cli.Command) {
var searchQuery string
helpCmd := &cli.Command{
Use: "help [topic]",
Short: "Display help documentation",
Args: cobra.RangeArgs(0, 1),
RunE: func(cmd *cli.Command, args []string) error {
catalog := gohelp.DefaultCatalog()
if searchQuery != "" {
return searchHelpTopics(catalog, searchQuery)
}
if len(args) == 0 {
return renderTopicList(catalog.List())
}
topic, err := catalog.Get(args[0])
if err != nil {
if suggestions := catalog.Search(args[0]); len(suggestions) > 0 {
if suggestErr := renderSearchResults(suggestions, args[0]); suggestErr != nil {
return suggestErr
}
cli.Blank()
renderHelpHint(args[0])
return cli.Err("help topic %q not found", args[0])
}
renderHelpHint(args[0])
return cli.Err("help topic %q not found", args[0])
}
renderTopic(topic)
return nil
},
}
searchCmd := &cli.Command{
Use: "search [query]",
Short: "Search help topics",
Args: cobra.ArbitraryArgs,
}
var searchCmdQuery string
searchCmd.Flags().StringVarP(&searchCmdQuery, "query", "q", "", "Search query")
searchCmd.RunE = func(cmd *cli.Command, args []string) error {
catalog := gohelp.DefaultCatalog()
query := strings.TrimSpace(searchCmdQuery)
if query == "" {
query = strings.TrimSpace(strings.Join(args, " "))
}
if query == "" {
renderHelpHint("")
return cli.Err("help search query is required")
}
return searchHelpTopics(catalog, query)
}
var serveAddr string
serveCmd := &cli.Command{
Use: "serve",
Short: "Serve help documentation over HTTP",
Args: cobra.NoArgs,
RunE: func(cmd *cli.Command, args []string) error {
return startHelpServer(gohelp.DefaultCatalog(), serveAddr)
},
}
serveCmd.Flags().StringVar(&serveAddr, "addr", ":8080", "HTTP listen address")
helpCmd.AddCommand(serveCmd)
helpCmd.AddCommand(searchCmd)
helpCmd.Flags().StringVarP(&searchQuery, "search", "s", "", "Search help topics")
root.AddCommand(helpCmd)
}
func searchHelpTopics(catalog *gohelp.Catalog, query string) error {
return renderSearchResults(catalog.Search(query), query)
}
func renderSearchResults(results []*gohelp.SearchResult, query string) error {
if len(results) == 0 {
renderHelpHint(query)
return cli.Err("no help topics matched %q", query)
}
cli.Section("Search Results")
for _, res := range results {
cli.Println(" %s - %s", res.Topic.ID, res.Topic.Title)
if snippet := strings.TrimSpace(res.Snippet); snippet != "" {
cli.Println("%s", cli.DimStr(" "+snippet))
}
}
return nil
}
func renderHelpHint(query string) {
cli.Hint("browse", "core help")
if trimmed := strings.TrimSpace(query); trimmed != "" {
cli.Hint("search", fmt.Sprintf("core help search %q", trimmed))
}
}
func renderTopicList(topics []*gohelp.Topic) error {
if len(topics) == 0 {
return cli.Err("no help topics available")
}
cli.Section("Available Help Topics")
for _, topic := range topics {
cli.Println(" %s - %s", topic.ID, topic.Title)
if summary := topicSummary(topic); summary != "" {
cli.Println("%s", cli.DimStr(" "+summary))
}
}
return nil
}
func topicSummary(topic *gohelp.Topic) string {
if topic == nil {
return ""
}
content := strings.TrimSpace(topic.Content)
if content == "" {
return ""
}
scanner := bufio.NewScanner(strings.NewReader(content))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
return line
}
return ""
}
func renderTopic(t *gohelp.Topic) {
cli.Blank()
cli.Println("%s", cli.TitleStyle.Render(t.Title))
cli.Println("%s", strings.Repeat("-", len(t.Title)))
cli.Blank()
cli.Println("%s", t.Content)
cli.Blank()
}