[object Object]

← back to Cli Printing Press

feat(cli): hide raw resource commands when promoted exist, add api discovery (#121)

4b12b325c3de463d80092d0768b090af1d13faac · 2026-04-04 10:38:53 -0700 · Trevin Chow

When the generator produces promoted commands (user-friendly aliases),
raw resource parent commands are now hidden from --help but remain
fully functional. An `api` discovery command is auto-generated to let
agents and power users browse hidden interfaces.

Machine changes:
- command_parent.go.tmpl: accepts Hidden bool, sets Hidden: true in cobra
- api_discovery.go.tmpl: NEW template for the api browsing command
- generator.go: computes promotedCommands early, sets Hidden on parents,
  renders api_discovery.go when promoted commands exist
- Removed resourceGroupNames self-collision check from buildPromotedCommands
  (was preventing all promoted commands from being generated)

The gate is simple: len(promotedCommands) > 0. APIs with clean resource
names (links, domains, tags) get clean help. APIs without promotable
endpoints show resource parents as before.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 4b12b325c3de463d80092d0768b090af1d13faac
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Sat Apr 4 10:38:53 2026 -0700

    feat(cli): hide raw resource commands when promoted exist, add api discovery (#121)
    
    When the generator produces promoted commands (user-friendly aliases),
    raw resource parent commands are now hidden from --help but remain
    fully functional. An `api` discovery command is auto-generated to let
    agents and power users browse hidden interfaces.
    
    Machine changes:
    - command_parent.go.tmpl: accepts Hidden bool, sets Hidden: true in cobra
    - api_discovery.go.tmpl: NEW template for the api browsing command
    - generator.go: computes promotedCommands early, sets Hidden on parents,
      renders api_discovery.go when promoted commands exist
    - Removed resourceGroupNames self-collision check from buildPromotedCommands
      (was preventing all promoted commands from being generated)
    
    The gate is simple: len(promotedCommands) > 0. APIs with clean resource
    names (links, domains, tags) get clean help. APIs without promotable
    endpoints show resource parents as before.
    
    Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
 ...01-feat-hide-raw-commands-api-discovery-plan.md | 165 +++++++++++++++++++++
 internal/generator/generator.go                    |  54 ++++---
 internal/generator/generator_test.go               |  43 +++---
 internal/generator/templates/api_discovery.go.tmpl |  76 ++++++++++
 .../generator/templates/command_parent.go.tmpl     |   3 +
 internal/generator/templates/root.go.tmpl          |   7 +-
 6 files changed, 310 insertions(+), 38 deletions(-)

diff --git a/docs/plans/2026-04-04-001-feat-hide-raw-commands-api-discovery-plan.md b/docs/plans/2026-04-04-001-feat-hide-raw-commands-api-discovery-plan.md
new file mode 100644
index 00000000..93f9ec0e
--- /dev/null
+++ b/docs/plans/2026-04-04-001-feat-hide-raw-commands-api-discovery-plan.md
@@ -0,0 +1,165 @@
+---
+title: "feat: Hide raw resource commands when promoted commands exist, add api discovery"
+type: feat
+status: active
+date: 2026-04-04
+---
+
+# Hide Raw Resource Commands + Add API Discovery
+
+## Overview
+
+When the generator produces promoted commands (friendly top-level aliases like `resolve`, `profile`, `games`), the raw resource parent commands (`isteam-user`, `iplayer-service`) should be hidden from `--help` but remain functional. An `api` discovery command should be auto-generated to let agents and power users browse hidden interfaces.
+
+This is conditional: CLIs without promoted commands show resource parents as usual (they ARE the user interface). The `api` command is only generated when there are hidden commands to discover.
+
+## Problem Frame
+
+The generator creates promoted commands (friendly aliases) alongside the original resource parent commands. Both appear in `--help`, creating a noisy, confusing listing where `isteam-user get-player-summaries` sits next to `profile`. Users don't know which to use. Agents parsing `--help` get duplicate functionality.
+
+The Steam CLI proved the pattern: hide raw commands with `Hidden: true`, add an `api` command for discovery. This plan makes it a machine change so every future CLI benefits.
+
+## Requirements Trace
+
+- R1. Resource parent commands set `Hidden: true` when the CLI has promoted commands
+- R2. Resource parent commands remain visible when no promoted commands exist
+- R3. An `api` discovery command is generated when promoted commands exist, listing all hidden interfaces and their methods
+- R4. The `api` command is NOT generated when there are no promoted commands (nothing to discover)
+- R5. Hidden commands remain fully functional — only hidden from `--help` output
+
+## Scope Boundaries
+
+- **Not changing promoted command generation logic.** `buildPromotedCommands()` is unchanged.
+- **Not changing which commands get promoted.** The selection criteria stay the same.
+- **Not adding manual promoted command configuration.** The Phase 3 agent-built commands (like Steam's `resolve`, `profile`) are printed-CLI work, not machine work. This plan handles the machine's auto-generated promoted commands only.
+
+## Key Technical Decisions
+
+- **Use `len(.PromotedCommands) > 0` as the gate**: The root template data already has `.PromotedCommands`. If the slice is non-empty, hide resource parents and add `api`. If empty, no change from today. No new flags or configuration needed.
+
+- **`Hidden` field on command_parent.go.tmpl**: Add a `Hidden bool` to the parent command template data struct. Set it to `true` when promoted commands exist. The template emits `Hidden: true,` in the cobra.Command struct.
+
+- **`api_discovery.go.tmpl` as a new template**: A standalone template (not vision-gated) rendered only when promoted commands exist. It creates the `api` command that lists hidden interface commands and drills into their methods.
+
+## Implementation Units
+
+- [ ] **Unit 1: Add `Hidden` field to resource parent template**
+
+**Goal:** Resource parent commands set `Hidden: true` when promoted commands exist.
+
+**Requirements:** R1, R2, R5
+
+**Dependencies:** None
+
+**Files:**
+- Modify: `internal/generator/templates/command_parent.go.tmpl`
+- Modify: `internal/generator/generator.go` (add `Hidden bool` to parent template data struct)
+- Test: `internal/generator/generator_test.go`
+
+**Approach:**
+- Add `Hidden bool` to the parent command template data struct in `generator.go` (around line 310)
+- Set `Hidden: len(promotedCommands) > 0` — but `promotedCommands` is computed after resource rendering. Solution: compute promoted commands earlier (before the resource rendering loop), or pass the count through a different path. Since we already moved profiling early (for HasStore), we can compute `promotedCommands` before the resource loop too.
+- In `command_parent.go.tmpl`, add `{{if .Hidden}}Hidden: true,{{end}}` after the `Short:` field
+
+**Patterns to follow:**
+- Existing `HasStore bool` field on endpoint template data (same pattern — generator sets, template uses)
+- Existing `Hidden: true` pattern in cobra (standard cobra field)
+
+**Test scenarios:**
+- Happy path: spec with promoted commands (e.g., clerk) → resource parent commands have `Hidden: true` in generated code
+- Happy path: spec without promoted commands (e.g., petstore with few resources) → resource parent commands do NOT have `Hidden: true`
+- Integration: generate a CLI with promoted commands → `--help` shows promoted commands, hides resource parents
+- Edge case: resource parent with no promotable endpoint → still hidden if OTHER resources have promoted commands (the gate is global, not per-resource)
+
+**Verification:**
+- Generated CLI with promoted commands shows clean `--help` without raw interface names
+- Generated CLI without promoted commands shows resource parents as before
+
+---
+
+- [ ] **Unit 2: Create `api_discovery.go.tmpl`**
+
+**Goal:** Auto-generate an `api` command that lists hidden interfaces and their methods.
+
+**Requirements:** R3, R4
+
+**Dependencies:** Unit 1
+
+**Files:**
+- Create: `internal/generator/templates/api_discovery.go.tmpl`
+- Modify: `internal/generator/generator.go` (render the template conditionally when promoted commands exist)
+- Test: `internal/generator/generator_test.go`
+
+**Approach:**
+- Create `api_discovery.go.tmpl` in the `cli` package. It generates a command that:
+  - With no args: lists all `Hidden` commands on the root, with name and Short description
+  - With one arg (interface name): lists that interface's subcommands (methods)
+  - Includes realistic examples in the help text
+- In `generator.go`, render `api_discovery.go.tmpl` only when `len(promotedCommands) > 0`. Place the render after promoted commands are computed but before `root.go.tmpl` is rendered (so the template can reference the api command constructor).
+- Register `newAPICmd` in `root.go.tmpl` conditionally: `{{if .PromotedCommands}}rootCmd.AddCommand(newAPICmd(&flags)){{end}}`
+
+**Patterns to follow:**
+- The `cmd_api.go` file we manually built for Steam — use it as the reference implementation
+- Existing conditional command registration in `root.go.tmpl` (e.g., `{{if .VisionSet.Search}}`)
+
+**Test scenarios:**
+- Happy path: generate a CLI with promoted commands → `api` command exists, lists hidden interfaces
+- Happy path: generate a CLI without promoted commands → no `api` command, no `api_discovery.go` file
+- Happy path: `api <interface>` shows methods for that interface
+- Edge case: `api nonexistent` returns a clear error
+- Integration: generated CLI compiles with `api` command, `--help` shows it alongside promoted commands
+
+**Verification:**
+- Generated CLI with promoted commands has `api` in `--help`
+- `api` lists all hidden interfaces
+- `api <interface>` shows methods
+- Generated CLI without promoted commands does NOT have `api`
+
+---
+
+- [ ] **Unit 3: Update root.go.tmpl registration and file count test**
+
+**Goal:** Register `api` command conditionally and update expected file counts.
+
+**Requirements:** R3, R4
+
+**Dependencies:** Unit 1, Unit 2
+
+**Files:**
+- Modify: `internal/generator/templates/root.go.tmpl`
+- Modify: `internal/generator/generator_test.go` (update expected file counts for specs that get promoted commands)
+
+**Approach:**
+- Add `{{if .PromotedCommands}}rootCmd.AddCommand(newAPICmd(&flags)){{end}}` before the promoted commands registration block
+- Update expected file counts in `TestGenerateProjectsCompile` — specs with promoted commands get +1 file (api_discovery.go)
+
+**Test scenarios:**
+- Happy path: all existing test specs compile with updated file counts
+- Happy path: petstore (no promoted commands) → no api_discovery.go, file count unchanged
+- Happy path: clerk/loops (with promoted commands) → api_discovery.go generated, file count +1
+
+**Verification:**
+- `go test ./internal/generator/...` passes
+- `go test ./...` passes (1055+ tests)
+
+## System-Wide Impact
+
+- **`--help` output changes for CLIs with promoted commands**: Resource parent commands disappear from `--help`. This is the intended behavior — the promoted commands are the user-facing interface. Hidden commands remain functional.
+- **No change for CLIs without promoted commands**: Petstore-class CLIs with few resources and no promotable endpoints show resource parents exactly as before.
+- **Scorecard unaffected**: The scorecard checks command count, help output, etc. — hidden commands still count as commands. The `api` command adds one more visible command.
+- **Verify unaffected**: Verify discovers commands from both `--help` and root.go parsing. Hidden commands are still testable.
+
+## Risks & Dependencies
+
+| Risk | Mitigation |
+|------|------------|
+| Promoted commands may not cover all important operations | The `api` command provides full access to hidden interfaces. Power users and agents can discover and use any endpoint. |
+| Breaking change for existing CLIs that regenerate | `--help` output changes. This is intentional and desirable — the old help was noisy. No functional behavior changes. |
+| `buildPromotedCommands()` may produce poor aliases for some APIs | Not in scope — the selection criteria are unchanged. The skill's Phase 3 is where the agent makes product decisions about command naming. |
+
+## Sources & References
+
+- Reference implementation: `steam-web-pp-cli/internal/cli/cmd_api.go` (manually built)
+- Related code: `internal/generator/generator.go` (buildPromotedCommands, root template data)
+- Related code: `internal/generator/templates/command_parent.go.tmpl` (resource parent template)
+- Related code: `internal/generator/templates/root.go.tmpl` (command registration)
diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index 8aa839e8..6859320e 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -301,21 +301,28 @@ func (g *Generator) Generate() error {
 		}
 	}
 
+	// Compute promoted commands early — needed to determine Hidden flag on parent commands
+	promotedCommands := buildPromotedCommands(g.Spec)
+	hasPromoted := len(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 {
 		// Parent file: wires subcommands together
+		// When promoted commands exist, hide resource parents from --help
 		parentData := struct {
 			ResourceName string
 			FuncPrefix   string
 			CommandPath  string
 			Resource     spec.Resource
+			Hidden       bool
 			*spec.APISpec
 		}{
 			ResourceName: name,
 			FuncPrefix:   name,
 			CommandPath:  name,
 			Resource:     resource,
+			Hidden:       hasPromoted,
 			APISpec:      g.Spec,
 		}
 		parentPath := filepath.Join("internal", "cli", name+".go")
@@ -355,12 +362,14 @@ func (g *Generator) Generate() error {
 				FuncPrefix   string
 				CommandPath  string
 				Resource     spec.Resource
+				Hidden       bool
 				*spec.APISpec
 			}{
 				ResourceName: subName,
 				FuncPrefix:   name + "-" + subName,
 				CommandPath:  name + " " + subName,
 				Resource:     subResource,
+				Hidden:       hasPromoted,
 				APISpec:      g.Spec,
 			}
 			subParentPath := filepath.Join("internal", "cli", name+"_"+subName+".go")
@@ -567,8 +576,15 @@ func (g *Generator) Generate() error {
 		}
 	}
 
+	// Generate api discovery command when promoted commands exist (lets users browse hidden interfaces)
+	if hasPromoted {
+		if err := g.renderTemplate("api_discovery.go.tmpl", filepath.Join("internal", "cli", "api_discovery.go"), g.Spec); err != nil {
+			return fmt.Errorf("rendering api discovery: %w", err)
+		}
+	}
+
 	// Generate promoted top-level commands (user-friendly aliases for nested API commands)
-	promotedCommands := buildPromotedCommands(g.Spec)
+	// promotedCommands was computed earlier (before resource rendering) for Hidden flag
 	for _, pc := range promotedCommands {
 		promotedData := struct {
 			PromotedName string
@@ -591,18 +607,27 @@ func (g *Generator) Generate() error {
 		}
 	}
 
+	// Build set of resource names that have promoted commands — root.go.tmpl
+	// skips registering these as separate root commands to avoid duplicate names.
+	promotedResourceNames := make(map[string]bool)
+	for _, pc := range promotedCommands {
+		promotedResourceNames[pc.ResourceName] = true
+	}
+
 	rootData := struct {
 		*spec.APISpec
-		VisionSet            VisionTemplateSet
-		WorkflowConstructors []string
-		InsightConstructors  []string
-		PromotedCommands     []PromotedCommand
+		VisionSet             VisionTemplateSet
+		WorkflowConstructors  []string
+		InsightConstructors   []string
+		PromotedCommands      []PromotedCommand
+		PromotedResourceNames map[string]bool
 	}{
-		APISpec:              g.Spec,
-		VisionSet:            g.VisionSet,
-		WorkflowConstructors: renderedWorkflowConstructors,
-		InsightConstructors:  renderedInsightConstructors,
-		PromotedCommands:     promotedCommands,
+		APISpec:               g.Spec,
+		VisionSet:             g.VisionSet,
+		WorkflowConstructors:  renderedWorkflowConstructors,
+		InsightConstructors:   renderedInsightConstructors,
+		PromotedCommands:      promotedCommands,
+		PromotedResourceNames: promotedResourceNames,
 	}
 	if err := g.renderTemplate("root.go.tmpl", filepath.Join("internal", "cli", "root.go"), rootData); err != nil {
 		return fmt.Errorf("rendering root: %w", err)
@@ -1139,12 +1164,6 @@ func buildPromotedCommands(s *spec.APISpec) []PromotedCommand {
 	var promoted []PromotedCommand
 	usedNames := make(map[string]bool)
 
-	// Collect resource group names so promoted commands don't collide with them
-	resourceGroupNames := make(map[string]bool)
-	for name := range s.Resources {
-		resourceGroupNames[toKebab(name)] = true
-	}
-
 	for name, resource := range s.Resources {
 		// Find the primary GET endpoint: prefer GET without positional params, else first GET
 		var bestName string
@@ -1180,9 +1199,6 @@ func buildPromotedCommands(s *spec.APISpec) []PromotedCommand {
 		if builtinCommands[promotedName] {
 			continue
 		}
-		if resourceGroupNames[promotedName] {
-			continue
-		}
 		if usedNames[promotedName] {
 			continue
 		}
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index d30cf246..d5632865 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -22,9 +22,9 @@ func TestGenerateProjectsCompile(t *testing.T) {
 		specPath      string
 		expectedFiles int
 	}{
-		{name: "stytch", specPath: filepath.Join("..", "..", "testdata", "stytch.yaml"), expectedFiles: 31},
-		{name: "clerk", specPath: filepath.Join("..", "..", "testdata", "clerk.yaml"), expectedFiles: 36},
-		{name: "loops", specPath: filepath.Join("..", "..", "testdata", "loops.yaml"), expectedFiles: 34},
+		{name: "stytch", specPath: filepath.Join("..", "..", "testdata", "stytch.yaml"), expectedFiles: 34},
+		{name: "clerk", specPath: filepath.Join("..", "..", "testdata", "clerk.yaml"), expectedFiles: 40},
+		{name: "loops", specPath: filepath.Join("..", "..", "testdata", "loops.yaml"), expectedFiles: 36},
 	}
 
 	for _, tt := range tests {
@@ -611,7 +611,7 @@ func TestToKebab(t *testing.T) {
 func TestBuildPromotedCommands(t *testing.T) {
 	t.Parallel()
 
-	t.Run("resource with list endpoint is NOT promoted (collides with resource group)", func(t *testing.T) {
+	t.Run("resource with list endpoint IS promoted (shortcut for resource group)", func(t *testing.T) {
 		t.Parallel()
 		s := &spec.APISpec{
 			Name:    "test",
@@ -626,10 +626,11 @@ func TestBuildPromotedCommands(t *testing.T) {
 			},
 		}
 		promoted := buildPromotedCommands(s)
-		assert.Empty(t, promoted, "promoted command should not be emitted when it collides with resource group name")
+		require.Len(t, promoted, 1)
+		assert.Equal(t, "users", promoted[0].PromotedName)
 	})
 
-	t.Run("ISteamUser resource is NOT promoted (collides with resource group)", func(t *testing.T) {
+	t.Run("ISteamUser resource IS promoted (shortcut for resource group)", func(t *testing.T) {
 		t.Parallel()
 		s := &spec.APISpec{
 			Name:    "test",
@@ -644,7 +645,8 @@ func TestBuildPromotedCommands(t *testing.T) {
 			},
 		}
 		promoted := buildPromotedCommands(s)
-		assert.Empty(t, promoted, "promoted command should not be emitted when it collides with resource group name")
+		require.Len(t, promoted, 1)
+		assert.Equal(t, "steam-user", promoted[0].PromotedName)
 	})
 
 	t.Run("resource named version is skipped (collides with built-in)", func(t *testing.T) {
@@ -684,7 +686,7 @@ func TestBuildPromotedCommands(t *testing.T) {
 		assert.Empty(t, promoted)
 	})
 
-	t.Run("prefers GET without positional params (collides with resource group)", func(t *testing.T) {
+	t.Run("prefers GET without positional params for promoted command", func(t *testing.T) {
 		t.Parallel()
 		s := &spec.APISpec{
 			Name:    "test",
@@ -701,7 +703,9 @@ func TestBuildPromotedCommands(t *testing.T) {
 			},
 		}
 		promoted := buildPromotedCommands(s)
-		assert.Empty(t, promoted, "promoted command collides with resource group name")
+		require.Len(t, promoted, 1)
+		assert.Equal(t, "items", promoted[0].PromotedName)
+		assert.Equal(t, "list", promoted[0].EndpointName, "should prefer the list endpoint (no positional params)")
 	})
 
 	t.Run("all built-in names are skipped", func(t *testing.T) {
@@ -748,12 +752,12 @@ func TestGeneratedOutput_PromotedCommandExists(t *testing.T) {
 	gen := New(apiSpec, outputDir)
 	require.NoError(t, gen.Generate())
 
-	// Promoted command file should NOT exist because "users" collides with the
-	// resource group command of the same name
+	// Promoted command file SHOULD exist — it provides a user-friendly shortcut.
+	// The resource group command is hidden from --help when promoted commands exist.
 	promotedFile := filepath.Join(outputDir, "internal", "cli", "promoted_users.go")
-	assert.NoFileExists(t, promotedFile)
+	assert.FileExists(t, promotedFile)
 
-	// The resource group command should still exist
+	// The resource group command should still exist (but hidden)
 	assert.FileExists(t, filepath.Join(outputDir, "internal", "cli", "users.go"))
 }
 
@@ -788,9 +792,12 @@ func TestGeneratedOutput_PromotedCommandCompiles(t *testing.T) {
 	gen := New(apiSpec, outputDir)
 	require.NoError(t, gen.Generate())
 
-	// Promoted files should NOT exist because they collide with resource group names
-	assert.NoFileExists(t, filepath.Join(outputDir, "internal", "cli", "promoted_i-steam-user.go"))
-	assert.NoFileExists(t, filepath.Join(outputDir, "internal", "cli", "promoted_items.go"))
+	// Promoted files SHOULD exist — they provide user-friendly shortcuts for resource groups.
+	// When promoted commands exist, resource parents are hidden from --help.
+	assert.FileExists(t, filepath.Join(outputDir, "internal", "cli", "promoted_steam-user.go"))
+	assert.FileExists(t, filepath.Join(outputDir, "internal", "cli", "promoted_items.go"))
+	// API discovery command should also be generated
+	assert.FileExists(t, filepath.Join(outputDir, "internal", "cli", "api_discovery.go"))
 
 	// Must compile
 	runGoCommand(t, outputDir, "mod", "tidy")
@@ -828,8 +835,8 @@ func TestGeneratedOutput_PromotedCommandNotForBuiltins(t *testing.T) {
 
 	// "version" should NOT have a promoted command (collides with built-in)
 	assert.NoFileExists(t, filepath.Join(outputDir, "internal", "cli", "promoted_version.go"))
-	// "users" should NOT have a promoted command (collides with resource group)
-	assert.NoFileExists(t, filepath.Join(outputDir, "internal", "cli", "promoted_users.go"))
+	// "users" SHOULD have a promoted command (shortcut for the resource group)
+	assert.FileExists(t, filepath.Join(outputDir, "internal", "cli", "promoted_users.go"))
 }
 
 // --- Unit 3: Auth Error Handling Tests ---
diff --git a/internal/generator/templates/api_discovery.go.tmpl b/internal/generator/templates/api_discovery.go.tmpl
new file mode 100644
index 00000000..987855ab
--- /dev/null
+++ b/internal/generator/templates/api_discovery.go.tmpl
@@ -0,0 +1,76 @@
+// Copyright {{currentYear}} {{.Owner}}. Licensed under Apache-2.0. See LICENSE.
+// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
+
+package cli
+
+import (
+	"fmt"
+	"sort"
+	"strings"
+
+	"github.com/spf13/cobra"
+)
+
+func newAPICmd(flags *rootFlags) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "api [interface]",
+		Short: "Browse all API endpoints by interface name",
+		Long: `Browse and call any API endpoint using the raw interface names.
+
+The friendly top-level commands cover the most common operations.
+This command provides access to ALL endpoints for power users and
+agents that need full API coverage.
+
+Run 'api' with no arguments to list all interfaces.
+Run 'api <interface>' to see that interface's methods.`,
+		Example: `  # List all available interfaces
+  {{.Name}}-pp-cli api
+
+  # Show methods for a specific interface
+  {{.Name}}-pp-cli api <interface-name>`,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			root := cmd.Root()
+
+			if len(args) > 0 {
+				target := strings.ToLower(args[0])
+				for _, child := range root.Commands() {
+					if child.Hidden && strings.ToLower(child.Name()) == target {
+						methods := child.Commands()
+						if len(methods) == 0 {
+							return child.Help()
+						}
+						fmt.Fprintf(cmd.OutOrStdout(), "%s — %s\n\nMethods:\n", child.Name(), child.Short)
+						for _, method := range methods {
+							fmt.Fprintf(cmd.OutOrStdout(), "  %-50s %s\n", child.Name()+" "+method.Name(), method.Short)
+						}
+						fmt.Fprintf(cmd.OutOrStdout(), "\nUse '%s-pp-cli %s <method> --help' for details.\n", "{{.Name}}", child.Name())
+						return nil
+					}
+				}
+				return fmt.Errorf("interface %q not found. Run '%s-pp-cli api' to list all interfaces", args[0], "{{.Name}}")
+			}
+
+			var interfaces []string
+			for _, child := range root.Commands() {
+				if child.Hidden {
+					interfaces = append(interfaces, fmt.Sprintf("  %-45s %s", child.Name(), child.Short))
+				}
+			}
+			sort.Strings(interfaces)
+
+			if len(interfaces) == 0 {
+				fmt.Fprintln(cmd.OutOrStdout(), "No hidden API interfaces found.")
+				return nil
+			}
+
+			fmt.Fprintf(cmd.OutOrStdout(), "Available API interfaces (%d):\n\n", len(interfaces))
+			for _, line := range interfaces {
+				fmt.Fprintln(cmd.OutOrStdout(), line)
+			}
+			fmt.Fprintf(cmd.OutOrStdout(), "\nUse '%s-pp-cli api <interface>' to see methods.\n", "{{.Name}}")
+			return nil
+		},
+	}
+
+	return cmd
+}
diff --git a/internal/generator/templates/command_parent.go.tmpl b/internal/generator/templates/command_parent.go.tmpl
index 24de3a3d..6f613c35 100644
--- a/internal/generator/templates/command_parent.go.tmpl
+++ b/internal/generator/templates/command_parent.go.tmpl
@@ -11,6 +11,9 @@ func new{{camel .FuncPrefix}}Cmd(flags *rootFlags) *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "{{.ResourceName}}",
 		Short: "{{oneline .Resource.Description}}",
+{{- if .Hidden}}
+		Hidden: true,
+{{- end}}
 	}
 {{range $eName, $endpoint := .Resource.Endpoints}}
 	cmd.AddCommand(new{{camel $.FuncPrefix}}{{camel $eName}}Cmd(flags))
diff --git a/internal/generator/templates/root.go.tmpl b/internal/generator/templates/root.go.tmpl
index 0fdc7a85..1cbdb02b 100644
--- a/internal/generator/templates/root.go.tmpl
+++ b/internal/generator/templates/root.go.tmpl
@@ -98,7 +98,9 @@ func Execute() error {
 	}
 
 {{- range $name, $resource := .Resources}}
-	rootCmd.AddCommand(new{{camel $name}}Cmd(&flags)) {{/* FuncPrefix matches resource name for top-level */}}
+{{- if not (index $.PromotedResourceNames $name)}}
+	rootCmd.AddCommand(new{{camel $name}}Cmd(&flags))
+{{- end}}
 {{- end}}
 	rootCmd.AddCommand(newDoctorCmd(&flags))
 	rootCmd.AddCommand(newAuthCmd(&flags))
@@ -129,6 +131,9 @@ func Execute() error {
 {{- range .InsightConstructors}}
 	rootCmd.AddCommand(new{{.}}Cmd(&flags))
 {{- end}}
+{{- if .PromotedCommands}}
+	rootCmd.AddCommand(newAPICmd(&flags))
+{{- end}}
 {{- range .PromotedCommands}}
 	rootCmd.AddCommand(new{{camel .PromotedName}}PromotedCmd(&flags))
 {{- end}}

← 360db307 fix(skills): publish skill checks for merged PRs before reus  ·  back to Cli Printing Press  ·  fix(cli): SQL reserved word safety, promoted subcommands, ve 289ac4bf →