[object Object]

← back to Cli Printing Press

fix(cli): hide raw resource groups when api browser is generated (#1045)

ed693b0d36b52746b8b43e2a075cd55cac44c0c9 · 2026-05-11 03:18:14 -0700 · Trevin Chow

* fix(cli): hide raw resource groups when api browser is generated

The api browser (api_discovery.go) filters root.Commands() by child.Hidden
to enumerate the raw API surface, but the generator's root template never
marked any command Hidden. Every printed CLI with the api command returned
"No hidden API interfaces found", and --help listed every raw resource at
top level.

renderResourceCommands now sets Hidden: true on the top-level parent
command for each raw resource when the spec produces promoted commands
(the same condition that triggers api_discovery.go emission). Sub-resource
parents stay visible. Direct invocation is unaffected since Cobra's Hidden
only suppresses listing.

Closes #872

* fix(cli): agent-context surfaces hidden resource groups and their endpoints

PR review surfaced a downstream regression in the agent-context discovery
JSON: collectAgentCommands filtered children by cmd.Hidden, so the raw
resource parents marked Hidden in the prior commit (and every endpoint
subcommand under them) silently disappeared from the documented agent
introspection surface.

Cobra's Hidden flag is a --help curation tool, not a "secret from agents"
signal. The agent-context surface must keep enumerating these commands so
agents retain the same reachable action set as a CLI user. The
agent-context self-reference skip stays as-is.

Files touched

Diff

commit ed693b0d36b52746b8b43e2a075cd55cac44c0c9
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Mon May 11 03:18:14 2026 -0700

    fix(cli): hide raw resource groups when api browser is generated (#1045)
    
    * fix(cli): hide raw resource groups when api browser is generated
    
    The api browser (api_discovery.go) filters root.Commands() by child.Hidden
    to enumerate the raw API surface, but the generator's root template never
    marked any command Hidden. Every printed CLI with the api command returned
    "No hidden API interfaces found", and --help listed every raw resource at
    top level.
    
    renderResourceCommands now sets Hidden: true on the top-level parent
    command for each raw resource when the spec produces promoted commands
    (the same condition that triggers api_discovery.go emission). Sub-resource
    parents stay visible. Direct invocation is unaffected since Cobra's Hidden
    only suppresses listing.
    
    Closes #872
    
    * fix(cli): agent-context surfaces hidden resource groups and their endpoints
    
    PR review surfaced a downstream regression in the agent-context discovery
    JSON: collectAgentCommands filtered children by cmd.Hidden, so the raw
    resource parents marked Hidden in the prior commit (and every endpoint
    subcommand under them) silently disappeared from the documented agent
    introspection surface.
    
    Cobra's Hidden flag is a --help curation tool, not a "secret from agents"
    signal. The agent-context surface must keep enumerating these commands so
    agents retain the same reachable action set as a CLI user. The
    agent-context self-reference skip stays as-is.
---
 internal/generator/generator.go                    |  10 +-
 internal/generator/generator_test.go               | 144 ++++++++++++++++++++-
 internal/generator/templates/agent_context.go.tmpl |  16 ++-
 .../internal/cli/agent_context.go                  |  16 ++-
 4 files changed, 174 insertions(+), 12 deletions(-)

diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index 28a0885f..947ffccb 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -1714,6 +1714,14 @@ func buildPromotedCommandPlan(apiSpec *spec.APISpec) ([]PromotedCommand, map[str
 }
 
 func (g *Generator) renderResourceCommands(promotedResourceNames map[string]bool, promotedEndpointNames map[string]string) error {
+	// When the spec emits promoted commands, the generator also emits the api
+	// browser (api_discovery.go), whose RunE filters root.Commands() by
+	// child.Hidden. Mark the raw top-level resource groups Hidden so they
+	// surface there instead of cluttering --help. Direct invocation still
+	// works (Cobra's Hidden only suppresses listing). For specs without
+	// promoted commands the api browser is not generated, so leave resources
+	// visible.
+	hideTopLevelResources := len(g.PromotedCommands) > 0
 	// Generate per-resource parent files + per-endpoint command files
 	// This produces more files (one per endpoint) which improves Breadth scoring
 	for name, resource := range g.Spec.Resources {
@@ -1733,7 +1741,7 @@ func (g *Generator) renderResourceCommands(promotedResourceNames map[string]bool
 				FuncPrefix:   name,
 				CommandPath:  name,
 				Resource:     resource,
-				Hidden:       false,
+				Hidden:       hideTopLevelResources,
 				APISpec:      g.Spec,
 			}
 			parentPath := filepath.Join("internal", "cli", safeResourceFileStem(name)+".go")
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index dd261603..943720b5 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -4414,6 +4414,143 @@ func TestGeneratedOutput_PromotedCommandCompiles(t *testing.T) {
 	runGoCommand(t, outputDir, "build", "./...")
 }
 
+func TestGeneratedOutput_ResourceParentsHiddenWhenAPIBrowserGenerated(t *testing.T) {
+	t.Parallel()
+
+	// Multi-endpoint resource -> parent group; single-endpoint resource -> promoted command.
+	// The promoted command's presence is what triggers api_discovery.go emission, and the
+	// api browser's RunE filters on child.Hidden. Without this fix the browser was empty by
+	// construction (issue #872).
+	apiSpec := &spec.APISpec{
+		Name:    "hiddentest",
+		Version: "0.1.0",
+		BaseURL: "https://api.example.com",
+		Auth:    spec.AuthConfig{Type: "api_key", Header: "X-Api-Key", EnvVars: []string{"HT_API_KEY"}},
+		Config:  spec.ConfigSpec{Format: "toml", Path: "~/.config/hiddentest-pp-cli/config.toml"},
+		Resources: map[string]spec.Resource{
+			"orders": {
+				Description: "Manage orders",
+				Endpoints: map[string]spec.Endpoint{
+					"list":   {Method: "GET", Path: "/orders", Description: "List orders"},
+					"create": {Method: "POST", Path: "/orders", Description: "Create order"},
+				},
+			},
+			"customers": {
+				Description: "Single-endpoint customers resource (gets promoted)",
+				Endpoints: map[string]spec.Endpoint{
+					"list": {Method: "GET", Path: "/customers", Description: "List customers"},
+				},
+			},
+		},
+	}
+
+	outputDir := filepath.Join(t.TempDir(), "hiddentest-pp-cli")
+	gen := New(apiSpec, outputDir)
+	require.NoError(t, gen.Generate())
+
+	require.FileExists(t, filepath.Join(outputDir, "internal", "cli", "api_discovery.go"))
+	require.FileExists(t, filepath.Join(outputDir, "internal", "cli", "promoted_customers.go"))
+
+	orders, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "orders.go"))
+	require.NoError(t, err)
+	assert.Contains(t, string(orders), "Hidden: true",
+		"raw resource parent must be Hidden so the api browser finds it")
+}
+
+func TestGeneratedOutput_ResourceParentsNotHiddenWithoutAPIBrowser(t *testing.T) {
+	t.Parallel()
+
+	// Without any single-endpoint resource to promote, api_discovery.go is not generated;
+	// hiding the resources in that case would just collapse --help without giving users
+	// a way to list them, so the parent files stay visible.
+	apiSpec := &spec.APISpec{
+		Name:    "novisibletest",
+		Version: "0.1.0",
+		BaseURL: "https://api.example.com",
+		Auth:    spec.AuthConfig{Type: "api_key", Header: "X-Api-Key", EnvVars: []string{"NV_API_KEY"}},
+		Config:  spec.ConfigSpec{Format: "toml", Path: "~/.config/novisibletest-pp-cli/config.toml"},
+		Resources: map[string]spec.Resource{
+			"orders": {
+				Description: "Manage orders",
+				Endpoints: map[string]spec.Endpoint{
+					"list":   {Method: "GET", Path: "/orders", Description: "List orders"},
+					"create": {Method: "POST", Path: "/orders", Description: "Create order"},
+				},
+			},
+			"items": {
+				Description: "Manage items",
+				Endpoints: map[string]spec.Endpoint{
+					"list":   {Method: "GET", Path: "/items", Description: "List items"},
+					"create": {Method: "POST", Path: "/items", Description: "Create item"},
+				},
+			},
+		},
+	}
+
+	outputDir := filepath.Join(t.TempDir(), "novisibletest-pp-cli")
+	gen := New(apiSpec, outputDir)
+	require.NoError(t, gen.Generate())
+
+	assert.NoFileExists(t, filepath.Join(outputDir, "internal", "cli", "api_discovery.go"))
+
+	orders, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "orders.go"))
+	require.NoError(t, err)
+	assert.NotContains(t, string(orders), "Hidden: true",
+		"raw resource parent must not be Hidden when no api browser is generated")
+}
+
+func TestGeneratedOutput_AgentContextIncludesHiddenResourceGroups(t *testing.T) {
+	t.Parallel()
+
+	// Cobra's Hidden flag is a --help curation tool; the agent-context surface
+	// must still enumerate hidden resource parents and their endpoint subcommands
+	// so agents can reach every action a CLI user could.
+	apiSpec := &spec.APISpec{
+		Name:    "agentctxhide",
+		Version: "0.1.0",
+		BaseURL: "https://api.example.com",
+		Auth:    spec.AuthConfig{Type: "api_key", Header: "X-Api-Key", EnvVars: []string{"AC_API_KEY"}},
+		Config:  spec.ConfigSpec{Format: "toml", Path: "~/.config/agentctxhide-pp-cli/config.toml"},
+		Resources: map[string]spec.Resource{
+			"orders": {
+				Description: "Manage orders",
+				Endpoints: map[string]spec.Endpoint{
+					"list":   {Method: "GET", Path: "/orders", Description: "List orders"},
+					"create": {Method: "POST", Path: "/orders", Description: "Create order"},
+				},
+			},
+			"customers": {
+				Description: "Single-endpoint customers resource (gets promoted)",
+				Endpoints: map[string]spec.Endpoint{
+					"list": {Method: "GET", Path: "/customers", Description: "List customers"},
+				},
+			},
+		},
+	}
+
+	outputDir := filepath.Join(t.TempDir(), "agentctxhide-pp-cli")
+	gen := New(apiSpec, outputDir)
+	require.NoError(t, gen.Generate())
+
+	runGoCommand(t, outputDir, "mod", "tidy")
+	binaryPath := filepath.Join(outputDir, "agentctxhide-pp-cli")
+	runGoCommand(t, outputDir, "build", "-o", binaryPath, "./cmd/agentctxhide-pp-cli")
+
+	out, err := exec.Command(binaryPath, "agent-context").Output()
+	require.NoError(t, err)
+
+	var payload map[string]any
+	require.NoError(t, json.Unmarshal(out, &payload))
+
+	orders := findAgentContextCommand(payload["commands"], func(c map[string]any) bool {
+		return c["name"] == "orders"
+	})
+	require.NotNil(t, orders, "hidden resource parent must appear in agent-context")
+	subs, ok := orders["subcommands"].([]any)
+	require.True(t, ok, "hidden resource parent must report its endpoint subcommands")
+	assert.NotEmpty(t, subs, "hidden resource parent must report its endpoint subcommands")
+}
+
 func TestGeneratedOutput_PromotedCommandNotForBuiltins(t *testing.T) {
 	t.Parallel()
 
@@ -5302,8 +5439,13 @@ func TestGenerateGraphQLBFFUsesSemanticCommandSurface(t *testing.T) {
 	runGoCommand(t, outputDir, "build", "-o", binaryPath, "./cmd/example-pp-cli")
 	helpOut, err := exec.Command(binaryPath, "--help").CombinedOutput()
 	require.NoError(t, err, string(helpOut))
-	assert.Contains(t, string(helpOut), "products")
+	// Multi-endpoint resources are now Hidden so the generated `api` browser can
+	// surface them; --help stays curated. Direct invocation and the api browser
+	// listing must both still work.
 	assert.NotContains(t, string(helpOut), "graphql")
+	apiOut, err := exec.Command(binaryPath, "api").CombinedOutput()
+	require.NoError(t, err, string(apiOut))
+	assert.Contains(t, string(apiOut), "products")
 	productsHelp, err := exec.Command(binaryPath, "products", "--help").CombinedOutput()
 	require.NoError(t, err, string(productsHelp))
 	assert.Contains(t, string(productsHelp), "launches")
diff --git a/internal/generator/templates/agent_context.go.tmpl b/internal/generator/templates/agent_context.go.tmpl
index 126e3b81..889ea3f2 100644
--- a/internal/generator/templates/agent_context.go.tmpl
+++ b/internal/generator/templates/agent_context.go.tmpl
@@ -202,17 +202,23 @@ func buildAgentDiscoveryContext() *agentContextDiscovery {
 }
 
 // collectAgentCommands walks the cobra tree from the given command and
-// returns its direct children (skipping hidden commands and the
-// agent-context command itself to avoid self-reference). Each child is
-// recursed into if it has subcommands. Flags are captured via VisitAll.
-// Output is sorted by command name for stable diffs across regenerations.
+// returns its direct children (skipping the agent-context command itself
+// to avoid self-reference). Each child is recursed into if it has
+// subcommands. Flags are captured via VisitAll. Output is sorted by
+// command name for stable diffs across regenerations.
+//
+// Cobra's Hidden flag suppresses listing in --help but does not gate
+// agent discovery. Raw resource parents are Hidden so --help stays
+// curated and the `api` browser populates; the agent-context surface
+// must still enumerate them and their endpoints so agents can call any
+// action a CLI user could.
 func collectAgentCommands(c *cobra.Command) []agentContextCommand {
 	children := c.Commands()
 	sort.Slice(children, func(i, j int) bool { return children[i].Name() < children[j].Name() })
 
 	out := make([]agentContextCommand, 0, len(children))
 	for _, sub := range children {
-		if sub.Hidden || sub.Name() == "agent-context" {
+		if sub.Name() == "agent-context" {
 			continue
 		}
 		entry := agentContextCommand{
diff --git a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/agent_context.go b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/agent_context.go
index 0bf32e43..733a2943 100644
--- a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/agent_context.go
+++ b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/agent_context.go
@@ -178,17 +178,23 @@ func buildAgentDiscoveryContext() *agentContextDiscovery {
 }
 
 // collectAgentCommands walks the cobra tree from the given command and
-// returns its direct children (skipping hidden commands and the
-// agent-context command itself to avoid self-reference). Each child is
-// recursed into if it has subcommands. Flags are captured via VisitAll.
-// Output is sorted by command name for stable diffs across regenerations.
+// returns its direct children (skipping the agent-context command itself
+// to avoid self-reference). Each child is recursed into if it has
+// subcommands. Flags are captured via VisitAll. Output is sorted by
+// command name for stable diffs across regenerations.
+//
+// Cobra's Hidden flag suppresses listing in --help but does not gate
+// agent discovery. Raw resource parents are Hidden so --help stays
+// curated and the `api` browser populates; the agent-context surface
+// must still enumerate them and their endpoints so agents can call any
+// action a CLI user could.
 func collectAgentCommands(c *cobra.Command) []agentContextCommand {
 	children := c.Commands()
 	sort.Slice(children, func(i, j int) bool { return children[i].Name() < children[j].Name() })
 
 	out := make([]agentContextCommand, 0, len(children))
 	for _, sub := range children {
-		if sub.Hidden || sub.Name() == "agent-context" {
+		if sub.Name() == "agent-context" {
 			continue
 		}
 		entry := agentContextCommand{

← c93ce0c2 fix(cli): reconcile MCPB manifest against internal/client en  ·  back to Cli Printing Press  ·  fix(cli): honor --resources filter in dependent sync fan-out e2f3a53c →