Skip to content
Merged
Show file tree
Hide file tree
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
13 changes: 12 additions & 1 deletion ai_tui_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,9 @@ func TestE2EDefaultHelpRoutesBareInvocation(t *testing.T) {

help := startTUISession(t, tuiSessionConfig{configDir: configDir, args: []string{}, rawFrame: true})
help.waitFor("(interactive AI session)") // the help screen's own example line
// The exit-code contract is part of the help screen, so a human (or an
// agent reading the same screen) learns it before the first failure.
help.waitFor("Exit codes: 0 success")
help.expectCleanExit()
}

Expand Down Expand Up @@ -873,10 +876,18 @@ func TestE2EAPICommandHelpRendersWithoutCredentials(t *testing.T) {
},
})
session.waitFor("Returns every widget the account can see.")
// The command's help carries only the persistent flags that act on a
// list (help_flags.go): --search inline, the table flags folded into
// the pointer line, the report flags gone.
session.waitFor("Output flags (apply to every command; add --help-full to list them):")
session.expectCleanExit()
if text := session.snapshot(); strings.Contains(text, "Open your browser") || strings.Contains(text, "credentials") {
text := session.snapshot()
if strings.Contains(text, "Open your browser") || strings.Contains(text, "credentials") {
t.Fatalf("help started a login or blamed credentials:\n%s", text)
}
if !helpListsFlag(text, "search") || helpListsFlag(text, "chart") || helpListsFlag(text, "table-mode") {
t.Fatalf("list-widgets help lists the wrong persistent flags:\n%s", text)
}
if atomic.LoadInt32(&authorized) != 0 || atomic.LoadInt32(&authorizeHits) != 0 {
t.Fatalf("help authenticated: %d authorized requests, %d OAuth endpoint hits", authorized, authorizeHits)
}
Expand Down
1 change: 1 addition & 0 deletions docs_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ var docsEntryPoints = [][2]string{
{"Embedded agent guidance", "dci skill <claude|codex|cursor|gemini|kiro|opencode>"},
{"Find a command for a task", "dci commands --search \"<task in plain words>\""},
{"Machine-readable commands", "dci commands --json"},
{"Exit codes and errors", "dci --help (Exit codes paragraph); full table: https://help.doit.com/docs/cli#exit-codes"},
}

func newDocsCommand() *cobra.Command {
Expand Down
2 changes: 2 additions & 0 deletions docs_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ func TestDocsCommandListsHumanAndAgentEntryPoints(t *testing.T) {
"dci skill",
"dci commands --search",
"dci commands --json",
"Exit codes and errors",
"https://help.doit.com/docs/cli#exit-codes",
} {
if !strings.Contains(output.String(), expected) {
t.Errorf("docs output missing %q", expected)
Expand Down
1 change: 0 additions & 1 deletion help_context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,4 +199,3 @@ func TestDeprecatedCommandHelpShowsNotice(t *testing.T) {
t.Fatalf("expected deprecation notice in --help output, got:\n%s", got)
}
}

303 changes: 303 additions & 0 deletions help_flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
// Chapter: per-command flag visibility in help. Every presentation flag is
// a persistent flag of the hidden dci API command, so each command's --help
// used to append all ~30 of them — most inert for that command — and bury
// the command's own flags under --chart, --pivot, --for-reimport and
// friends. This chapter keeps every flag accepted everywhere (no script
// breaks) and hides from a command's help the ones that cannot act on it,
// deciding by the same signals the pipeline uses when it acts: the
// report/query result shape (charts, pivot, rollup, row caps), the
// file-shaped exports (export_pagination.go's fileExportOperations), the
// page-token paging of list commands (pagination.go), the list-insights
// view (response_transform.go), and a resolvable positional argument
// (name_resolution.go). The table-presentation flags that apply to every
// command fold into one pointer line; --help-full lists everything. `dci
// commands --json` is unaffected: flags are hidden only while help renders
// and restored right after.
package main

import (
"sort"
"strings"

"github.com/spf13/cobra"
"github.com/spf13/pflag"
)

// helpFlagScope names what a dci persistent flag acts on. Every flag
// addOutputFlag registers must appear in helpFlagScopes (help_flags_test.go
// enforces it) so a new flag is classified deliberately rather than
// defaulting to inline everywhere.
type helpFlagScope int

const (
// helpFlagAlways is the agent contract plus the flags that act on every
// API call: inline in every command's help.
helpFlagAlways helpFlagScope = iota
// helpFlagPresentation shapes table/TOON rendering of any response:
// applies everywhere, so it folds into the one-line pointer instead of
// repeating under every command.
helpFlagPresentation
// helpFlagReport acts on report/query result rows (schema + rows).
helpFlagReport
// helpFlagExport acts on a file-shaped export body (CSV/NDJSON).
helpFlagExport
// helpFlagAllPages is --all: follows page tokens on paged lists and
// walks the time windows of a file export.
helpFlagAllPages
// helpFlagListSearch is --search: filters the items of a list response.
helpFlagListSearch
// helpFlagInsights is the list-insights view's own switch.
helpFlagInsights
// helpFlagResolution governs name resolution of a positional argument.
helpFlagResolution
)

var helpFlagScopes = map[string]helpFlagScope{
"output": helpFlagAlways,
"fields": helpFlagAlways,
"exclude": helpFlagAlways,
"full": helpFlagAlways,
"dry-run": helpFlagAlways,
"yes": helpFlagAlways,
"customer-context": helpFlagAlways,

"table-mode": helpFlagPresentation,
"table-columns": helpFlagPresentation,
"table-width": helpFlagPresentation,
"table-max-col-width": helpFlagPresentation,
"no-truncate": helpFlagPresentation,
"raw-numbers": helpFlagPresentation,
"utc": helpFlagPresentation,
"output-order": helpFlagPresentation,
// --output-file writes any command's response to a file, but it is the
// primary way to keep a file-shaped export: inline there, folded elsewhere.
"output-file": helpFlagPresentation,

"chart": helpFlagReport,
"pivot": helpFlagReport,
"flat": helpFlagReport,
"rollup": helpFlagReport,
"max-rows": helpFlagReport,
"rows": helpFlagReport,
"include-empty-rows": helpFlagReport,
"drop-unlabeled-rows": helpFlagReport,
"heatmap": helpFlagReport,

"for-reimport": helpFlagExport,

"all": helpFlagAllPages,
"search": helpFlagListSearch,

"include-dismissed": helpFlagInsights,

"id": helpFlagResolution,
"name": helpFlagResolution,
}

// reportResultOperations are the operations whose response carries report
// rows (`result.rows`/`results.rows` beside a `schema`) — the shape
// nestedReportRows detects at render time and every report flag acts on.
// Help needs the answer before any response exists, so the set is spelled
// out here, keyed by command name — the beta subtree names its commands by
// x-cli-name, so the async results operation appears twice (GA spelling and
// beta spelling). help_flags_test.go checks the set against the response
// schemas of the live GA spec (DCI_COMMAND_DOCS_SPEC, fetched in CI) and
// the embedded beta spec.
var reportResultOperations = map[string]bool{
"query": true,
"get-report": true,
"get-async-operation-results": true,
"get-report-results": true, // beta: getAsyncOperationResults
}
Comment thread
spark2ignite marked this conversation as resolved.

// helpFlagPlacement is where a dci persistent flag lands in one command's
// help: inline in the Flags block, folded into the output-flags pointer
// line, or hidden because it cannot act on that command.
type helpFlagPlacement int

const (
helpFlagInline helpFlagPlacement = iota
helpFlagCollapsed
helpFlagHidden
)

func helpFlagPlacementFor(flagName string, cmd *cobra.Command) helpFlagPlacement {
shown := func(applies bool) helpFlagPlacement {
if applies {
return helpFlagInline
}
return helpFlagHidden
}
scope, known := helpFlagScopes[flagName]
if !known {
return helpFlagInline
}
switch scope {
case helpFlagPresentation:
if flagName == "output-file" && helpCommandIsFileExport(cmd) {
return helpFlagInline
}
return helpFlagCollapsed
case helpFlagReport:
return shown(helpCommandIsReportShaped(cmd))
case helpFlagExport:
return shown(helpCommandIsFileExport(cmd))
case helpFlagAllPages:
return shown(helpCommandPages(cmd) || helpCommandIsFileExport(cmd))
case helpFlagListSearch:
// A file body has no items to filter; the preflight rejects --search
// on exports even though they page.
return shown(helpCommandIsList(cmd) && !helpCommandIsFileExport(cmd))
case helpFlagInsights:
return shown(cmd.Name() == "list-insights")
case helpFlagResolution:
return shown(helpCommandResolvesNames(cmd))
}
return helpFlagInline
}

// helpCommandUnderAPI reports whether cmd is a command under the hidden dci
// API command — a GA operation, a question command, or the beta subtree —
// i.e. one that inherits the dci persistent flags. The dci command itself
// and the local root commands (status, login, docs …) render unchanged.
func helpCommandUnderAPI(cmd, dciCmd *cobra.Command) bool {
if cmd == nil || dciCmd == nil || cmd == dciCmd {
return false
}
for parent := cmd.Parent(); parent != nil; parent = parent.Parent() {
if parent == dciCmd {
return true
}
}
return false
}

func helpCommandIsReportShaped(cmd *cobra.Command) bool {
return reportResultOperations[cmd.Name()]
}

func helpCommandIsFileExport(cmd *cobra.Command) bool {
_, ok := fileExportOperations[cmd.Name()]
return ok
}

// helpCommandPages mirrors operationHasQueryParam(op, "pageToken"): restish
// registers every query parameter as a local flag named after it, and the
// question commands register --page-token by hand.
func helpCommandPages(cmd *cobra.Command) bool {
return cmd.LocalFlags().Lookup("page-token") != nil
}

// helpCommandIsList reports whether the command answers with a collection
// --search can filter: anything that pages, the list-* and search-*
// operations (some return their whole collection in one response), the
// curated list views, and the question commands wrapping a list.
func helpCommandIsList(cmd *cobra.Command) bool {
name := cmd.Name()
if helpCommandPages(cmd) || questionCommandNames[name] {
return true
}
if _, ok := listViews[name]; ok {
return true
}
return strings.HasPrefix(name, "list-") || strings.HasPrefix(name, "search-")
}

// helpCommandResolvesNames reports whether --id/--name have a positional
// argument to govern. The resolution index is authoritative when loaded;
// help usually renders before any operation metadata is read, so the
// fallback is the command's own usage line: exactly one path parameter
// after the name ("get-budget id"), the only arity buildResolutionIndex
// ever resolves — a two-parameter command (create-ticket-comment
// customerid ticketid) is never resolvable.
func helpCommandResolvesNames(cmd *cobra.Command) bool {
if len(resolutionIndex) > 0 {
_, ok := resolutionIndex[cmd.Name()]
return ok
}
return len(strings.Fields(cmd.Use)) == 2
}

// helpFlagsFolded is true between applyHelpFlagVisibility and its restore:
// the pointer line renders only when the flags it stands in for were
// actually hidden. The usage template is shared with cobra's own usage dump
// on a flag-parse error, where nothing was folded and the full inherited
// list follows — a pointer line there would contradict it.
var helpFlagsFolded bool

// applyHelpFlagVisibility hides, for the duration of one help render, the
// dci persistent flags that cannot act on cmd (and the presentation flags
// the pointer line stands in for). It returns the restore function; the
// flags are shared with every other command and with the catalog, so the
// caller defers it. --help-full lists every flag and so does nothing here.
func applyHelpFlagVisibility(cmd *cobra.Command) func() {
dciCmd := findDCICommand()
if helpFullRequested || !helpCommandUnderAPI(cmd, dciCmd) {
return func() {}
}
var hidden []*pflag.Flag
dciCmd.PersistentFlags().VisitAll(func(flag *pflag.Flag) {
if flag.Hidden || helpFlagPlacementFor(flag.Name, cmd) == helpFlagInline {
return
}
flag.Hidden = true
hidden = append(hidden, flag)
})
helpFlagsFolded = true
return func() {
helpFlagsFolded = false
for _, flag := range hidden {
flag.Hidden = false
}
}
}

// helpCollapsedFlagsNote renders the pointer line for the presentation
// flags applyHelpFlagVisibility folded away: the usage template appends it
// after the Flags block. Empty whenever nothing was folded — commands
// outside the API subtree, --help-full, and the usage dump cobra prints on
// a flag-parse error.
func helpCollapsedFlagsNote(cmd *cobra.Command) string {
dciCmd := findDCICommand()
if !helpFlagsFolded || helpFullRequested || !helpCommandUnderAPI(cmd, dciCmd) {
return ""
}
labels := make([]string, 0)
dciCmd.PersistentFlags().VisitAll(func(flag *pflag.Flag) {
if helpFlagPlacementFor(flag.Name, cmd) != helpFlagCollapsed {
return
}
label := "--" + flag.Name
if flag.Shorthand != "" {
label = "-" + flag.Shorthand + "/" + label
}
labels = append(labels, label)
})
if len(labels) == 0 {
return ""
}
sort.Strings(labels)
return "Output flags (apply to every command; add --help-full to list them):\n" +
wrapHelpItems(labels, " ", 100)
}
Comment thread
spark2ignite marked this conversation as resolved.

// wrapHelpItems joins items with ", " into lines no wider than width, each
// prefixed with indent.
func wrapHelpItems(items []string, indent string, width int) string {
var lines []string
current := indent
for _, item := range items {
if current != indent && len(current)+len(", ")+len(item) > width {
lines = append(lines, current)
current = indent
}
if current != indent {
current += ", "
}
current += item
}
if current != indent {
lines = append(lines, current)
}
return strings.Join(lines, "\n")
}
Loading
Loading