Files
VERSE ebde9ba68f refactor(commands): separate commands and its core into two folders (#32)
- I think it would be better if one for commands only, one for the spec
handling core logic so people won't be confused by the core logic inside
the commands folder (cleaner I guess)
- Regen docs
2026-07-05 11:27:30 +07:00

203 lines
5.5 KiB
Go

package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
)
type SpecInfo struct {
Name string
Description string
File string
}
type Section struct {
Cat string
Title string
Specs []SpecInfo
}
type CategoryMeta struct {
ID string `json:"id"`
Title string `json:"title"`
}
type CategoriesConfig struct {
Categories []CategoryMeta `json:"categories"`
}
func main() {
commandsDir := "commands"
if len(os.Args) > 1 {
commandsDir = os.Args[1]
} else if _, err := os.Stat(commandsDir); os.IsNotExist(err) {
if _, err2 := os.Stat("../commands"); err2 == nil {
commandsDir = "../commands"
}
}
metaFile := filepath.Join(commandsDir, "categories.json")
metaData, err := os.ReadFile(metaFile)
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading %s: %v\n", metaFile, err)
os.Exit(1)
}
var config CategoriesConfig
if unmarshalErr := json.Unmarshal(metaData, &config); unmarshalErr != nil {
fmt.Fprintf(os.Stderr, "Error parsing %s: %v\n", metaFile, unmarshalErr)
os.Exit(1)
}
orderMap := make(map[string]int)
titleMap := make(map[string]string)
for idx, cat := range config.Categories {
orderMap[cat.ID] = idx
titleMap[cat.ID] = cat.Title
}
entries, err := os.ReadDir(commandsDir)
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading commands dir %s: %v\n", commandsDir, err)
os.Exit(1)
}
nameRe := regexp.MustCompile(`Name:\s*["'` + "`" + `]([^"'` + "`" + `]+)["'` + "`" + `]`)
descRe := regexp.MustCompile(`Description:\s*["'` + "`" + `]([^"'` + "`" + `]*)["'` + "`" + `]`)
var sections []Section
totalCommands := 0
for _, entry := range entries {
if !entry.IsDir() {
continue
}
cat := entry.Name()
if cat == "core" || strings.HasPrefix(cat, ".") || strings.HasPrefix(cat, "_") {
continue
}
title, ok := titleMap[cat]
if !ok {
if len(cat) <= 3 {
title = strings.ToUpper(cat) + " Tools"
} else {
title = strings.ToUpper(cat[:1]) + cat[1:] + " Tools"
}
}
dirPath := filepath.Join(commandsDir, cat)
files, readDirErr := os.ReadDir(dirPath)
if readDirErr != nil {
continue
}
var specs []SpecInfo
seen := make(map[string]bool)
for _, f := range files {
if f.IsDir() || !strings.HasSuffix(f.Name(), ".go") || strings.HasSuffix(f.Name(), "_test.go") {
continue
}
content, readFileErr := os.ReadFile(filepath.Join(dirPath, f.Name()))
if readFileErr != nil {
continue
}
blocks := strings.Split(string(content), "spec.Register(")
for i := 1; i < len(blocks); i++ {
block := blocks[i]
nameMatch := nameRe.FindStringSubmatch(block)
if nameMatch == nil {
continue
}
name := nameMatch[1]
if seen[name] {
continue
}
seen[name] = true
desc := "(no description)"
descMatch := descRe.FindStringSubmatch(block)
if descMatch != nil && descMatch[1] != "" {
desc = descMatch[1]
}
desc = strings.ReplaceAll(desc, "\n", " ")
desc = strings.ReplaceAll(desc, "|", "\\|")
specs = append(specs, SpecInfo{
Name: name,
Description: desc,
File: f.Name(),
})
}
}
if len(specs) == 0 {
continue
}
sort.Slice(specs, func(i, j int) bool {
return strings.Compare(specs[i].Name, specs[j].Name) < 0
})
totalCommands += len(specs)
sections = append(sections, Section{
Cat: cat,
Title: title,
Specs: specs,
})
}
sort.Slice(sections, func(i, j int) bool {
idxI, okI := orderMap[sections[i].Cat]
idxJ, okJ := orderMap[sections[j].Cat]
if okI && okJ {
return idxI < idxJ
}
if okI {
return true
}
if okJ {
return false
}
return strings.Compare(sections[i].Cat, sections[j].Cat) < 0
})
var sb strings.Builder
sb.WriteString("# Iris commands\n\n")
sb.WriteString("This directory contains all modular CLI command specifications and autocompletion definitions supported by **Iris**. Every command is defined as a `spec.Spec` registered via package `init()` functions and grouped into category subdirectories.\n\n")
sb.WriteString("The top-level **[`spec/`](../spec)** package is the primary engine package. It implements the underlying command registry (`spec.Registry`), data structures (`Spec`, `Arg`, `Flag`), dynamic generators, and autocompletion matching logic. The **[`all.go`](./all.go)** file anonymously imports all category subpackages to trigger their initialization and register all available commands at startup.\n\n")
sb.WriteString("## Overview\n\n")
fmt.Fprintf(&sb, "Currently, Iris natively supports **%d** top-level CLI commands across **%d** categories:\n\n", totalCommands, len(sections))
for _, sec := range sections {
fmt.Fprintf(&sb, "- [%s (`%s/`)](#%s): **%d** commands\n", sec.Title, sec.Cat, sec.Cat, len(sec.Specs))
}
sb.WriteString("\n---\n\n")
for _, sec := range sections {
fmt.Fprintf(&sb, "<a id=\"%s\"></a>\n", sec.Cat)
fmt.Fprintf(&sb, "## %s (`%s/`)\n\n", sec.Title, sec.Cat)
sb.WriteString("| Command | Description | Source File |\n")
sb.WriteString("| :--- | :--- | :--- |\n")
for _, spec := range sec.Specs {
fmt.Fprintf(&sb, "| **`%s`** | %s | [`%s`](./%s/%s) |\n", spec.Name, spec.Description, spec.File, sec.Cat, spec.File)
}
sb.WriteString("\n")
}
outputFile := filepath.Join(commandsDir, "README.md")
if writeErr := os.WriteFile(outputFile, []byte(sb.String()), 0644); writeErr != nil {
fmt.Fprintf(os.Stderr, "Error writing %s: %v\n", outputFile, writeErr)
os.Exit(1)
}
fmt.Printf("Generated %s with %d commands across %d categories.\n", outputFile, totalCommands, len(sections))
}