Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 69 additions & 17 deletions pkg/cmd/root/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/spf13/pflag"

"github.com/instill-ai/cli/pkg/cmdutil"
"github.com/instill-ai/cli/pkg/markdown"
"github.com/instill-ai/cli/pkg/text"
)

Expand Down Expand Up @@ -44,6 +45,11 @@ func rootFlagErrorFunc(cmd *cobra.Command, err error) error {

var hasFailed bool

type helpEntry struct {
Title string
Body string
}

// HasFailed signals that the main process should exit with non-zero status
func HasFailed() bool {
return hasFailed
Expand Down Expand Up @@ -81,7 +87,7 @@ func isRootCmd(command *cobra.Command) bool {
}

func rootHelpFunc(f *cmdutil.Factory, command *cobra.Command, args []string) {
cs := f.IOStreams.ColorScheme()
io := f.IOStreams

if isRootCmd(command.Parent()) && len(args) >= 2 && args[1] != "--help" && args[1] != "-h" {
nestedSuggestFunc(command, args[1])
Expand Down Expand Up @@ -130,59 +136,105 @@ func rootHelpFunc(f *cmdutil.Factory, command *cobra.Command, args []string) {
"\n\nFor more information about output formatting flags, see `inst help formatting`."
}

helpEntries := []helpEntry{}
entries := []helpEntry{}
if longText != "" {
helpEntries = append(helpEntries, helpEntry{"", longText})
entries = append(entries, helpEntry{"", longText})
}
helpEntries = append(helpEntries, helpEntry{"USAGE", command.UseLine()})
entries = append(entries, helpEntry{"USAGE", command.UseLine()})
if len(coreCommands) > 0 {
helpEntries = append(helpEntries, helpEntry{"CORE COMMANDS", strings.Join(coreCommands, "\n")})
entries = append(entries, helpEntry{"CORE COMMANDS", strings.Join(coreCommands, "\n")})
}
if len(actionsCommands) > 0 {
helpEntries = append(helpEntries, helpEntry{"ACTIONS COMMANDS", strings.Join(actionsCommands, "\n")})
entries = append(entries, helpEntry{"ACTIONS COMMANDS", strings.Join(actionsCommands, "\n")})
}
if len(additionalCommands) > 0 {
helpEntries = append(helpEntries, helpEntry{"ADDITIONAL COMMANDS", strings.Join(additionalCommands, "\n")})
entries = append(entries, helpEntry{"ADDITIONAL COMMANDS", strings.Join(additionalCommands, "\n")})
}

flagUsages := command.LocalFlags().FlagUsages()
if flagUsages != "" {
helpEntries = append(helpEntries, helpEntry{"FLAGS", dedent(flagUsages)})
entries = append(entries, helpEntry{"FLAGS", dedent(flagUsages)})
}
inheritedFlagUsages := command.InheritedFlags().FlagUsages()
if inheritedFlagUsages != "" {
helpEntries = append(helpEntries, helpEntry{"INHERITED FLAGS", dedent(inheritedFlagUsages)})
entries = append(entries, helpEntry{"INHERITED FLAGS", dedent(inheritedFlagUsages)})
}
if _, ok := command.Annotations["help:arguments"]; ok {
helpEntries = append(helpEntries, helpEntry{"ARGUMENTS", command.Annotations["help:arguments"]})
entries = append(entries, helpEntry{"ARGUMENTS", command.Annotations["help:arguments"]})
}
if command.Example != "" {
helpEntries = append(helpEntries, helpEntry{"EXAMPLES", command.Example})
entries = append(entries, helpEntry{"EXAMPLES", command.Example})
}
if _, ok := command.Annotations["help:environment"]; ok {
helpEntries = append(helpEntries, helpEntry{"ENVIRONMENT VARIABLES", command.Annotations["help:environment"]})
entries = append(entries, helpEntry{"ENVIRONMENT VARIABLES", command.Annotations["help:environment"]})
}
helpEntries = append(helpEntries, helpEntry{"LEARN MORE", `
entries = append(entries, helpEntry{"LEARN MORE", `
Use 'inst <command> <subcommand> --help' for more information about a command.
Read the manual at https://www.instill.tech/docs`})
if _, ok := command.Annotations["help:feedback"]; ok {
helpEntries = append(helpEntries, helpEntry{"FEEDBACK", command.Annotations["help:feedback"]})
entries = append(entries, helpEntry{"FEEDBACK", command.Annotations["help:feedback"]})
}

// Build markdown for rendering
md := buildHelpMarkdown(entries)

Comment on lines +178 to +180
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

entries is declared as []helpEntry from a function-local helpEntry type (declared earlier in rootHelpFunc), but buildHelpMarkdown expects the package-level helpEntry type. Because these are distinct types in Go, this call won’t compile. Remove the function-local type helpEntry (or rename it) so entries uses the package-level type.

Copilot uses AI. Check for mistakes.
// Render with glamour when output is TTY for better readability
if io.IsStdoutTTY() {
wrapWidth := io.TerminalWidth()
style := markdown.GetStyle(io.DetectTerminalTheme())
rendered, err := markdown.RenderWithWrap(md, style, wrapWidth)
if err == nil {
_ = io.StartPager()
defer io.StopPager()
fmt.Fprint(io.Out, rendered)
Comment on lines +186 to +189
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This starts the pager whenever stdout is a TTY and markdown rendering succeeds (subject to pager config), regardless of output length. The PR description says to use a pager for long help output; if that’s still the intent, add a length/height check before starting the pager (e.g., compare rendered line count to terminal height) and print directly when it’s short.

Copilot uses AI. Check for mistakes.
return
Comment on lines +178 to +190
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New behavior is introduced here (markdown construction + TTY-only glamour rendering + plain-text fallback), but there are no tests covering the generated markdown or the TTY/non-TTY branching. Adding unit tests for buildHelpMarkdown and an integration-style test that asserts which renderer is used based on IOStreams.IsStdoutTTY() would help prevent regressions in help output formatting.

Copilot uses AI. Check for mistakes.
}
// Fall back to plain output on render error
}

// Plain output when not TTY or on render error
out := command.OutOrStdout()
for _, e := range helpEntries {
cs := io.ColorScheme()
for _, e := range entries {
if e.Title != "" {
// If there is a title, add indentation to each line in the body
fmt.Fprintln(out, cs.Bold(e.Title))
fmt.Fprintln(out, text.Indent(strings.Trim(e.Body, "\r\n"), " "))
} else {
// If there is no title print the body as is
fmt.Fprintln(out, e.Body)
}
fmt.Fprintln(out)
}
}

// buildHelpMarkdown converts help entries to markdown format for glamour rendering.
func buildHelpMarkdown(entries []helpEntry) string {
codeBlockSections := map[string]bool{
"USAGE": true, "FLAGS": true, "INHERITED FLAGS": true,
"EXAMPLES": true, "ARGUMENTS": true,
"ENVIRONMENT VARIABLES": true, "LEARN MORE": true, "FEEDBACK": true,
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The command list sections (e.g., CORE/ACTIONS/ADDITIONAL COMMANDS) build strings with padded spaces for alignment, but those sections are not treated as code blocks here. In markdown, consecutive spaces are collapsed outside code blocks, so the alignment (and possibly line formatting) will be lost in the glamour-rendered help. Consider adding those section titles to codeBlockSections or emitting them as a proper markdown list/table.

Suggested change
"ENVIRONMENT VARIABLES": true, "LEARN MORE": true, "FEEDBACK": true,
"ENVIRONMENT VARIABLES": true, "LEARN MORE": true, "FEEDBACK": true,
// Sections that list commands with padded spaces for alignment
"CORE": true, "ACTIONS": true, "ADDITIONAL COMMANDS": true,

Copilot uses AI. Check for mistakes.
}
var buf strings.Builder
for _, e := range entries {
if e.Title != "" {
buf.WriteString("## ")
buf.WriteString(e.Title)
buf.WriteString("\n\n")
if codeBlockSections[e.Title] {
buf.WriteString("```\n")
}
buf.WriteString(strings.Trim(e.Body, "\r\n"))
if codeBlockSections[e.Title] {
buf.WriteString("\n```")
}
buf.WriteString("\n\n")
} else {
buf.WriteString(e.Body)
buf.WriteString("\n\n")
}
}
return buf.String()
}

// rpad adds padding to the right of a string.
func rpad(s string, padding int) string {
template := fmt.Sprintf("%%-%ds ", padding)
Expand Down
Loading