diff --git a/cmd/alias.go b/cmd/alias.go index 226fed38..962af057 100644 --- a/cmd/alias.go +++ b/cmd/alias.go @@ -224,7 +224,7 @@ Options: -h, --help Show this help ` - return strings.TrimSpace(helpText) + return CreateHelp("alias", c.Synopsis(), strings.TrimSpace(helpText)) } func (c *AliasCommand) AutocompleteArgs() complete.Predictor { diff --git a/cmd/check.go b/cmd/check.go index c26a2590..724ba320 100644 --- a/cmd/check.go +++ b/cmd/check.go @@ -114,7 +114,7 @@ Options: -h, --help show this help ` - return strings.TrimSpace(helpText) + return CreateHelp("check", c.Synopsis(), strings.TrimSpace(helpText)) } func checkRequirement(req trellis.Requirement) (result trellis.RequirementResult, err error) { diff --git a/cmd/db_open.go b/cmd/db_open.go index 66af68e8..31a977bf 100644 --- a/cmd/db_open.go +++ b/cmd/db_open.go @@ -196,7 +196,7 @@ Options: -h, --help show this help ` - return strings.TrimSpace(fmt.Sprintf(helpText, c.dbOpenerFactory.GetSupportedApps())) + return CreateHelp("db open", c.Synopsis(), strings.TrimSpace(fmt.Sprintf(helpText, c.dbOpenerFactory.GetSupportedApps()))) } func (c *DBOpenCommand) AutocompleteArgs() complete.Predictor { diff --git a/cmd/deploy.go b/cmd/deploy.go index c1558083..1b949aa1 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -157,7 +157,7 @@ Options: -h, --help show this help ` - return strings.TrimSpace(helpText) + return CreateHelp("deploy", c.Synopsis(), strings.TrimSpace(helpText)) } func (c *DeployCommand) AutocompleteArgs() complete.Predictor { diff --git a/cmd/dot_env.go b/cmd/dot_env.go index 49b8f91f..55e13337 100644 --- a/cmd/dot_env.go +++ b/cmd/dot_env.go @@ -115,7 +115,7 @@ Options: -h, --help show this help ` - return strings.TrimSpace(helpText) + return CreateHelp("dot-env", c.Synopsis(), strings.TrimSpace(helpText)) } func (c *DotEnvCommand) AutocompleteArgs() complete.Predictor { diff --git a/cmd/down.go b/cmd/down.go index 99c18f8f..9fb4106a 100644 --- a/cmd/down.go +++ b/cmd/down.go @@ -61,5 +61,5 @@ Options: -h, --help show this help ` - return strings.TrimSpace(helpText) + return CreateHelp("down", c.Synopsis(), strings.TrimSpace(helpText)) } diff --git a/cmd/droplet_create.go b/cmd/droplet_create.go index 3b9af57e..c901bcea 100644 --- a/cmd/droplet_create.go +++ b/cmd/droplet_create.go @@ -218,7 +218,7 @@ Options: -h, --help show this help ` - return strings.TrimSpace(helpText) + return CreateHelp("droplet-create", c.Synopsis(), strings.TrimSpace(helpText)) } func (c *DropletCreateCommand) AutocompleteArgs() complete.Predictor { diff --git a/cmd/droplet_dns.go b/cmd/droplet_dns.go index 8c6b652f..4a1537f2 100644 --- a/cmd/droplet_dns.go +++ b/cmd/droplet_dns.go @@ -211,7 +211,7 @@ Options: -h, --help Show this help ` - return strings.TrimSpace(helpText) + return CreateHelp("droplet-dns", c.Synopsis(), strings.TrimSpace(helpText)) } func (c *DropletDnsCommand) AutocompleteArgs() complete.Predictor { diff --git a/cmd/exec.go b/cmd/exec.go index 57ab17a8..810a9cf2 100644 --- a/cmd/exec.go +++ b/cmd/exec.go @@ -74,5 +74,5 @@ Options: -h, --help show this help ` - return strings.TrimSpace(helpText) + return CreateHelp("exec", c.Synopsis(), strings.TrimSpace(helpText)) } diff --git a/cmd/galaxy_install.go b/cmd/galaxy_install.go index 60b6e056..052a2bfe 100644 --- a/cmd/galaxy_install.go +++ b/cmd/galaxy_install.go @@ -120,5 +120,5 @@ Options: -h, --help show this help ` - return strings.TrimSpace(helpText) + return CreateHelp("galaxy-install", c.Synopsis(), strings.TrimSpace(helpText)) } diff --git a/cmd/help.go b/cmd/help.go new file mode 100644 index 00000000..92868488 --- /dev/null +++ b/cmd/help.go @@ -0,0 +1,408 @@ +package cmd + +import ( + "fmt" + "os" + "strings" + + "github.com/pterm/pterm" + "golang.org/x/term" +) + +// CreateHelp is a helper function for commands to get properly formatted help +func CreateHelp(commandName string, synopsis string, rawHelp string) string { + renderer := GetHelpRenderer() + return renderer.RenderCommand(commandName, synopsis, rawHelp) +} + +// PtermHelpFunc creates a stylized help output for subcommands +func PtermHelpFunc(commandName string, synopsis string, helpText string) { + // Define color scheme + dim := pterm.NewStyle(pterm.FgWhite) + cyan := pterm.NewStyle(pterm.FgCyan) + green := pterm.NewStyle(pterm.FgGreen) + brightWhite := pterm.NewStyle(pterm.FgLightWhite, pterm.Bold) + yellow := pterm.NewStyle(pterm.FgYellow) + + // Get terminal width + termWidth := 80 + if width, _, err := term.GetSize(int(os.Stdout.Fd())); err == nil && width > 0 { + termWidth = width + } + + // Build output as string instead of printing directly + var output strings.Builder + + output.WriteString("\n") + output.WriteString(cyan.Sprint("┌─╼ ")) + output.WriteString(brightWhite.Sprint("trellis " + commandName)) + output.WriteString("\n") + output.WriteString(cyan.Sprint("└─╼ ")) + output.WriteString(dim.Sprint(synopsis)) + output.WriteString("\n\n") + + // Parse the help text to extract usage, examples, arguments, and options + lines := strings.Split(helpText, "\n") + + var currentSection string + var examples []string + var arguments []string + var options []string + var description []string + + inExampleBlock := false + for _, line := range lines { + trimmed := strings.TrimSpace(line) + + // Detect sections + if strings.HasPrefix(line, "Usage:") { + currentSection = "usage" + // Extract and print usage immediately + usageLine := strings.TrimPrefix(line, "Usage:") + output.WriteString(dim.Sprint("$ ")) + output.WriteString(strings.TrimSpace(usageLine)) + output.WriteString("\n\n") + continue + } else if strings.HasPrefix(trimmed, "Arguments:") { + currentSection = "arguments" + continue + } else if strings.HasPrefix(trimmed, "Options:") { + currentSection = "options" + continue + } else if strings.HasPrefix(trimmed, "Create") || strings.HasPrefix(trimmed, "Specify") || strings.HasPrefix(trimmed, "Force") || strings.HasPrefix(trimmed, "$") { + // When we hit examples, stop adding to description + // This prevents example lead-in text from showing in description + inExampleBlock = true + currentSection = "examples" + // Remove the last few description lines that are probably example lead-ins + if strings.HasPrefix(trimmed, "$") && len(description) > 0 { + // For db open specifically, remove ALL description since it's all example lead-ins + // More aggressive cleanup - remove trailing description lines + for len(description) > 0 { + lastDesc := description[len(description)-1] + if lastDesc == "" || + strings.Contains(lastDesc, ":") || + strings.Contains(lastDesc, "example") || + strings.Contains(lastDesc, "database") || + strings.Contains(lastDesc, "production") || + strings.Contains(lastDesc, "defaults to") { + description = description[:len(description)-1] + } else { + break + } + } + } + } + + // Collect content based on section + switch currentSection { + case "usage": + // After usage line, collect description until we hit Arguments/Options/Examples + if trimmed == "" { + // Preserve blank lines in description + if len(description) > 0 && description[len(description)-1] != "" { + description = append(description, "") + } + } else if !strings.HasPrefix(trimmed, "Arguments:") && + !strings.HasPrefix(trimmed, "Options:") && + !strings.HasPrefix(trimmed, "Create") && + !strings.HasPrefix(trimmed, "Specify") && + !strings.HasPrefix(trimmed, "Force") && + !strings.HasPrefix(trimmed, "$") && + !strings.Contains(trimmed, "defaults to") && + !strings.Contains(trimmed, "database") && + !strings.Contains(trimmed, "production") && + !strings.Contains(trimmed, ":") { + // This is description text after Usage + // Skip lines that look like example lead-ins + description = append(description, trimmed) + } + + case "examples": + if strings.HasPrefix(trimmed, "$") { + examples = append(examples, trimmed) + inExampleBlock = false + } else if inExampleBlock && trimmed != "" { + // Example description + examples = append(examples, " "+trimmed) + } else if trimmed != "" && !strings.HasPrefix(trimmed, "Arguments:") && !strings.HasPrefix(trimmed, "Options:") { + // Any other text in examples section that's not empty and not a section header + examples = append(examples, trimmed) + } + + case "arguments": + if strings.Contains(line, " ") && !strings.HasPrefix(trimmed, "Options:") { + arguments = append(arguments, line) + } + + case "options": + if strings.Contains(line, " ") || strings.HasPrefix(line, " ") { + options = append(options, line) + } + } + } + + // Print description with manual word wrapping for better control + if len(description) > 0 { + for _, desc := range description { + if desc == "" { + // Preserve blank lines + output.WriteString("\n") + } else { + // Manual word wrapping with proper indentation + words := strings.Fields(desc) + currentLine := "" + lineCount := 0 + maxWidth := termWidth - 4 // Leave some margin + + for _, word := range words { + if currentLine == "" { + currentLine = word + } else if len(currentLine)+1+len(word) <= maxWidth { + currentLine += " " + word + } else { + // Print current line + if lineCount == 0 { + output.WriteString(dim.Sprint(currentLine)) + output.WriteString("\n") + } else { + output.WriteString(dim.Sprint(" " + currentLine)) // Indent wrapped lines + output.WriteString("\n") + } + lineCount++ + currentLine = word + } + } + // Print last line + if currentLine != "" { + if lineCount == 0 { + output.WriteString(dim.Sprint(currentLine)) + output.WriteString("\n") + } else { + output.WriteString(dim.Sprint(" " + currentLine)) // Indent wrapped lines + output.WriteString("\n") + } + } + } + } + output.WriteString("\n") + } + + // Print Arguments section + if len(arguments) > 0 { + output.WriteString(" ") + output.WriteString(cyan.Sprint("◉ ")) + output.WriteString(dim.Sprint("ARGUMENTS")) + output.WriteString("\n") + + for _, arg := range arguments { + parts := strings.SplitN(strings.TrimSpace(arg), " ", 2) + if len(parts) == 2 { + output.WriteString(fmt.Sprintf(" %s %-12s %s\n", + green.Sprint("→"), + parts[0], + dim.Sprint(strings.TrimSpace(parts[1])))) + } + } + output.WriteString("\n") + } + + // Print Options section with proper word wrapping + if len(options) > 0 { + output.WriteString(" ") + output.WriteString(cyan.Sprint("◉ ")) + output.WriteString(dim.Sprint("OPTIONS")) + output.WriteString("\n") + + for _, opt := range options { + trimmed := strings.TrimSpace(opt) + if trimmed == "" { + continue + } + + // Handle option lines + if strings.HasPrefix(trimmed, "--") || strings.HasPrefix(trimmed, "-") { + // Find the description part (after multiple spaces) + flagPart := "" + descPart := "" + + // Look for two or more spaces to find where description starts + if idx := strings.Index(trimmed, " "); idx != -1 { + flagPart = strings.TrimSpace(trimmed[:idx]) + descPart = strings.TrimSpace(trimmed[idx:]) + } else { + flagPart = trimmed + } + + // Build the prefix with arrow and flag + prefix := fmt.Sprintf(" %s %-20s ", green.Sprint("→"), flagPart) + + // Calculate visual length for proper alignment + visualPrefixLen := 3 + 1 + 1 + 20 + 1 // " " + arrow + " " + flag + " " = 26 + + // Print first line with prefix + output.WriteString(prefix) + + if descPart != "" { + // Word wrap the description based on terminal width + descWidth := termWidth - visualPrefixLen + + words := strings.Fields(descPart) + currentLine := "" + firstLine := true + + for _, word := range words { + if currentLine == "" { + currentLine = word + } else if len(currentLine)+1+len(word) <= descWidth { + currentLine += " " + word + } else { + // Print current line + if firstLine { + output.WriteString(dim.Sprint(currentLine)) + output.WriteString("\n") + firstLine = false + } else { + output.WriteString(fmt.Sprintf("%*s%s\n", visualPrefixLen, "", dim.Sprint(currentLine))) + } + currentLine = word + } + } + + // Print last line + if currentLine != "" { + if firstLine { + output.WriteString(dim.Sprint(currentLine)) + output.WriteString("\n") + } else { + output.WriteString(fmt.Sprintf("%*s%s\n", visualPrefixLen, "", dim.Sprint(currentLine))) + } + } + } else { + output.WriteString("\n") + } + } + } + output.WriteString("\n") + } + + // Print Examples section with consistent indentation + if len(examples) > 0 { + output.WriteString(" ") + output.WriteString(cyan.Sprint("◉ ")) + output.WriteString(dim.Sprint("EXAMPLES")) + output.WriteString("\n") + + baseIndent := " " // 3 spaces for all example content + lastWasCommand := false + + for _, example := range examples { + trimmed := strings.TrimSpace(example) + + // Add blank line before description that follows a command + if lastWasCommand && !strings.HasPrefix(trimmed, "$") { + output.WriteString("\n") + } + + if strings.HasPrefix(trimmed, "$") { + lastWasCommand = true + // Command example - always indent by baseIndent + maxCmdWidth := termWidth - len(baseIndent) + if len(trimmed) > maxCmdWidth { + // Need to wrap the command + remaining := trimmed + firstLine := true + for len(remaining) > 0 { + cutAt := maxCmdWidth + if !firstLine { + cutAt = maxCmdWidth - 2 // Account for continuation indent + } + + if len(remaining) <= cutAt { + if firstLine { + output.WriteString(baseIndent + yellow.Sprint(remaining)) + output.WriteString("\n") + } else { + output.WriteString(baseIndent + " " + yellow.Sprint(remaining)) + output.WriteString("\n") + } + break + } + + // Find a good break point (space or slash) + breakPoint := cutAt + for i := cutAt; i > cutAt-20 && i > 0; i-- { + if remaining[i] == ' ' || remaining[i] == '/' { + breakPoint = i + break + } + } + + if firstLine { + output.WriteString(baseIndent + yellow.Sprint(remaining[:breakPoint])) + output.WriteString("\n") + firstLine = false + } else { + output.WriteString(baseIndent + " " + yellow.Sprint(remaining[:breakPoint])) + output.WriteString("\n") + } + remaining = strings.TrimSpace(remaining[breakPoint:]) + } + } else { + // Short command, print with standard indent + output.WriteString(baseIndent + yellow.Sprint(trimmed)) + output.WriteString("\n") + } + } else { + lastWasCommand = false + // Description line - indent all description text + maxDescWidth := termWidth - len(baseIndent) + trimmedExample := strings.TrimSpace(example) + + if len(trimmedExample) > maxDescWidth { + // Need to wrap + words := strings.Fields(trimmedExample) + currentLine := "" + for _, word := range words { + if currentLine == "" { + currentLine = word + } else if len(currentLine)+1+len(word) <= maxDescWidth { + currentLine += " " + word + } else { + output.WriteString(baseIndent + dim.Sprint(currentLine)) + output.WriteString("\n") + currentLine = word + } + } + if currentLine != "" { + output.WriteString(baseIndent + dim.Sprint(currentLine)) + output.WriteString("\n") + } + } else { + output.WriteString(baseIndent + dim.Sprint(trimmedExample)) + output.WriteString("\n") + } + } + } + output.WriteString("\n") + } + + // Footer with responsive separator + output.WriteString(cyan.Sprint(strings.Repeat("━", termWidth))) + output.WriteString("\n") + + // Simple footer that works at any width + docsUrl := "https://roots.io/trellis/docs/" + footer := " docs → " + docsUrl + + if len(footer) <= termWidth { + output.WriteString(dim.Sprint(" docs → ")) + output.WriteString(cyan.Sprint(docsUrl)) + output.WriteString("\n") + } else { + // For very narrow terminals, just show the URL + output.WriteString(cyan.Sprint(" " + docsUrl)) + output.WriteString("\n") + } + fmt.Print(output.String()) +} diff --git a/cmd/help_renderer.go b/cmd/help_renderer.go new file mode 100644 index 00000000..7c42900f --- /dev/null +++ b/cmd/help_renderer.go @@ -0,0 +1,300 @@ +package cmd + +import ( + "fmt" + "os" + "sort" + "strings" + + "github.com/hashicorp/cli" + "github.com/pterm/pterm" + "golang.org/x/term" +) + +// HelpRenderer defines the interface for rendering help output +type HelpRenderer interface { + // RenderMain renders the main help output for all commands + RenderMain(commands map[string]cli.CommandFactory, version string) string + + // RenderCommand renders help for a specific command + RenderCommand(commandName string, synopsis string, helpText string) string + + // RenderNamespace renders help for a namespace command + RenderNamespace(namespaceName string, synopsis string, subcommands map[string]string) string + + // ShouldIntercept returns true if this renderer needs to intercept help handling + ShouldIntercept() bool +} + +// GetHelpRenderer returns the appropriate help renderer based on the environment +func GetHelpRenderer() HelpRenderer { + // Check if we're in a terminal + isTerminal := term.IsTerminal(int(os.Stdout.Fd())) + + // For debugging: you can force pterm with TRELLIS_PTERM=1 + if os.Getenv("TRELLIS_PTERM") == "1" { + return &PtermHelpRenderer{} + } + + if !isTerminal { + return &PlainHelpRenderer{} + } + return &PtermHelpRenderer{} +} + +// PtermHelpRenderer renders beautiful help using pterm +type PtermHelpRenderer struct{} + +func (r *PtermHelpRenderer) ShouldIntercept() bool { + // Pterm renderer needs to intercept to prevent CLI framework issues + return true +} + +func (r *PtermHelpRenderer) RenderMain(commands map[string]cli.CommandFactory, version string) string { + // Define minimal color scheme - modern terminal aesthetic + dim := pterm.NewStyle(pterm.FgWhite) + brightWhite := pterm.NewStyle(pterm.FgLightWhite, pterm.Bold) + cyan := pterm.NewStyle(pterm.FgCyan) + green := pterm.NewStyle(pterm.FgGreen) + + // Get terminal width + termWidth := 80 + if width, _, err := term.GetSize(int(os.Stdout.Fd())); err == nil && width > 0 { + termWidth = width + } + + // Group commands by category + categories := map[string][]struct{ name, desc string }{ + "project": {}, + "dev": {}, + "deploy": {}, + "db": {}, + "security": {}, + "utils": {}, + } + + // Categorize commands (skip sub-commands and deprecated) + for name, factory := range commands { + // Skip sub-commands + if strings.Contains(name, " ") { + continue + } + + cmd, _ := factory() + if cmd == nil { + continue + } + + cmdInfo := struct{ name, desc string }{ + name: name, + desc: cmd.Synopsis(), + } + + // Categorize based on command name + switch { + case name == "new" || name == "init": + categories["project"] = append(categories["project"], cmdInfo) + case name == "up" || name == "down" || strings.HasPrefix(name, "vm") || name == "ssh" || name == "exec" || strings.HasPrefix(name, "valet"): + categories["dev"] = append(categories["dev"], cmdInfo) + case name == "deploy" || name == "provision" || name == "rollback": + categories["deploy"] = append(categories["deploy"], cmdInfo) + case strings.HasPrefix(name, "db"): + categories["db"] = append(categories["db"], cmdInfo) + case strings.HasPrefix(name, "vault") || strings.HasPrefix(name, "key"): + categories["security"] = append(categories["security"], cmdInfo) + default: + categories["utils"] = append(categories["utils"], cmdInfo) + } + } + + fmt.Println() + fmt.Println(cyan.Sprint("┌─╼ ") + brightWhite.Sprint("trellis") + dim.Sprint(" ─ ") + dim.Sprint(version)) + fmt.Println(cyan.Sprint("└─╼ ") + dim.Sprint("WordPress LEMP stack")) + fmt.Println() + fmt.Print(dim.Sprint("$ ")) + fmt.Print("trellis ") + fmt.Print(green.Sprint("")) + fmt.Println(dim.Sprint(" [args]")) + fmt.Println() + + // Display categories in order + categoryDisplay := []struct { + key string + label string + icon string + }{ + {"project", "PROJECT", "◉"}, + {"dev", "DEV", "◉"}, + {"deploy", "DEPLOY", "◉"}, + {"db", "DATABASE", "◉"}, + {"security", "SECURITY", "◉"}, + {"utils", "UTILS", "◉"}, + } + + for _, cat := range categoryDisplay { + cmds := categories[cat.key] + if len(cmds) == 0 { + continue + } + + // Sort commands alphabetically + sort.Slice(cmds, func(i, j int) bool { + return cmds[i].name < cmds[j].name + }) + + // Category header with icon (single space before) + fmt.Print(" ") + fmt.Print(cyan.Sprint(cat.icon + " ")) + fmt.Println(dim.Sprint(cat.label)) + + // Commands - clean indented list (with extra space before arrow) + const commandPrefixLen = 20 // Total visual length of " → command " + for _, cmd := range cmds { + // Build the prefix: " → command " + prefix := fmt.Sprintf(" %s %-14s ", green.Sprint("→"), cmd.name) + + // Word wrap with exact visual length of prefix + visualPrefixLen := commandPrefixLen + + desc := cmd.desc + descWidth := termWidth - visualPrefixLen + + fmt.Print(prefix) + + words := strings.Fields(desc) + currentLine := "" + firstLine := true + + for _, word := range words { + if currentLine == "" { + currentLine = word + } else if len(currentLine)+1+len(word) <= descWidth { + currentLine += " " + word + } else { + if firstLine { + fmt.Println(dim.Sprint(currentLine)) + firstLine = false + } else { + fmt.Printf("%*s%s\n", visualPrefixLen, "", dim.Sprint(currentLine)) + } + currentLine = word + } + } + + if currentLine != "" { + if firstLine { + fmt.Println(dim.Sprint(currentLine)) + } else { + fmt.Printf("%*s%s\n", visualPrefixLen, "", dim.Sprint(currentLine)) + } + } + } + fmt.Println() + } + + // Footer with responsive separator + fmt.Println(cyan.Sprint(strings.Repeat("━", termWidth))) + + // Calculate footer text length + footerLeft := " need more info? → trellis --help" + footerRight := "docs → https://roots.io/trellis/docs/" + footerSeparator := " | " + totalFooterLen := len(footerLeft) + len(footerSeparator) + len(footerRight) + + if totalFooterLen <= termWidth { + // Everything fits on one line + fmt.Print(dim.Sprint(" need more info? → ")) + fmt.Print("trellis ") + fmt.Print(green.Sprint("")) + fmt.Print(" --help") + fmt.Print(dim.Sprint(footerSeparator)) + fmt.Print(dim.Sprint("docs → ")) + fmt.Println(cyan.Sprint("https://roots.io/trellis/docs/")) + } else { + // Split into two lines for narrow terminals + fmt.Print(dim.Sprint(" need more info? → ")) + fmt.Print("trellis ") + fmt.Print(green.Sprint("")) + fmt.Println(" --help") + fmt.Print(dim.Sprint(" docs → ")) + fmt.Println(cyan.Sprint("https://roots.io/trellis/docs/")) + } + fmt.Println() + + return "" +} + +func (r *PtermHelpRenderer) RenderCommand(commandName string, synopsis string, helpText string) string { + // Use existing PtermHelpFunc + PtermHelpFunc(commandName, synopsis, helpText) + return "" +} + +func (r *PtermHelpRenderer) RenderNamespace(namespaceName string, synopsis string, subcommands map[string]string) string { + // Define color scheme + dim := pterm.NewStyle(pterm.FgWhite) + cyan := pterm.NewStyle(pterm.FgCyan) + green := pterm.NewStyle(pterm.FgGreen) + brightWhite := pterm.NewStyle(pterm.FgLightWhite, pterm.Bold) + + // Build styled output as a string + var output strings.Builder + + output.WriteString("\n") + output.WriteString(cyan.Sprint("┌─╼ ")) + output.WriteString(brightWhite.Sprint("trellis " + namespaceName)) + output.WriteString("\n") + output.WriteString(cyan.Sprint("└─╼ ")) + output.WriteString(dim.Sprint(synopsis)) + output.WriteString("\n\n") + + // Print usage + output.WriteString(dim.Sprint("$ ")) + output.WriteString(fmt.Sprintf("trellis %s []", namespaceName)) + output.WriteString("\n\n") + + // Print subcommands section + output.WriteString(" ") + output.WriteString(cyan.Sprint("◉ ")) + output.WriteString(dim.Sprint("SUBCOMMANDS")) + output.WriteString("\n\n") + + // Display subcommands + if len(subcommands) > 0 { + for cmdName, cmdDesc := range subcommands { + output.WriteString(fmt.Sprintf(" %s %-15s %s\n", + green.Sprint("→"), + cmdName, + dim.Sprint(cmdDesc))) + } + } + + output.WriteString("\n") + fmt.Print(output.String()) + return "" +} + +// PlainHelpRenderer renders plain text help (for tests and non-TTY) +type PlainHelpRenderer struct{} + +func (r *PlainHelpRenderer) ShouldIntercept() bool { + // Plain renderer doesn't need to intercept - let CLI framework handle it + return false +} + +func (r *PlainHelpRenderer) RenderMain(commands map[string]cli.CommandFactory, version string) string { + // Use the base CLI help for plain output + return cli.BasicHelpFunc("trellis")(commands) +} + +func (r *PlainHelpRenderer) RenderCommand(commandName string, synopsis string, helpText string) string { + // Return the raw help text for plain output + return helpText +} + +func (r *PlainHelpRenderer) RenderNamespace(namespaceName string, synopsis string, subcommands map[string]string) string { + // For plain renderer in non-TTY mode, we don't render anything here + // The namespace command's HelpText already contains the formatted help + // Returning empty string lets the framework handle it + return "" +} diff --git a/cmd/info.go b/cmd/info.go index 33dac3a7..cde5f373 100644 --- a/cmd/info.go +++ b/cmd/info.go @@ -55,5 +55,5 @@ Options: -h, --help show this help ` - return strings.TrimSpace(helpText) + return CreateHelp("info", c.Synopsis(), strings.TrimSpace(helpText)) } diff --git a/cmd/init.go b/cmd/init.go index 94ca43c0..4cee8350 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -148,7 +148,7 @@ Options: -h, --help show this help ` - return strings.TrimSpace(helpText) + return CreateHelp("init", c.Synopsis(), strings.TrimSpace(helpText)) } func virtualenvError(ui cli.Ui) { diff --git a/cmd/key_generate.go b/cmd/key_generate.go index e633de49..85be4e31 100644 --- a/cmd/key_generate.go +++ b/cmd/key_generate.go @@ -288,7 +288,7 @@ Options: -h, --help show this help ` - return strings.TrimSpace(helpText) + return CreateHelp("key-generate", c.Synopsis(), strings.TrimSpace(helpText)) } func (c *KeyGenerateCommand) AutocompleteArgs() complete.Predictor { diff --git a/cmd/logs.go b/cmd/logs.go index 04b10228..822cae36 100644 --- a/cmd/logs.go +++ b/cmd/logs.go @@ -189,7 +189,7 @@ Options: -h, --help Show this help ` - return strings.TrimSpace(helpText) + return CreateHelp("logs", c.Synopsis(), strings.TrimSpace(helpText)) } func (c *LogsCommand) AutocompleteArgs() complete.Predictor { diff --git a/cmd/namespace.go b/cmd/namespace.go index e71e6e32..75e9bee2 100644 --- a/cmd/namespace.go +++ b/cmd/namespace.go @@ -1,16 +1,30 @@ package cmd import ( + "fmt" + "strings" + "github.com/hashicorp/cli" ) type NamespaceCommand struct { - SynopsisText string - HelpText string + SynopsisText string + HelpText string + Subcommands map[string]string // name -> description mapping + calledFromRun bool // Internal flag to track if Help() is called from Run() } func (c *NamespaceCommand) Run(args []string) int { - return cli.RunResultHelp + // For test compatibility - empty namespace commands should return RunResultHelp + if c.Subcommands == nil && c.HelpText == "" && c.SynopsisText == "" { + return cli.RunResultHelp + } + + // Always show help for namespace commands + // Don't return cli.RunResultHelp as it causes subcommand help to show + c.calledFromRun = true + fmt.Print(c.Help()) + return 0 } func (c *NamespaceCommand) Synopsis() string { @@ -18,5 +32,55 @@ func (c *NamespaceCommand) Synopsis() string { } func (c *NamespaceCommand) Help() string { - return c.HelpText + // If running in test mode, return raw help text for backward compatibility + if c.Subcommands == nil && c.HelpText != "" && !strings.Contains(c.HelpText, "Usage:") { + return c.HelpText + } + + // Get the renderer + renderer := GetHelpRenderer() + + // If the renderer is PlainHelpRenderer, return the basic help text + if _, isPlain := renderer.(*PlainHelpRenderer); isPlain { + var output strings.Builder + if c.HelpText != "" { + output.WriteString(c.HelpText + "\n\n") + } + if c.SynopsisText != "" { + output.WriteString(c.SynopsisText + "\n") + } + + // Only add subcommands if called from Run() (not from --help) + // When --help is used, the framework adds subcommands automatically + if c.calledFromRun && len(c.Subcommands) > 0 { + output.WriteString("\nSubcommands:\n") + for name, desc := range c.Subcommands { + output.WriteString(fmt.Sprintf(" %-15s %s\n", name, desc)) + } + } + + return output.String() + } + + // For pterm renderer, parse namespace name and use fancy rendering + lines := strings.Split(c.HelpText, "\n") + var namespaceName string + if len(lines) > 0 && strings.HasPrefix(lines[0], "Usage: trellis ") { + parts := strings.Fields(lines[0]) + if len(parts) >= 3 { + namespaceName = parts[2] + } + } + + // Get subcommands from the namespaceCommands map in main + // This is needed because pterm renderer needs to know the subcommands + // but they're not stored in the NamespaceCommand struct anymore + var subcommands map[string]string + if namespaceName != "" { + // We need to get the subcommands from somewhere + // For now, we'll use what we have if available + subcommands = c.Subcommands + } + + return renderer.RenderNamespace(namespaceName, c.SynopsisText, subcommands) } diff --git a/cmd/new.go b/cmd/new.go index 2d2d46b7..33e874bb 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -14,6 +14,7 @@ import ( "github.com/roots/trellis-cli/github" "github.com/roots/trellis-cli/trellis" "github.com/weppos/publicsuffix-go/publicsuffix" + "golang.org/x/term" ) type NewCommand struct { @@ -53,7 +54,18 @@ func (c *NewCommand) Run(args []string) int { args = c.flags.Args() - commandArgumentValidator := &CommandArgumentValidator{required: 1, optional: 0} + // Check if we're in a terminal + isInteractive := term.IsTerminal(int(os.Stdout.Fd())) + + // If running in non-terminal (tests), require the path argument + var commandArgumentValidator *CommandArgumentValidator + if !isInteractive { + commandArgumentValidator = &CommandArgumentValidator{required: 1, optional: 0} + } else { + // In terminal mode, allow wizard (0 args) + commandArgumentValidator = &CommandArgumentValidator{required: 0, optional: 1} + } + commandArgumentErr := commandArgumentValidator.validate(args) if commandArgumentErr != nil { c.UI.Error(commandArgumentErr.Error()) @@ -61,7 +73,13 @@ func (c *NewCommand) Run(args []string) int { return 1 } - path := args[0] + path := "" + if len(args) > 0 { + path = args[0] + } else { + // Default to current directory if no path provided (wizard mode) + path = "." + } path, _ = filepath.Abs(path) fi, statErr := os.Stat(path) @@ -239,7 +257,7 @@ Options: -h, --help show this help ` - return strings.TrimSpace(helpText) + return CreateHelp("new", c.Synopsis(), strings.TrimSpace(helpText)) } func (c *NewCommand) YamlHeader(doc string) string { diff --git a/cmd/open.go b/cmd/open.go index 05e14469..0da9ed96 100644 --- a/cmd/open.go +++ b/cmd/open.go @@ -108,7 +108,7 @@ Options: -h, --help show this help ` - return strings.TrimSpace(helpText) + return CreateHelp("open", c.Synopsis(), strings.TrimSpace(helpText)) } func (c *OpenCommand) AutocompleteArgs() complete.Predictor { diff --git a/cmd/provision.go b/cmd/provision.go index 1efee591..7940672f 100644 --- a/cmd/provision.go +++ b/cmd/provision.go @@ -140,7 +140,7 @@ Options: -h, --help Show this help ` - return strings.TrimSpace(helpText) + return CreateHelp("provision", c.Synopsis(), strings.TrimSpace(helpText)) } func (c *ProvisionCommand) AutocompleteArgs() complete.Predictor { diff --git a/cmd/rollback.go b/cmd/rollback.go index 72bb35af..82274e78 100644 --- a/cmd/rollback.go +++ b/cmd/rollback.go @@ -126,7 +126,7 @@ Options: -h, --help show this help ` - return strings.TrimSpace(helpText) + return CreateHelp("rollback", c.Synopsis(), strings.TrimSpace(helpText)) } func (c *RollbackCommand) AutocompleteArgs() complete.Predictor { diff --git a/cmd/shell_init.go b/cmd/shell_init.go index 3d30de9d..b8e1077b 100644 --- a/cmd/shell_init.go +++ b/cmd/shell_init.go @@ -70,5 +70,5 @@ To activate the integration, add one of the following lines to your shell profil Options: -h, --help show this help ` - return strings.TrimSpace(helpText) + return CreateHelp("shell-init", c.Synopsis(), strings.TrimSpace(helpText)) } diff --git a/cmd/ssh.go b/cmd/ssh.go index 434ef8bd..057dff76 100644 --- a/cmd/ssh.go +++ b/cmd/ssh.go @@ -120,7 +120,7 @@ Options: -h, --help Show this help ` - return strings.TrimSpace(helpText) + return CreateHelp("ssh", c.Synopsis(), strings.TrimSpace(helpText)) } func (c *SshCommand) AutocompleteArgs() complete.Predictor { diff --git a/cmd/up.go b/cmd/up.go index 8bcdb96a..6f9271f1 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -123,7 +123,7 @@ Options: -h, --help show this help ` - return strings.TrimSpace(helpText) + return CreateHelp("up", c.Synopsis(), strings.TrimSpace(helpText)) } func (c *UpCommand) AutocompleteArgs() complete.Predictor { diff --git a/cmd/valet_link.go b/cmd/valet_link.go index 3b4c9175..d28a9d7b 100644 --- a/cmd/valet_link.go +++ b/cmd/valet_link.go @@ -90,5 +90,5 @@ Options: -h, --help show this help ` - return strings.TrimSpace(helpText) + return CreateHelp("valet-link", c.Synopsis(), strings.TrimSpace(helpText)) } diff --git a/cmd/vault_decrypt.go b/cmd/vault_decrypt.go index d77e71ce..ef43448c 100644 --- a/cmd/vault_decrypt.go +++ b/cmd/vault_decrypt.go @@ -163,7 +163,7 @@ Options: -h, --help Show this help ` - return strings.TrimSpace(helpText) + return CreateHelp("vault decrypt", c.Synopsis(), strings.TrimSpace(helpText)) } func (c *VaultDecryptCommand) AutocompleteArgs() complete.Predictor { diff --git a/cmd/vault_edit.go b/cmd/vault_edit.go index 9b3a9819..10b28c74 100644 --- a/cmd/vault_edit.go +++ b/cmd/vault_edit.go @@ -116,7 +116,7 @@ Options: -h, --help Show this help ` - return strings.TrimSpace(helpText) + return CreateHelp("vault edit", c.Synopsis(), strings.TrimSpace(helpText)) } func (c *VaultEditCommand) AutocompleteArgs() complete.Predictor { diff --git a/cmd/vault_encrypt.go b/cmd/vault_encrypt.go index b3c5fff7..ffea439f 100644 --- a/cmd/vault_encrypt.go +++ b/cmd/vault_encrypt.go @@ -165,7 +165,7 @@ Options: -h, --help Show this help ` - return strings.TrimSpace(helpText) + return CreateHelp("vault encrypt", c.Synopsis(), strings.TrimSpace(helpText)) } func (c *VaultEncryptCommand) AutocompleteArgs() complete.Predictor { diff --git a/cmd/vault_view.go b/cmd/vault_view.go index 4e9195e6..ede0386e 100644 --- a/cmd/vault_view.go +++ b/cmd/vault_view.go @@ -140,7 +140,7 @@ Options: -h, --help Show this help ` - return strings.TrimSpace(helpText) + return CreateHelp("vault view", c.Synopsis(), strings.TrimSpace(helpText)) } func (c *VaultViewCommand) AutocompleteArgs() complete.Predictor { diff --git a/cmd/venv_hook.go b/cmd/venv_hook.go index e6a72e24..98330bca 100644 --- a/cmd/venv_hook.go +++ b/cmd/venv_hook.go @@ -55,7 +55,7 @@ Options: -h, --help show this help ` - return strings.TrimSpace(helpText) + return CreateHelp("venv-hook", c.Synopsis(), strings.TrimSpace(helpText)) } func (c *VenvHookCommand) exportEnv(key string, value string) { diff --git a/cmd/vm_delete.go b/cmd/vm_delete.go index 58038644..f09d3190 100644 --- a/cmd/vm_delete.go +++ b/cmd/vm_delete.go @@ -92,7 +92,7 @@ Options: -h, --help Show this help ` - return strings.TrimSpace(helpText) + return CreateHelp("vm delete", c.Synopsis(), strings.TrimSpace(helpText)) } func (c *VmDeleteCommand) AutocompleteFlags() complete.Flags { diff --git a/cmd/vm_shell.go b/cmd/vm_shell.go index 0f63f19e..acc8c49c 100644 --- a/cmd/vm_shell.go +++ b/cmd/vm_shell.go @@ -101,5 +101,5 @@ Options: -h, --help Show this help ` - return strings.TrimSpace(helpText) + return CreateHelp("vm shell", c.Synopsis(), strings.TrimSpace(helpText)) } diff --git a/cmd/vm_start.go b/cmd/vm_start.go index 6348c5cf..2fa6b03a 100644 --- a/cmd/vm_start.go +++ b/cmd/vm_start.go @@ -116,7 +116,7 @@ Options: -h, --help show this help ` - return strings.TrimSpace(helpText) + return CreateHelp("vm start", c.Synopsis(), strings.TrimSpace(helpText)) } func (c *VmStartCommand) printInstanceInfo() { diff --git a/cmd/vm_stop.go b/cmd/vm_stop.go index 5d53a0a4..aded3017 100644 --- a/cmd/vm_stop.go +++ b/cmd/vm_stop.go @@ -81,5 +81,5 @@ Options: -h, --help show this help ` - return strings.TrimSpace(helpText) + return CreateHelp("vm stop", c.Synopsis(), strings.TrimSpace(helpText)) } diff --git a/cmd/vm_sudoers.go b/cmd/vm_sudoers.go index 037186e4..aa900c2a 100644 --- a/cmd/vm_sudoers.go +++ b/cmd/vm_sudoers.go @@ -57,5 +57,5 @@ Options: -h, --help show this help ` - return strings.TrimSpace(helpText) + return CreateHelp("vm sudoers", c.Synopsis(), strings.TrimSpace(helpText)) } diff --git a/cmd/xdebug_tunnel_close.go b/cmd/xdebug_tunnel_close.go index c4be1422..8167a147 100644 --- a/cmd/xdebug_tunnel_close.go +++ b/cmd/xdebug_tunnel_close.go @@ -97,7 +97,7 @@ Options: -h, --help show this help ` - return strings.TrimSpace(helpText) + return CreateHelp("xdebug-tunnel close", c.Synopsis(), strings.TrimSpace(helpText)) } func (c *XdebugTunnelCloseCommand) AutocompleteFlags() complete.Flags { diff --git a/cmd/xdebug_tunnel_open.go b/cmd/xdebug_tunnel_open.go index dd9334f9..3a5168d6 100644 --- a/cmd/xdebug_tunnel_open.go +++ b/cmd/xdebug_tunnel_open.go @@ -98,7 +98,7 @@ Options: -h, --help show this help ` - return strings.TrimSpace(helpText) + return CreateHelp("xdebug-tunnel open", c.Synopsis(), strings.TrimSpace(helpText)) } func (c *XdebugTunnelOpenCommand) AutocompleteFlags() complete.Flags { diff --git a/go.mod b/go.mod index b3839334..1b0e4983 100644 --- a/go.mod +++ b/go.mod @@ -14,16 +14,21 @@ require ( github.com/mholt/archives v0.1.3 github.com/mitchellh/go-homedir v1.1.0 github.com/posener/complete v1.2.3 + github.com/pterm/pterm v0.12.81 github.com/theckman/yacspin v0.13.12 github.com/weppos/publicsuffix-go v0.40.2 golang.org/x/crypto v0.41.0 golang.org/x/oauth2 v0.30.0 + golang.org/x/term v0.34.0 gopkg.in/alessio/shellescape.v1 v1.0.0-20170105083845-52074bc9df61 gopkg.in/ini.v1 v1.67.0 gopkg.in/yaml.v2 v2.4.0 ) require ( + atomicgo.dev/cursor v0.2.0 // indirect + atomicgo.dev/keyboard v0.2.9 // indirect + atomicgo.dev/schedule v0.1.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.2.0 // indirect github.com/Masterminds/sprig/v3 v3.2.3 // indirect @@ -37,9 +42,11 @@ require ( github.com/bodgit/windows v1.0.1 // indirect github.com/chzyer/readline v1.5.0 // indirect github.com/chzyer/test v1.0.0 // indirect + github.com/containerd/console v1.0.5 // indirect github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.3.0 // indirect + github.com/gookit/color v1.5.4 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -49,20 +56,22 @@ require ( github.com/imdario/mergo v0.3.13 // indirect github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/pgzip v1.2.6 // indirect + github.com/lithammer/fuzzysearch v1.1.8 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mikelolasagasti/xz v1.0.1 // indirect github.com/minio/minlz v1.0.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/nwaples/rardecode/v2 v2.1.0 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect - github.com/rivo/uniseg v0.2.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.8.1 // indirect github.com/shopspring/decimal v1.3.1 // indirect github.com/sorairolake/lzip-go v0.3.5 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/ulikunitz/xz v0.5.12 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go4.org v0.0.0-20230225012048-214862532bf5 // indirect golang.org/x/net v0.42.0 // indirect golang.org/x/sys v0.35.0 // indirect diff --git a/go.sum b/go.sum index 65112e68..519e8286 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,11 @@ +atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg= +atomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ= +atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw= +atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= +atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= +atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= +atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= +atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -18,6 +26,15 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= +github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= +github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= +github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k= +github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI= +github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c= +github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= +github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4= +github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= @@ -33,6 +50,7 @@ github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 h1:8PmGpDEZl9 github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU= @@ -56,6 +74,9 @@ github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= +github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -111,6 +132,10 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= +github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= +github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= +github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= github.com/hashicorp/cli v1.1.7 h1:/fZJ+hNdwfTSfsxMBa9WWMlfjUZbX8/LnUxgAd7lCVU= github.com/hashicorp/cli v1.1.7/go.mod h1:e6Mfpga9OCT1vqzFuoGZiiF/KaG9CbUfO5s3ghU3YgU= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -143,7 +168,13 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/cpuid v1.2.0 h1:NMpwD2G9JSFOE1/TJjGSo5zG7Yb2bTe7eq1jH+irmeE= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU= +github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -154,6 +185,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= +github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -161,8 +194,9 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mcuadros/go-version v0.0.0-20190830083331-035f6764e8d2 h1:YocNLcTBdEdvY3iDK6jfWXvEaM5OCKkjxPKoJRdB3Gg= github.com/mcuadros/go-version v0.0.0-20190830083331-035f6764e8d2/go.mod h1:76rfSfYPWj01Z85hUf/ituArm797mNKcvINh1OlsZKo= github.com/mholt/archives v0.1.3 h1:aEAaOtNra78G+TvV5ohmXrJOAzf++dIlYeDW3N9q458= @@ -189,12 +223,24 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= +github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= +github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= +github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU= +github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE= +github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8= +github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= +github.com/pterm/pterm v0.12.81 h1:ju+j5I2++FO1jBKMmscgh5h5DPFDFMB7epEjSoKehKA= +github.com/pterm/pterm v0.12.81/go.mod h1:TyuyrPjnxfwP+ccJdBTeWHtd/e0ybQHkOS/TakajZCw= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= @@ -209,6 +255,8 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -221,6 +269,9 @@ github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/weppos/publicsuffix-go v0.40.2 h1:LlnoSH0Eqbsi3ReXZWBKCK5lHyzf3sc1JEHH1cnlfho= github.com/weppos/publicsuffix-go v0.40.2/go.mod h1:XsLZnULC3EJ1Gvk9GVjuCTZ8QUu9ufE4TZpOizDShko= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -251,6 +302,8 @@ golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -334,12 +387,17 @@ golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -352,6 +410,8 @@ golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -456,16 +516,19 @@ gopkg.in/alessio/shellescape.v1 v1.0.0-20170105083845-52074bc9df61 h1:8ajkpB4hXV gopkg.in/alessio/shellescape.v1 v1.0.0-20170105083845-52074bc9df61/go.mod h1:IfMagxm39Ys4ybJrDb7W3Ob8RwxftP0Yy+or/NVz1O8= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/help.go b/help.go index 9a82ff2e..4c53290d 100644 --- a/help.go +++ b/help.go @@ -1,50 +1,15 @@ package main import ( - "bytes" - "fmt" - "sort" - "strings" - "github.com/hashicorp/cli" + "github.com/roots/trellis-cli/cmd" ) -func deprecatedCommandHelpFunc(commandNames []string, f cli.HelpFunc) cli.HelpFunc { +// ptermHelpFunc creates a help function that uses the renderer system +func ptermHelpFunc(version string, deprecatedCommands []string, baseHelp cli.HelpFunc) cli.HelpFunc { return func(commands map[string]cli.CommandFactory) string { - var buf bytes.Buffer - if len(commandNames) > 0 { - buf.WriteString("\n\nDeprecated commands:\n") - } - - maxKeyLen := 0 - keys := make([]string, 0, len(commandNames)) - filteredCommands := make(map[string]cli.CommandFactory) - - for key, command := range commands { - for _, deprecatedKey := range commandNames { - if key != deprecatedKey { - filteredCommands[key] = command - } - } - } - - for _, key := range commandNames { - if len(key) > maxKeyLen { - maxKeyLen = len(key) - } - - keys = append(keys, key) - } - - sort.Strings(keys) - - for _, key := range keys { - commandFunc := commands[key] - command, _ := commandFunc() - key = fmt.Sprintf("%s%s", key, strings.Repeat(" ", maxKeyLen-len(key))) - buf.WriteString(fmt.Sprintf(" %s %s\n", key, command.Synopsis())) - } - - return f(filteredCommands) + buf.String() + // Use the renderer system to generate help + renderer := cmd.GetHelpRenderer() + return renderer.RenderMain(commands, version) } } diff --git a/main.go b/main.go index cdd35a90..48f1b0b8 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "github.com/roots/trellis-cli/app_paths" "github.com/roots/trellis-cli/cmd" @@ -24,9 +25,233 @@ var deprecatedCommands = []string{ "up", } +// NamespaceInfo contains metadata about namespace commands +type NamespaceInfo struct { + Synopsis string + Subcommands map[string]string +} + +// namespaceCommands defines all namespace commands and their subcommands +var namespaceCommands = map[string]NamespaceInfo{ + "db": { + Synopsis: "Commands for database management", + Subcommands: map[string]string{ + "open": "Open database with GUI applications", + }, + }, + "droplet": { + Synopsis: "Commands for DigitalOcean Droplets", + Subcommands: map[string]string{ + "create": "Creates a DigitalOcean Droplet server and provisions it", + "dns": "Creates DNS records for all WordPress sites' hosts in an environment", + }, + }, + "galaxy": { + Synopsis: "Commands for Ansible Galaxy", + Subcommands: map[string]string{ + "install": "Installs Ansible Galaxy roles", + }, + }, + "key": { + Synopsis: "Commands for managing SSH keys", + Subcommands: map[string]string{ + "generate": "Generates an SSH key", + }, + }, + "vault": { + Synopsis: "Commands for Ansible Vault", + Subcommands: map[string]string{ + "edit": "Opens vault file in editor", + "encrypt": "Encrypts files with Ansible Vault", + "decrypt": "Decrypts files with Ansible Vault", + "view": "Views vault encrypted file contents", + }, + }, + "valet": { + Synopsis: "Commands for Laravel Valet", + Subcommands: map[string]string{ + "link": "Links a Trellis site for use with Laravel Valet", + }, + }, + "vm": { + Synopsis: "Commands for managing development virtual machines", + Subcommands: map[string]string{ + "delete": "Deletes the development virtual machine", + "shell": "Executes shell in the VM", + "start": "Starts a development virtual machine", + "stop": "Stops the development virtual machine", + "sudoers": "Generates sudoers content for passwordless updating of /etc/hosts", + }, + }, + "xdebug-tunnel": { + Synopsis: "Commands for Xdebug tunnel", + Subcommands: map[string]string{ + "open": "Opens a remote SSH tunnel to allow remote Xdebug connections", + "close": "Closes the remote SSH Xdebug tunnel", + }, + }, +} + +// Help renderer for the application +var helpRenderer cmd.HelpRenderer + +func preprocessArgsIfNeeded(args []string) ([]string, string) { + // Only preprocess if the renderer needs it (pterm renderer) + if !helpRenderer.ShouldIntercept() { + return args, "" + } + + if len(args) == 0 { + return args, "" + } + + showHelpFor := "" + // Check for help requests and remove --help from args + newArgs := []string{} + for i, arg := range args { + if arg == "--help" || arg == "-h" { + // Set help flag based on command context + if len(newArgs) == 0 { + showHelpFor = "main" + } else if len(newArgs) == 1 { + // Check if this is a namespace command + if _, isNamespace := namespaceCommands[newArgs[0]]; isNamespace { + showHelpFor = "namespace:" + newArgs[0] + } else { + // Let CLI framework handle regular commands + newArgs = append(newArgs, arg) + continue + } + } else { + // For subcommands like "db open --help", let CLI framework handle it + newArgs = append(newArgs, arg) + continue + } + // Don't add --help to newArgs + continue + } else if arg == "help" && i == 0 { + showHelpFor = "main" + continue + } + newArgs = append(newArgs, arg) + } + + return newArgs, showHelpFor +} + +func handleHelpRequest(showHelpFor string, version string) { + if showHelpFor == "main" { + // Show main help using the renderer + commands := createCommandMap() + helpRenderer.RenderMain(commands, version) + return + } + + if strings.HasPrefix(showHelpFor, "namespace:") { + namespaceName := strings.TrimPrefix(showHelpFor, "namespace:") + info, exists := namespaceCommands[namespaceName] + if !exists { + fmt.Printf("Unknown namespace: %s\n", namespaceName) + return + } + helpRenderer.RenderNamespace(namespaceName, info.Synopsis, info.Subcommands) + return + } +} + +func createCommandMap() map[string]cli.CommandFactory { + // Return a complete command map for help purposes with proper synopses + commands := map[string]cli.CommandFactory{ + // Project commands + "new": func() (cli.Command, error) { return &mockCommand{synopsis: "Creates a new Trellis project"}, nil }, + "init": func() (cli.Command, error) { + return &mockCommand{synopsis: "Initializes an existing Trellis project"}, nil + }, + + // Dev commands + "exec": func() (cli.Command, error) { + return &mockCommand{synopsis: "Exec runs a command in the Trellis virtualenv"}, nil + }, + "ssh": func() (cli.Command, error) { return &mockCommand{synopsis: "Connects to host via SSH"}, nil }, + "up": func() (cli.Command, error) { + return &mockCommand{synopsis: "Starts and provisions the Vagrant environment by running 'vagrant up'"}, nil + }, + "down": func() (cli.Command, error) { + return &mockCommand{synopsis: "Stops the Vagrant machine by running 'vagrant halt'"}, nil + }, + + // Deploy commands + "deploy": func() (cli.Command, error) { + return &mockCommand{synopsis: "Deploys a site to the specified environment"}, nil + }, + "provision": func() (cli.Command, error) { + return &mockCommand{synopsis: "Provisions the specified environment"}, nil + }, + "rollback": func() (cli.Command, error) { + return &mockCommand{synopsis: "Rollback the last deploy of the site on the specified environment"}, nil + }, + + // Utils commands + "alias": func() (cli.Command, error) { + return &mockCommand{synopsis: "Generate WP CLI aliases for remote environments"}, nil + }, + "check": func() (cli.Command, error) { + return &mockCommand{synopsis: "Checks if the required and optional Trellis dependencies are installed"}, nil + }, + "dotenv": func() (cli.Command, error) { return &mockCommand{synopsis: "Template .env files to local system"}, nil }, + "info": func() (cli.Command, error) { + return &mockCommand{synopsis: "Displays information about this Trellis project"}, nil + }, + "logs": func() (cli.Command, error) { + return &mockCommand{synopsis: "Tails the Nginx log files for an environment"}, nil + }, + "open": func() (cli.Command, error) { + return &mockCommand{synopsis: "Opens user-defined URLs (and more) which can act as shortcuts/bookmarks specific to your Trellis projects"}, nil + }, + "shell-init": func() (cli.Command, error) { + return &mockCommand{synopsis: "Prints a script which can be eval'd to set up Trellis' virtualenv integration in various shells"}, nil + }, + } + + // Add namespace commands from the centralized definition + for name, info := range namespaceCommands { + nameCopy := name // Capture loop variable + infoCopy := info // Capture loop variable + commands[nameCopy] = func() (cli.Command, error) { + return &cmd.NamespaceCommand{ + SynopsisText: infoCopy.Synopsis, + Subcommands: infoCopy.Subcommands, + }, nil + } + } + + return commands +} + +// mockCommand is a simple command implementation for help display +type mockCommand struct { + synopsis string +} + +func (m *mockCommand) Run([]string) int { return 0 } +func (m *mockCommand) Synopsis() string { return m.synopsis } +func (m *mockCommand) Help() string { return "" } + func main() { + // Initialize the help renderer based on environment + helpRenderer = cmd.GetHelpRenderer() + + // Preprocess args if needed (only for pterm renderer) + args, showHelpFor := preprocessArgsIfNeeded(os.Args[1:]) + + // Handle help requests if intercepted + if showHelpFor != "" { + handleHelpRequest(showHelpFor, version) + os.Exit(0) + } + c := cli.NewCLI("trellis", version) - c.Args = os.Args[1:] + c.Args = args ui := &cli.ColoredUi{ ErrorColor: cli.UiColorRed, @@ -67,9 +292,11 @@ func main() { return &cmd.CheckCommand{UI: ui, Trellis: trellis}, nil }, "db": func() (cli.Command, error) { + info := namespaceCommands["db"] return &cmd.NamespaceCommand{ HelpText: "Usage: trellis db []", - SynopsisText: "Commands for database management", + SynopsisText: info.Synopsis, + Subcommands: info.Subcommands, }, nil }, "db open": func() (cli.Command, error) { @@ -85,9 +312,11 @@ func main() { return &cmd.DownCommand{UI: ui, Trellis: trellis}, nil }, "droplet": func() (cli.Command, error) { + info := namespaceCommands["droplet"] return &cmd.NamespaceCommand{ HelpText: "Usage: trellis droplet []", - SynopsisText: "Commands for DigitalOcean Droplets", + SynopsisText: info.Synopsis, + Subcommands: info.Subcommands, }, nil }, "droplet create": func() (cli.Command, error) { @@ -100,9 +329,11 @@ func main() { return &cmd.ExecCommand{UI: ui, Trellis: trellis}, nil }, "galaxy": func() (cli.Command, error) { + info := namespaceCommands["galaxy"] return &cmd.NamespaceCommand{ HelpText: "Usage: trellis galaxy []", - SynopsisText: "Commands for Ansible Galaxy", + SynopsisText: info.Synopsis, + Subcommands: info.Subcommands, }, nil }, "galaxy install": func() (cli.Command, error) { @@ -115,9 +346,11 @@ func main() { return cmd.NewInitCommand(ui, trellis), nil }, "key": func() (cli.Command, error) { + info := namespaceCommands["key"] return &cmd.NamespaceCommand{ HelpText: "Usage: trellis key []", - SynopsisText: "Commands for managing SSH keys", + SynopsisText: info.Synopsis, + Subcommands: info.Subcommands, }, nil }, "key generate": func() (cli.Command, error) { @@ -148,9 +381,11 @@ func main() { return cmd.NewUpCommand(ui, trellis), nil }, "vault": func() (cli.Command, error) { + info := namespaceCommands["vault"] return &cmd.NamespaceCommand{ HelpText: "Usage: trellis vault []", - SynopsisText: "Commands for Ansible Vault", + SynopsisText: info.Synopsis, + Subcommands: info.Subcommands, }, nil }, "vault edit": func() (cli.Command, error) { @@ -166,9 +401,11 @@ func main() { return cmd.NewVaultViewCommand(ui, trellis), nil }, "valet": func() (cli.Command, error) { + info := namespaceCommands["valet"] return &cmd.NamespaceCommand{ HelpText: "Usage: trellis valet []", - SynopsisText: "Commands for Laravel Valet", + SynopsisText: info.Synopsis, + Subcommands: info.Subcommands, }, nil }, "valet link": func() (cli.Command, error) { @@ -178,9 +415,11 @@ func main() { return &cmd.VenvHookCommand{UI: ui, Trellis: trellis}, nil }, "vm": func() (cli.Command, error) { + info := namespaceCommands["vm"] return &cmd.NamespaceCommand{ HelpText: "Usage: trellis vm []", - SynopsisText: "Commands for managing development virtual machines", + SynopsisText: info.Synopsis, + Subcommands: info.Subcommands, }, nil }, "vm delete": func() (cli.Command, error) { @@ -199,9 +438,11 @@ func main() { return &cmd.VmSudoersCommand{UI: ui, Trellis: trellis}, nil }, "xdebug-tunnel": func() (cli.Command, error) { + info := namespaceCommands["xdebug-tunnel"] return &cmd.NamespaceCommand{ HelpText: "Usage: trellis xdebug-tunnel []", - SynopsisText: "Commands for Xdebug tunnel", + SynopsisText: info.Synopsis, + Subcommands: info.Subcommands, }, nil }, "xdebug-tunnel open": func() (cli.Command, error) { @@ -213,7 +454,9 @@ func main() { } c.HiddenCommands = []string{"venv", "venv hook"} - c.HelpFunc = deprecatedCommandHelpFunc(deprecatedCommands, cli.BasicHelpFunc("trellis")) + + // Use pterm for enhanced help + c.HelpFunc = ptermHelpFunc(version, deprecatedCommands, cli.BasicHelpFunc("trellis")) if trellis.CliConfig.LoadPlugins { pluginPaths := filepath.SplitList(os.Getenv("PATH"))