From 6ee762e4b5bc7b801a1bd66cb058ac252e9e03e3 Mon Sep 17 00:00:00 2001 From: Ravi Suhag Date: Thu, 5 Dec 2024 21:24:32 -0600 Subject: [PATCH] feat: improve ux for cmdx package --- cli/cmdx/README.md | 193 --------------------------- cli/cmdx/cmdx.go | 128 ++++++++++++++++++ cli/cmdx/{shell.go => completion.go} | 74 ++++++---- cli/cmdx/doc.go | 82 ++++++++++++ cli/cmdx/errors.go | 35 ----- cli/cmdx/help.go | 25 ++-- cli/cmdx/hook.go | 30 ----- cli/cmdx/hooks.go | 12 ++ cli/cmdx/{docs.go => markdown.go} | 37 +++-- cli/cmdx/ref.go | 87 ------------ cli/cmdx/reference.go | 95 +++++++++++++ cli/cmdx/topic.go | 59 -------- cli/cmdx/topics.go | 51 +++++++ cli/cmdx/utils.go | 11 -- cli/{cmdx => config}/config.go | 12 +- 15 files changed, 468 insertions(+), 463 deletions(-) delete mode 100644 cli/cmdx/README.md create mode 100644 cli/cmdx/cmdx.go rename cli/cmdx/{shell.go => completion.go} (57%) create mode 100644 cli/cmdx/doc.go delete mode 100644 cli/cmdx/errors.go delete mode 100644 cli/cmdx/hook.go create mode 100644 cli/cmdx/hooks.go rename cli/cmdx/{docs.go => markdown.go} (51%) delete mode 100644 cli/cmdx/ref.go create mode 100644 cli/cmdx/reference.go delete mode 100644 cli/cmdx/topic.go create mode 100644 cli/cmdx/topics.go rename cli/{cmdx => config}/config.go (94%) diff --git a/cli/cmdx/README.md b/cli/cmdx/README.md deleted file mode 100644 index 57856b3..0000000 --- a/cli/cmdx/README.md +++ /dev/null @@ -1,193 +0,0 @@ - -# cmdx - -`cmdx` is a utility package designed to enhance the functionality of [Cobra](https://github.com/spf13/cobra), a popular Go library for creating command-line interfaces. It provides various helper functions and features to streamline CLI development, such as custom help topics, shell completion, command annotations, and client-specific configurations. - -## Features - -- **Help Topics**: Add custom help topics with descriptions and examples. -- **Shell Completions**: Generate completion scripts for Bash, Zsh, Fish, and PowerShell. -- **Command Reference**: Generate markdown documentation for all commands. -- **Client Hooks**: Apply custom configurations to commands annotated with `client:true`. - - -## Installation - -To install the `cmdx` package, add it to your project using `go get`: - -```bash -go get github.com/raystack/salt/cli/cmdx -``` - -## Usages - -### SetHelpTopicCmd - -Provides a way to define custom help topics that appear in the `help` command. - -#### Example Usage -```go -topic := map[string]string{ - "short": "Environment variables help", - "long": "Detailed information about environment variables used by the CLI.", - "example": "$ mycli help env", -} - -rootCmd.AddCommand(cmdx.SetHelpTopicCmd("env", topic)) -``` - -#### Output -```bash -$ mycli help env -Detailed information about environment variables used by the CLI. - -EXAMPLES - - $ mycli help env -``` - ---- - -### SetCompletionCmd - -Adds a `completion` command to generate shell completion scripts for Bash, Zsh, Fish, and PowerShell. - -#### Example Usage -```go -completionCmd := cmdx.SetCompletionCmd("mycli") -rootCmd.AddCommand(completionCmd) -``` - -#### Command Output -```bash -# Generate Bash completion script -$ mycli completion bash - -# Generate Zsh completion script -$ mycli completion zsh - -# Generate Fish completion script -$ mycli completion fish -``` - -#### Supported Shells -- **Bash**: Use `mycli completion bash`. -- **Zsh**: Use `mycli completion zsh`. -- **Fish**: Use `mycli completion fish`. -- **PowerShell**: Use `mycli completion powershell`. - ---- - -### SetRefCmd - -Adds a `reference` command to generate markdown documentation for all commands in the CLI hierarchy. - -#### Example Usage -```go -refCmd := cmdx.SetRefCmd(rootCmd) -rootCmd.AddCommand(refCmd) -``` - -#### Command Output -```bash -$ mycli reference -# mycli reference - -## `example` - -A sample subcommand for the CLI. - -## `another` - -Another example subcommand. -``` - ---- - -### SetClientHook - -Applies a custom function to commands annotated with `client:true`. Useful for client-specific configurations. - -#### Example Usage -```go -cmdx.SetClientHook(rootCmd, func(cmd *cobra.Command) { - cmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { - fmt.Println("Executing client-specific setup for", cmd.Name()) - } -}) -``` - -#### Command Example -```go -clientCmd := &cobra.Command{ - Use: "client-action", - Short: "A client-specific action", - Annotations: map[string]string{ - "client": "true", - }, -} -rootCmd.AddCommand(clientCmd) -``` - -#### Output -```bash -$ mycli client-action -Executing client-specific setup for client-action -``` - ---- - -## Examples - -Adding all features to a CLI - -```go -package main - -import ( - "fmt" - "github.com/spf13/cobra" - "github.com/raystack/salt/cli/cmdx" -) - -func main() { - rootCmd := &cobra.Command{ - Use: "mycli", - Short: "A custom CLI tool", - } - - // Add Help Topic - topic := map[string]string{ - "short": "Environment variables help", - "long": "Details about environment variables used by the CLI.", - "example": "$ mycli help env", - } - rootCmd.AddCommand(cmdx.SetHelpTopicCmd("env", topic)) - - // Add Completion Command - rootCmd.AddCommand(cmdx.SetCompletionCmd("mycli")) - - // Add Reference Command - rootCmd.AddCommand(cmdx.SetRefCmd(rootCmd)) - - // Add Client Hook - clientCmd := &cobra.Command{ - Use: "client-action", - Short: "A client-specific action", - Annotations: map[string]string{ - "client": "true", - }, - } - rootCmd.AddCommand(clientCmd) - - cmdx.SetClientHook(rootCmd, func(cmd *cobra.Command) { - cmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { - fmt.Println("Executing client-specific setup for", cmd.Name()) - } - }) - - if err := rootCmd.Execute(); err != nil { - fmt.Println("Error:", err) - } -} -``` \ No newline at end of file diff --git a/cli/cmdx/cmdx.go b/cli/cmdx/cmdx.go new file mode 100644 index 0000000..3bccfe6 --- /dev/null +++ b/cli/cmdx/cmdx.go @@ -0,0 +1,128 @@ +package cmdx + +import ( + "strings" + + "github.com/spf13/cobra" +) + +// Manager manages and configures features for a CLI tool. +type Manager struct { + RootCmd *cobra.Command + Help bool // Enable custom help. + Reference bool // Enable reference command. + Completion bool // Enable shell completion. + Config bool // Enable configuration management. + Docs bool // Enable markdown documentation + Hooks []HookBehavior // Hook behaviors to apply to commands + Topics []HelpTopic // Help topics with their details. +} + +// HelpTopic defines a single help topic with its details. +type HelpTopic struct { + Name string + Short string + Long string + Example string +} + +// HookBehavior defines a specific behavior applied to commands. +type HookBehavior struct { + Name string // Name of the hook (e.g., "setup", "auth"). + Behavior func(cmd *cobra.Command) // Function to apply to commands. +} + +// NewManager creates a new CLI Manager using the provided root command and optional configurations. +// +// Parameters: +// - rootCmd: The root Cobra command for the CLI. +// - options: Functional options for configuring the Manager. +// +// Example: +// +// rootCmd := &cobra.Command{Use: "mycli"} +// manager := cmdx.NewManager(rootCmd, cmdx.WithTopics(...), cmdx.WithHooks(...)) +func NewManager(rootCmd *cobra.Command, options ...func(*Manager)) *Manager { + // Create Manager with defaults + manager := &Manager{ + RootCmd: rootCmd, + Help: true, // Default enabled + Reference: true, // Default enabled + Completion: true, // Default enabled + Docs: false, // Default disabled + Topics: []HelpTopic{}, + Hooks: []HookBehavior{}, + } + + // Apply functional options + for _, opt := range options { + opt(manager) + } + + return manager +} + +// Init sets up the CLI features based on the Manager's configuration. +// +// It enables or disables features like custom help, reference documentation, +// shell completion, help topics, and client hooks based on the Manager's settings. +func (m *Manager) Init() { + if m.Help { + m.SetCustomHelp() + } + if m.Reference { + m.AddReferenceCommand() + } + if m.Completion { + m.AddCompletionCommand() + } + if m.Docs { + m.AddMarkdownCommand("./docs") + } + if len(m.Topics) > 0 { + m.AddHelpTopics() + } + + if len(m.Hooks) > 0 { + m.AddClientHooks() + } +} + +// WithTopics sets the help topics for the Manager. +func WithTopics(topics []HelpTopic) func(*Manager) { + return func(m *Manager) { + m.Topics = topics + } +} + +// WithHooks sets the hook behaviors for the Manager. +func WithHooks(hooks []HookBehavior) func(*Manager) { + return func(m *Manager) { + m.Hooks = hooks + } +} + +// IsCLIErr checks if the given error is related to a Cobra command error. +// +// This is useful for distinguishing between user errors (e.g., incorrect commands or flags) +// and program errors, allowing the application to display appropriate messages. +func IsCLIErr(err error) bool { + if err == nil { + return false + } + + // Known Cobra command error keywords + cmdErrorKeywords := []string{ + "unknown command", + "unknown flag", + "unknown shorthand flag", + } + + errMessage := err.Error() + for _, keyword := range cmdErrorKeywords { + if strings.Contains(errMessage, keyword) { + return true + } + } + return false +} diff --git a/cli/cmdx/shell.go b/cli/cmdx/completion.go similarity index 57% rename from cli/cmdx/shell.go rename to cli/cmdx/completion.go index 6aae0e8..581d473 100644 --- a/cli/cmdx/shell.go +++ b/cli/cmdx/completion.go @@ -7,16 +7,57 @@ import ( "github.com/spf13/cobra" ) -// SetCompletionCmd creates a `completion` command to generate shell completion scripts. +// AddCompletionCommand adds a `completion` command to the CLI. // -// The command supports generating scripts for Bash, Zsh, Fish, and PowerShell. -// It should be added to the root command of the application. -func SetCompletionCmd(exec string) *cobra.Command { +// The completion command generates shell completion scripts for Bash, Zsh, +// Fish, and PowerShell. +// +// Example: +// +// manager := cmdx.NewManager(rootCmd) +// manager.AddCompletionCommand() +// +// Usage: +// +// $ mycli completion bash +// $ mycli completion zsh +func (m *Manager) AddCompletionCommand() { + summary := m.generateCompletionSummary(m.RootCmd.Use) + + completionCmd := &cobra.Command{ + Use: "completion [bash|zsh|fish|powershell]", + Short: "Generate shell completion scripts", + Long: summary, + DisableFlagsInUseLine: true, + ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, + Args: cobra.ExactValidArgs(1), + Run: m.runCompletionCommand, + } + + m.RootCmd.AddCommand(completionCmd) +} + +// runCompletionCommand executes the appropriate shell completion generation logic. +func (m *Manager) runCompletionCommand(cmd *cobra.Command, args []string) { + switch args[0] { + case "bash": + cmd.Root().GenBashCompletion(os.Stdout) + case "zsh": + cmd.Root().GenZshCompletion(os.Stdout) + case "fish": + cmd.Root().GenFishCompletion(os.Stdout, true) + case "powershell": + cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) + } +} + +// generateCompletionSummary creates the long description for the `completion` command. +func (m *Manager) generateCompletionSummary(exec string) string { var execs []interface{} for i := 0; i < 12; i++ { execs = append(execs, exec) } - summary := heredoc.Docf(`To load completions: + return heredoc.Docf(`To load completions: `+"```"+` Bash: @@ -40,7 +81,7 @@ func SetCompletionCmd(exec string) *cobra.Command { # You will need to start a new shell for this setup to take effect. - fish: + Fish: $ %s completion fish | source @@ -56,25 +97,4 @@ func SetCompletionCmd(exec string) *cobra.Command { # and source this file from your PowerShell profile. `+"```"+` `, execs...) - - return &cobra.Command{ - Use: "completion [bash|zsh|fish|powershell]", - Short: "Generate shell completion scripts", - Long: summary, - DisableFlagsInUseLine: true, - ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, - Args: cobra.ExactValidArgs(1), - Run: func(cmd *cobra.Command, args []string) { - switch args[0] { - case "bash": - cmd.Root().GenBashCompletion(os.Stdout) - case "zsh": - cmd.Root().GenZshCompletion(os.Stdout) - case "fish": - cmd.Root().GenFishCompletion(os.Stdout, true) - case "powershell": - cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) - } - }, - } } diff --git a/cli/cmdx/doc.go b/cli/cmdx/doc.go new file mode 100644 index 0000000..7ca995e --- /dev/null +++ b/cli/cmdx/doc.go @@ -0,0 +1,82 @@ +// Package cmdx extends the capabilities of the Cobra library to build advanced CLI tools. +// It provides features such as custom help, shell completion, reference documentation generation, +// help topics, and client-specific hooks. +// +// # Features +// +// 1. **Custom Help**: +// Enhance the default help output with a structured and detailed format. +// +// 2. **Reference Command**: +// Generate markdown documentation for the entire CLI command tree. +// +// 3. **Shell Completion**: +// Generate shell completion scripts for Bash, Zsh, Fish, and PowerShell. +// +// 4. **Help Topics**: +// Add custom help topics to provide detailed information about specific subjects. +// +// 5. **Client Hooks**: +// Apply custom logic to commands annotated with `client:true`. +// +// # Example +// +// The following example demonstrates how to use the cmdx package: +// +// package main +// +// import ( +// "fmt" +// "github.com/spf13/cobra" +// "github.com/your-username/cmdx" +// ) +// +// func main() { +// rootCmd := &cobra.Command{ +// Use: "mycli", +// Short: "A sample CLI tool", +// } +// +// // Define help topics +// helpTopics := []cmdx.HelpTopic{ +// { +// Name: "env", +// Short: "Environment variables help", +// Long: "Details about environment variables used by the CLI.", +// Example: "$ mycli help env", +// }, +// } +// +// // Define hooks +// hooks := []cmdx.HookBehavior{ +// { +// Name: "setup", +// Behavior: func(cmd *cobra.Command) { +// cmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { +// fmt.Println("Setting up for", cmd.Name()) +// } +// }, +// }, +// } +// +// // Create the Manager with configurations +// manager := cmdx.NewManager( +// rootCmd, +// cmdx.WithTopics(helpTopics), +// cmdx.WithHooks(hooks), +// cmdx.EnableConfig(), +// cmdx.EnableDocs(), +// ) +// +// // Initialize the manager +// if err := manager.Initialize(); err != nil { +// fmt.Println("Error initializing CLI:", err) +// return +// } +// +// // Execute the CLI +// if err := rootCmd.Execute(); err != nil { +// fmt.Println("Command execution failed:", err) +// } +// } +package cmdx diff --git a/cli/cmdx/errors.go b/cli/cmdx/errors.go deleted file mode 100644 index df276c5..0000000 --- a/cli/cmdx/errors.go +++ /dev/null @@ -1,35 +0,0 @@ -package cmdx - -import "strings" - -// IsCmdErr checks if the given error is related to a Cobra command error. -// -// This is useful for distinguishing between user errors (e.g., incorrect commands or flags) -// and program errors, allowing the application to display appropriate messages. -// -// Parameters: -// - err: The error to check. -// -// Returns: -// - true if the error message contains any known Cobra command error keywords. -// - false otherwise. -func IsCmdErr(err error) bool { - if err == nil { - return false - } - - // Known Cobra command error keywords - cmdErrorKeywords := []string{ - "unknown command", - "unknown flag", - "unknown shorthand flag", - } - - errMessage := err.Error() - for _, keyword := range cmdErrorKeywords { - if strings.Contains(errMessage, keyword) { - return true - } - } - return false -} diff --git a/cli/cmdx/help.go b/cli/cmdx/help.go index fee5bb2..36cf6ef 100644 --- a/cli/cmdx/help.go +++ b/cli/cmdx/help.go @@ -12,8 +12,8 @@ import ( const ( USAGE = "Usage" CORECMD = "Core commands" - HELPCMD = "Help topics" OTHERCMD = "Other commands" + HELPCMD = "Help topics" FLAGS = "Flags" IFLAGS = "Inherited flags" ARGUMENTS = "Arguments" @@ -23,16 +23,23 @@ const ( FEEDBACK = "Feedback" ) -// SetHelp configures custom help and usage functions for a Cobra command. -// It organizes commands into sections based on annotations and provides enhanced error handling. -func SetHelp(cmd *cobra.Command) { - cmd.PersistentFlags().Bool("help", false, "Show help for command") - - cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { +// SetCustomHelp configures a custom help function for the CLI. +// +// The custom help function organizes commands into sections and provides +// detailed error messages for incorrect flag usage. +// +// Example: +// +// manager := cmdx.NewManager(rootCmd) +// manager.SetCustomHelp() +func (m *Manager) SetCustomHelp() { + m.RootCmd.PersistentFlags().Bool("help", false, "Show help for command") + + m.RootCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { displayHelp(cmd, args) }) - cmd.SetUsageFunc(generateUsage) - cmd.SetFlagErrorFunc(handleFlagError) + m.RootCmd.SetUsageFunc(generateUsage) + m.RootCmd.SetFlagErrorFunc(handleFlagError) } // generateUsage customizes the usage function for a command. diff --git a/cli/cmdx/hook.go b/cli/cmdx/hook.go deleted file mode 100644 index 5842d5e..0000000 --- a/cli/cmdx/hook.go +++ /dev/null @@ -1,30 +0,0 @@ -package cmdx - -import "github.com/spf13/cobra" - -// SetClientHook recursively applies a custom function to all commands -// with the annotation `client:true` in the given Cobra command tree. -// -// This is particularly useful for applying client-specific configurations -// to commands annotated as "client". -// -// Parameters: -// - rootCmd: The root Cobra command to start traversing from. -// - applyFunc: A function that applies the desired configuration -// to commands with the `client:true` annotation. -// -// Example Usage: -// -// cmdx.SetClientHook(rootCmd, func(cmd *cobra.Command) { -// cmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { -// fmt.Println("Client-specific setup") -// } -// }) -func SetClientHook(rootCmd *cobra.Command, applyFunc func(cmd *cobra.Command)) { - for _, subCmd := range rootCmd.Commands() { - if subCmd.Annotations != nil && subCmd.Annotations["client"] == "true" { - applyFunc(subCmd) - } - SetClientHook(subCmd, applyFunc) - } -} diff --git a/cli/cmdx/hooks.go b/cli/cmdx/hooks.go new file mode 100644 index 0000000..7524a08 --- /dev/null +++ b/cli/cmdx/hooks.go @@ -0,0 +1,12 @@ +package cmdx + +// AddClientHooks applies all configured hooks to commands annotated with `client:true`. +func (m *Manager) AddClientHooks() { + for _, cmd := range m.RootCmd.Commands() { + for _, hook := range m.Hooks { + if cmd.Annotations["client"] == "true" { + hook.Behavior(cmd) + } + } + } +} diff --git a/cli/cmdx/docs.go b/cli/cmdx/markdown.go similarity index 51% rename from cli/cmdx/docs.go rename to cli/cmdx/markdown.go index aaf672a..56d9332 100644 --- a/cli/cmdx/docs.go +++ b/cli/cmdx/markdown.go @@ -9,8 +9,25 @@ import ( "github.com/spf13/cobra/doc" ) -// GenerateMarkdownTree generates a Markdown documentation tree for all commands -// in the given Cobra command hierarchy. +// AddMarkdownCommand integrates a hidden `markdown` command into the root command. +// This command generates a Markdown documentation tree for all commands in the hierarchy. +func (m *Manager) AddMarkdownCommand(outputPath string) { + markdownCmd := &cobra.Command{ + Use: "markdown", + Short: "Generate Markdown documentation for all commands", + Hidden: true, + Annotations: map[string]string{ + "group": "help", + }, + RunE: func(cmd *cobra.Command, args []string) error { + return m.generateMarkdownTree(outputPath, m.RootCmd) + }, + } + + m.RootCmd.AddCommand(markdownCmd) +} + +// generateMarkdownTree generates a Markdown documentation tree for the given command hierarchy. // // Parameters: // - rootOutputPath: The root directory where the Markdown files will be generated. @@ -18,25 +35,22 @@ import ( // // Returns: // - An error if any part of the process (file creation, directory creation) fails. -// -// Example Usage: -// -// rootCmd := &cobra.Command{Use: "mycli"} -// cmdx.GenerateMarkdownTree("./docs", rootCmd) -func GenerateMarkdownTree(rootOutputPath string, cmd *cobra.Command) error { +func (m *Manager) generateMarkdownTree(rootOutputPath string, cmd *cobra.Command) error { dirFilePath := filepath.Join(rootOutputPath, cmd.Name()) + + // Handle subcommands by creating a directory and iterating through subcommands. if len(cmd.Commands()) > 0 { if err := ensureDir(dirFilePath); err != nil { return fmt.Errorf("failed to create directory for command %q: %w", cmd.Name(), err) } for _, subCmd := range cmd.Commands() { - if err := GenerateMarkdownTree(dirFilePath, subCmd); err != nil { + if err := m.generateMarkdownTree(dirFilePath, subCmd); err != nil { return err } } } else { - outFilePath := filepath.Join(rootOutputPath, cmd.Name()) - outFilePath = outFilePath + ".md" + // Generate a Markdown file for leaf commands. + outFilePath := filepath.Join(rootOutputPath, cmd.Name()+".md") f, err := os.Create(outFilePath) if err != nil { @@ -44,6 +58,7 @@ func GenerateMarkdownTree(rootOutputPath string, cmd *cobra.Command) error { } defer f.Close() + // Generate Markdown with a custom link handler. return doc.GenMarkdownCustom(cmd, f, func(s string) string { return filepath.Join(dirFilePath, s) }) diff --git a/cli/cmdx/ref.go b/cli/cmdx/ref.go deleted file mode 100644 index 93612c3..0000000 --- a/cli/cmdx/ref.go +++ /dev/null @@ -1,87 +0,0 @@ -package cmdx - -import ( - "bytes" - "fmt" - "io" - "strings" - - "github.com/raystack/salt/cli/printer" - - "github.com/spf13/cobra" -) - -// SetRefCmd adds a `reference` command to the root command to generate -// comprehensive reference documentation for the command tree. -// -// The `reference` command outputs the documentation in markdown format -// and supports a `--plain` flag to control whether ANSI colors are used. -func SetRefCmd(root *cobra.Command) *cobra.Command { - var isPlain bool - cmd := &cobra.Command{ - Use: "reference", - Short: "Comprehensive reference of all commands", - Long: referenceLong(root), - Run: referenceHelpFn(&isPlain), - Annotations: map[string]string{ - "group": "help", - }, - } - cmd.SetHelpFunc(referenceHelpFn(&isPlain)) - cmd.Flags().BoolVarP(&isPlain, "plain", "p", true, "output in plain markdown (without ansi color)") - return cmd -} - -// referenceHelpFn generates the output for the `reference` command. -// It renders the documentation either as plain markdown or with ANSI color. -func referenceHelpFn(isPlain *bool) func(*cobra.Command, []string) { - return func(cmd *cobra.Command, args []string) { - var ( - output string - err error - ) - - if *isPlain { - output = cmd.Long - } else { - output, err = printer.Markdown(cmd.Long) - if err != nil { - fmt.Println("Error generating markdown:", err) - return - } - } - - fmt.Print(output) - } -} - -// referenceLong generates the complete reference documentation -// for the command tree in markdown format. -func referenceLong(cmd *cobra.Command) string { - buf := bytes.NewBufferString(fmt.Sprintf("# %s reference\n\n", cmd.Name())) - for _, c := range cmd.Commands() { - if c.Hidden { - continue - } - generateCommandReference(buf, c, 2) - } - return buf.String() -} - -func generateCommandReference(w io.Writer, cmd *cobra.Command, depth int) { - // Name + Description - fmt.Fprintf(w, "%s `%s`\n\n", strings.Repeat("#", depth), cmd.UseLine()) - fmt.Fprintf(w, "%s\n\n", cmd.Short) - - if flagUsages := cmd.Flags().FlagUsages(); flagUsages != "" { - fmt.Fprintf(w, "```\n%s````\n\n", dedent(flagUsages)) - } - - // Subcommands - for _, c := range cmd.Commands() { - if c.Hidden { - continue - } - generateCommandReference(w, c, depth+1) - } -} diff --git a/cli/cmdx/reference.go b/cli/cmdx/reference.go new file mode 100644 index 0000000..3e3ec91 --- /dev/null +++ b/cli/cmdx/reference.go @@ -0,0 +1,95 @@ +package cmdx + +import ( + "bytes" + "fmt" + "io" + "strings" + + "github.com/raystack/salt/cli/printer" + + "github.com/spf13/cobra" +) + +// AddReferenceCommand adds a `reference` command to the CLI. +// +// The reference command generates markdown documentation for all commands +// in the CLI command tree. +// +// Example: +// +// manager := cmdx.NewManager(rootCmd) +// manager.AddReferenceCommand() +func (m *Manager) AddReferenceCommand() { + var isPlain bool + refCmd := &cobra.Command{ + Use: "reference", + Short: "Comprehensive reference of all commands", + Long: m.generateReferenceMarkdown(), + Run: m.runReferenceCommand(&isPlain), + Annotations: map[string]string{ + "group": "help", + }, + } + refCmd.SetHelpFunc(m.runReferenceCommand(&isPlain)) + refCmd.Flags().BoolVarP(&isPlain, "plain", "p", true, "output in plain markdown (without ANSI color)") + + m.RootCmd.AddCommand(refCmd) +} + +// runReferenceCommand handles the output generation for the `reference` command. +// It renders the documentation either as plain markdown or with ANSI color. +func (m *Manager) runReferenceCommand(isPlain *bool) func(cmd *cobra.Command, args []string) { + return func(cmd *cobra.Command, args []string) { + var ( + output string + err error + ) + + if *isPlain { + output = cmd.Long + } else { + output, err = printer.Markdown(cmd.Long) + if err != nil { + fmt.Println("Error generating markdown:", err) + return + } + } + + fmt.Print(output) + } +} + +// generateReferenceMarkdown generates a complete markdown representation +// of the command tree for the `reference` command. +func (m *Manager) generateReferenceMarkdown() string { + buf := bytes.NewBufferString(fmt.Sprintf("# %s reference\n\n", m.RootCmd.Name())) + for _, c := range m.RootCmd.Commands() { + if c.Hidden { + continue + } + m.generateCommandReference(buf, c, 2) + } + return buf.String() +} + +// generateCommandReference recursively generates markdown for a given command +// and its subcommands. +func (m *Manager) generateCommandReference(w io.Writer, cmd *cobra.Command, depth int) { + // Name + Description + fmt.Fprintf(w, "%s `%s`\n\n", strings.Repeat("#", depth), cmd.UseLine()) + fmt.Fprintf(w, "%s\n\n", cmd.Short) + + // Flags + if flagUsages := cmd.Flags().FlagUsages(); flagUsages != "" { + fmt.Fprintf(w, "```\n%s```\n\n", dedent(flagUsages)) + } + + // Subcommands + for _, c := range cmd.Commands() { + if c.Hidden { + continue + } + m.generateCommandReference(w, c, depth+1) + } +} diff --git a/cli/cmdx/topic.go b/cli/cmdx/topic.go deleted file mode 100644 index da4fef5..0000000 --- a/cli/cmdx/topic.go +++ /dev/null @@ -1,59 +0,0 @@ -package cmdx - -import ( - "github.com/spf13/cobra" -) - -// SetHelpTopicCmd creates a custom help topic command. -// -// This function allows you to define a help topic that provides detailed information -// about a specific subject. The generated command should be added to the root command. -// -// Parameters: -// - title: The name of the help topic (e.g., "env"). -// - topic: A map containing the following keys: -// - "short": A brief description of the topic. -// - "long": A detailed explanation of the topic. -// - "example": An example usage of the topic. -// -// Returns: -// - A pointer to the configured help topic `cobra.Command`. -// -// Example: -// -// topic := map[string]string{ -// "short": "Environment variables help", -// "long": "Details about environment variables used by the CLI.", -// "example": "$ mycli help env", -// } -// rootCmd.AddCommand(cmdx.SetHelpTopicCmd("env", topic)) -func SetHelpTopicCmd(title string, topic map[string]string) *cobra.Command { - cmd := &cobra.Command{ - Use: title, - Short: topic["short"], - Long: topic["long"], - Example: topic["example"], - Hidden: false, - Annotations: map[string]string{ - "group": "help", - }, - } - - cmd.SetHelpFunc(helpTopicHelpFunc) - cmd.SetUsageFunc(helpTopicUsageFunc) - - return cmd -} - -func helpTopicHelpFunc(command *cobra.Command, args []string) { - command.Print(command.Long) - if command.Example != "" { - command.Printf("\nEXAMPLES\n") - command.Print(indent(command.Example, " ")) - } -} - -func helpTopicUsageFunc(command *cobra.Command) error { - command.Printf("Usage: %s help %s\n", command.Root().Name(), command.Use) - return nil -} diff --git a/cli/cmdx/topics.go b/cli/cmdx/topics.go new file mode 100644 index 0000000..df67561 --- /dev/null +++ b/cli/cmdx/topics.go @@ -0,0 +1,51 @@ +package cmdx + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// AddHelpTopics adds all configured help topics to the CLI. +// +// Help topics provide detailed information about specific subjects, +// such as environment variables or configuration. +func (m *Manager) AddHelpTopics() { + for _, topic := range m.Topics { + m.addHelpTopicCommand(topic) + } +} + +// addHelpTopicCommand adds a single help topic command to the CLI. +func (m *Manager) addHelpTopicCommand(topic HelpTopic) { + helpCmd := &cobra.Command{ + Use: topic.Name, + Short: topic.Short, + Long: topic.Long, + Example: topic.Example, + Hidden: false, + Annotations: map[string]string{ + "group": "help", + }, + } + + helpCmd.SetHelpFunc(helpTopicHelpFunc) + helpCmd.SetUsageFunc(helpTopicUsageFunc) + + m.RootCmd.AddCommand(helpCmd) +} + +// helpTopicHelpFunc customizes the help message for a help topic command. +func helpTopicHelpFunc(cmd *cobra.Command, args []string) { + fmt.Fprintln(cmd.OutOrStdout(), cmd.Long) + if cmd.Example != "" { + fmt.Fprintln(cmd.OutOrStdout(), "\nEXAMPLES") + fmt.Fprintln(cmd.OutOrStdout(), indent(cmd.Example, " ")) + } +} + +// helpTopicUsageFunc customizes the usage message for a help topic command. +func helpTopicUsageFunc(cmd *cobra.Command) error { + fmt.Fprintf(cmd.OutOrStdout(), "Usage: %s help %s\n", cmd.Root().Name(), cmd.Use) + return nil +} diff --git a/cli/cmdx/utils.go b/cli/cmdx/utils.go index 45521da..b00e7c8 100644 --- a/cli/cmdx/utils.go +++ b/cli/cmdx/utils.go @@ -3,7 +3,6 @@ package cmdx import ( "bytes" "fmt" - "os" "regexp" "strings" @@ -53,16 +52,6 @@ func indent(s, indent string) string { return lineRE.ReplaceAllLiteralString(s, indent) } -func dirExists(path string) bool { - f, err := os.Stat(path) - return err == nil && f.IsDir() -} - -func fileExists(filename string) bool { - _, err := os.Stat(filename) - return err == nil -} - func toTitle(text string) string { heading := cases.Title(language.Und).String(text) return heading diff --git a/cli/cmdx/config.go b/cli/config/config.go similarity index 94% rename from cli/cmdx/config.go rename to cli/config/config.go index 855ca4a..4ed512f 100644 --- a/cli/cmdx/config.go +++ b/cli/config/config.go @@ -1,4 +1,4 @@ -package cmdx +package config import ( "errors" @@ -141,3 +141,13 @@ func configDir(root string) string { return path } + +func dirExists(path string) bool { + f, err := os.Stat(path) + return err == nil && f.IsDir() +} + +func fileExists(filename string) bool { + _, err := os.Stat(filename) + return err == nil +}