From 8e9831ad7a2c03e34f81fd7a0dc00e6ff34d1cee Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Mon, 4 Mar 2024 09:47:44 -0800 Subject: [PATCH] WIP --- .gitignore | 3 + Makefile | 6 ++ cmd/gendocs/main.go | 68 +++++++++++++ internal/pkg/cmd/command_internal.go | 37 ++++--- internal/pkg/cmd/gen_md.go | 139 +++++++++++++++++++++++++++ 5 files changed, 240 insertions(+), 13 deletions(-) create mode 100644 cmd/gendocs/main.go create mode 100644 internal/pkg/cmd/gen_md.go diff --git a/.gitignore b/.gitignore index d8de4ea9..4565a627 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ tmp/ # bin/ directory bin/* + +# web-docs/ directory +web-docs/* diff --git a/Makefile b/Makefile index af2f881d..2ad779d7 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,12 @@ build: ## Build the HCP CLI binary screenshot: go/install ## Create a screenshot of the HCP CLI @go run github.com/homeport/termshot/cmd/termshot@v0.2.7 -c -f assets/hcp.png -- hcp +.PHONY: gen/docs +gen/docs: build ## Generate the HCP CLI documentation + @mkdir -p web-docs + @rm -rf web-docs/* + @./bin/gendocs -output-dir web-docs/ + .PHONY: go/install go/install: ## Install the HCP CLI binary @go install diff --git a/cmd/gendocs/main.go b/cmd/gendocs/main.go new file mode 100644 index 00000000..c2b16170 --- /dev/null +++ b/cmd/gendocs/main.go @@ -0,0 +1,68 @@ +package main + +import ( + "context" + "flag" + "fmt" + "os" + + "github.com/hashicorp/hcp/internal/commands/hcp" + "github.com/hashicorp/hcp/internal/pkg/cmd" + "github.com/hashicorp/hcp/internal/pkg/format" + "github.com/hashicorp/hcp/internal/pkg/iostreams" + "github.com/hashicorp/hcp/internal/pkg/profile" +) + +func main() { + if err := run(); err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } +} + +func run() error { + // Define the flags + var outputDir string + var linkPrefix string + + flag.StringVar(&outputDir, "output-dir", "docs", "The output directory for the generated documentation") + flag.StringVar(&linkPrefix, "cmd-link-prefix", "hcp/docs/cli/commands/", "Link prefix for the commands") + + // Parse the flags + flag.Parse() + + // Create the command context + l, err := profile.NewLoader() + if err != nil { + return fmt.Errorf("failed to create profile loader: %v", err) + } + + // Create the IO streams and configure for generating markdown + io, err := iostreams.System(context.Background()) + if err != nil { + return fmt.Errorf("failed to create IO streams: %v", err) + } + io.ForceNoColor() + + ctx := &cmd.Context{ + IO: io, + Profile: l.DefaultProfile(), + Output: format.New(io), + ShutdownCtx: context.Background(), + } + + // Get the root command + rootCmd := hcp.NewCmdHcp(ctx) + + // Create the link handler + linkHandler := func(cmd string) string { + return linkPrefix + cmd + } + + // Generate the markdown + if err := cmd.GenMarkdownTree(rootCmd, outputDir, linkHandler); err != nil { + return fmt.Errorf("failed to generate markdown: %v", err) + } + + return nil +} diff --git a/internal/pkg/cmd/command_internal.go b/internal/pkg/cmd/command_internal.go index 8f6782b7..de22f63b 100644 --- a/internal/pkg/cmd/command_internal.go +++ b/internal/pkg/cmd/command_internal.go @@ -172,19 +172,10 @@ func (c *Command) help() string { // Print any available aliases if len(c.Aliases) > 0 { - aliases := make([]string, len(c.Aliases)) - for i, a := range c.Aliases { - var useline string - if c.hasParent() { - useline = c.parent.commandPath() + " " + a - } else { - useline = a - } - if c.RunF == nil { - useline += " " - } - - aliases[i] = fmt.Sprintf("%s - %s", a, useline) + usages := c.aliasUsages() + var aliases []string + for a, u := range usages { + aliases = append(aliases, fmt.Sprintf("%s - %s", a, u)) } helpEntries = append(helpEntries, helpEntry{"ALIASES", strings.Join(aliases, "\n")}) @@ -387,6 +378,26 @@ func (a PositionalArgument) text(cs *iostreams.ColorScheme) string { return buf.String() } +// aliasUsages returns a map from the alias to its usage +func (c *Command) aliasUsages() map[string]string { + aliases := make(map[string]string) + for _, a := range c.Aliases { + var useline string + if c.hasParent() { + useline = c.parent.commandPath() + " " + a + } else { + useline = a + } + if c.RunF == nil { + useline += " " + } + + aliases[a] = useline + } + + return aliases +} + // usageHelp returns the short usage help that displays the commands usage and // flags. func (c *Command) usageHelp() string { diff --git a/internal/pkg/cmd/gen_md.go b/internal/pkg/cmd/gen_md.go new file mode 100644 index 00000000..371fdac6 --- /dev/null +++ b/internal/pkg/cmd/gen_md.go @@ -0,0 +1,139 @@ +package cmd + +import ( + "bytes" + "fmt" + "io" + "os" + "path/filepath" + "strings" +) + +const markdownExtension = ".mdx" + +// LinkHandler is a function that can be used to modify the links in the +// generated markdown. The path string is the unmodified path to the file. +type LinkHandler func(path string) string + +func GenMarkdownTree(c *Command, dir string, link LinkHandler) error { + // Create the directory if it doesn't exist + if err := os.MkdirAll(dir, 0766); err != nil { + return err + } + + // Determine the filename + filename := "index" + markdownExtension + if c.RunF != nil { + filename = c.Name + markdownExtension + } + + // Create the file + f, err := os.Create(filepath.Join(dir, filename)) + if err != nil { + return err + } + defer f.Close() + + // Generate the markdown + if err := GenMarkdown(c, f, link); err != nil { + return err + } + + for _, c := range c.children { + dir := dir + if len(c.children) > 0 { + dir = filepath.Join(dir, c.Name) + } + + if err := GenMarkdownTree(c, dir, link); err != nil { + return err + } + } + + return nil +} + +// GenMarkdown creates custom markdown output. +func GenMarkdown(c *Command, w io.Writer, link LinkHandler) error { + cs := c.getIO().ColorScheme() + + buf := new(bytes.Buffer) + name := c.commandPath() + + buf.WriteString("---\n") + buf.WriteString(fmt.Sprintf("page_title: %s\n", name)) + buf.WriteString(fmt.Sprintf("description: |-\n %s\n", c.ShortHelp)) + buf.WriteString("---\n\n") + + buf.WriteString("# " + name + "\n\n") + + buf.WriteString("## Name\n\n") + buf.WriteString(fmt.Sprintf("%s - %s\n\n", name, c.ShortHelp)) + + buf.WriteString("## Usage\n\n") + buf.WriteString(fmt.Sprintf("```shell-session\n$ %s\n```\n\n", c.useLine())) + + // Description + if len(c.LongHelp) > 0 { + buf.WriteString("## Description\n\n") + buf.WriteString(c.LongHelp + "\n\n") + } + + // Aliases + if len(c.Aliases) > 0 { + buf.WriteString("## Aliases\n\n") + for a, u := range c.aliasUsages() { + buf.WriteString(fmt.Sprintf("%s - `%s`\n", a, u)) + } + buf.WriteString("\n") + } + + // Examples + if len(c.Examples) > 0 { + buf.WriteString("## Examples\n\n") + + for _, e := range c.Examples { + buf.WriteString(fmt.Sprintf("%s\n\n", e.Preamble)) + buf.WriteString(fmt.Sprintf("```shell-session\n%s\n```\n\n", e.Command)) + } + } + + // Children commands + if len(c.children) > 0 { + var commands, groups []string + for _, c := range c.children { + path := strings.ReplaceAll(c.commandPath(), " ", "/") + markdownExtension + entry := fmt.Sprintf("- [%s](%s) - %s", c.Name, link(path), c.ShortHelp) + + if c.RunF != nil { + commands = append(commands, entry) + } else { + groups = append(groups, entry) + } + } + if len(groups) > 0 { + buf.WriteString("## Command Groups\n\n") + buf.WriteString(strings.Join(groups, "\n") + "\n\n") + } + + if len(commands) > 0 { + buf.WriteString("## Commands\n\n") + buf.WriteString(strings.Join(commands, "\n") + "\n\n") + } + } + + // Positional arguments + if len(c.Args.Args) > 0 { + buf.WriteString("## Positional Arguments\n\n") + buf.WriteString(fmt.Sprintf("%s", c.Args.text(cs))) + } + + // TODO Print flags + + // TODO Additional help + + // TODO Print global flags. Handle being the root command + + _, err := buf.WriteTo(w) + return err +}