package help import ( "io/fs" "iter" "maps" "os" "path/filepath" "slices" "strings" log "forge.lthn.ai/core/go-log" ) // Catalog manages help topics. type Catalog struct { topics map[string]*Topic index *searchIndex } // DefaultCatalog returns a catalog with built-in topics. func DefaultCatalog() *Catalog { c := &Catalog{ topics: make(map[string]*Topic), index: newSearchIndex(), } // Add default topics c.Add(&Topic{ ID: "getting-started", Title: "Getting Started", Content: `# Getting Started Welcome to Core! This CLI tool helps you manage development workflows. ## Common Commands - core dev: Development workflows - core setup: Setup repository - core doctor: Check environment health - core test: Run tests ## Next Steps Run 'core help ' to learn more about a specific topic. `, }) c.Add(&Topic{ ID: "config", Title: "Configuration", Content: `# Configuration Core is configured via environment variables and config files. ## Environment Variables - CORE_DEBUG: Enable debug logging - GITHUB_TOKEN: GitHub API token ## Config Files Config is stored in ~/.core/config.yaml `, }) return c } // Add adds a topic to the catalog. func (c *Catalog) Add(t *Topic) { c.topics[t.ID] = t c.index.Add(t) } // List returns all topics. func (c *Catalog) List() []*Topic { var list []*Topic for _, t := range c.topics { list = append(list, t) } return list } // All returns an iterator for all topics. func (c *Catalog) All() iter.Seq[*Topic] { return maps.Values(c.topics) } // Search searches for topics. func (c *Catalog) Search(query string) []*SearchResult { return c.index.Search(query) } // SearchResults returns an iterator for search results. func (c *Catalog) SearchResults(query string) iter.Seq[*SearchResult] { return slices.Values(c.Search(query)) } // LoadContentDir recursively loads all .md files from a directory into a Catalog. func LoadContentDir(dir string) (*Catalog, error) { c := &Catalog{ topics: make(map[string]*Topic), index: newSearchIndex(), } err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { return nil } if !strings.HasSuffix(strings.ToLower(d.Name()), ".md") { return nil } content, err := os.ReadFile(path) if err != nil { return log.E("catalog.LoadContentDir", "reading "+path, err) } topic, err := ParseTopic(path, content) if err != nil { return log.E("catalog.LoadContentDir", "parsing "+path, err) } c.Add(topic) return nil }) if err != nil { return nil, log.E("catalog.LoadContentDir", "walking directory "+dir, err) } return c, nil } // Get returns a topic by ID. func (c *Catalog) Get(id string) (*Topic, error) { t, ok := c.topics[id] if !ok { return nil, log.E("catalog.Get", "topic not found: "+id, nil) } return t, nil }