[object Object]

← back to Cli Printing Press

fix(cli): dogfood uses cobra Use: fields and recursive help walking

b80d31401f6c1c094786d24cab2617c4d76c798b · 2026-04-04 16:10:57 -0700 · Trevin Chow

Two fixes to the command tree wiring check:

1. Extract command names from cobra Use: fields instead of Go function
   names. Function names encode file hierarchy (newBookingsPromotedCmd →
   "bookings-promoted") while Use: fields are actual command names
   ("bookings"). This eliminates false positives from naming mismatches.

2. Recursive help walking instead of two-level. Many CLIs have 3+ levels
   (bookings → attendees → add). Now walks up to 4 levels deep to
   discover all registered subcommands.

Cal.com CLI: 119 false positives → 95 genuinely unreachable commands
(hidden sub-resource groups wired into promoted commands retain Hidden:
true, making their children unreachable — a real generator wiring issue
to fix separately).

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

Files touched

Diff

commit b80d31401f6c1c094786d24cab2617c4d76c798b
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Sat Apr 4 16:10:57 2026 -0700

    fix(cli): dogfood uses cobra Use: fields and recursive help walking
    
    Two fixes to the command tree wiring check:
    
    1. Extract command names from cobra Use: fields instead of Go function
       names. Function names encode file hierarchy (newBookingsPromotedCmd →
       "bookings-promoted") while Use: fields are actual command names
       ("bookings"). This eliminates false positives from naming mismatches.
    
    2. Recursive help walking instead of two-level. Many CLIs have 3+ levels
       (bookings → attendees → add). Now walks up to 4 levels deep to
       discover all registered subcommands.
    
    Cal.com CLI: 119 false positives → 95 genuinely unreachable commands
    (hidden sub-resource groups wired into promoted commands retain Hidden:
    true, making their children unreachable — a real generator wiring issue
    to fix separately).
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
 internal/pipeline/dogfood.go      | 44 ++++++++++++++++++++++++++-------------
 internal/pipeline/dogfood_test.go | 20 +++++++++---------
 2 files changed, 40 insertions(+), 24 deletions(-)

diff --git a/internal/pipeline/dogfood.go b/internal/pipeline/dogfood.go
index d6cc2586..1108bd77 100644
--- a/internal/pipeline/dogfood.go
+++ b/internal/pipeline/dogfood.go
@@ -963,21 +963,24 @@ func checkCommandTree(dir string) CommandTreeResult {
 	cliDir := filepath.Join(dir, "internal", "cli")
 	files := listGoFiles(cliDir)
 
-	// Scan for func new*Cmd() patterns
-	cmdFuncRe := regexp.MustCompile(`(?m)^func\s+new(\w+)Cmd\s*\(`)
+	// Scan for cobra Use: fields to get the actual command names users see.
+	// Previous approach derived names from Go function names (newXxxCmd → xxx),
+	// but function names encode file hierarchy (e.g., newBookingsPromotedCmd)
+	// while Use: fields are the actual cobra names (e.g., "bookings").
+	useFieldRe := regexp.MustCompile(`(?m)Use:\s*"([^"\s]+)`)
 	definedCmds := make(map[string]struct{})
 	for _, file := range files {
 		data, err := os.ReadFile(file)
 		if err != nil {
 			continue
 		}
-		matches := cmdFuncRe.FindAllStringSubmatch(string(data), -1)
+		matches := useFieldRe.FindAllStringSubmatch(string(data), -1)
 		for _, match := range matches {
-			// Convert CamelCase function name to kebab-case command name.
-			// e.g., newApiGetCategoryCmd -> "api-get-category"
-			// This matches cobra's command naming convention in help output.
-			cmdName := camelToKebab(match[1])
-			definedCmds[cmdName] = struct{}{}
+			// Extract just the command name (before any positional args)
+			cmdName := strings.Fields(match[1])[0]
+			if cmdName != "" {
+				definedCmds[cmdName] = struct{}{}
+			}
 		}
 	}
 
@@ -1007,14 +1010,27 @@ func checkCommandTree(dir string) CommandTreeResult {
 		return result
 	}
 
-	// Also gather subcommand help by extracting top-level commands
+	// Recursively gather help output from all command levels.
+	// Many CLIs have 3+ levels (e.g., bookings → attendees → add).
+	// Only checking two levels misses nested subcommands.
 	helpLower := strings.ToLower(helpOut)
-	topCmds := extractCommandNames(helpOut)
-	for _, topCmd := range topCmds {
-		subOut, err := runDogfoodCmd(binaryPath, 15*time.Second, topCmd, "--help")
-		if err == nil {
-			helpLower += "\n" + strings.ToLower(subOut)
+	var walkHelp func(parentArgs []string, depth int)
+	walkHelp = func(parentArgs []string, depth int) {
+		if depth > 4 {
+			return // safety cap
+		}
+		args := append(parentArgs, "--help")
+		out, err := runDogfoodCmd(binaryPath, 15*time.Second, args...)
+		if err != nil {
+			return
 		}
+		helpLower += "\n" + strings.ToLower(out)
+		for _, sub := range extractCommandNames(out) {
+			walkHelp(append(parentArgs, sub), depth+1)
+		}
+	}
+	for _, topCmd := range extractCommandNames(helpOut) {
+		walkHelp([]string{topCmd}, 1)
 	}
 
 	for cmdName := range definedCmds {
diff --git a/internal/pipeline/dogfood_test.go b/internal/pipeline/dogfood_test.go
index 1311f8a2..35deec66 100644
--- a/internal/pipeline/dogfood_test.go
+++ b/internal/pipeline/dogfood_test.go
@@ -330,12 +330,12 @@ func TestCheckCommandTree(t *testing.T) {
 	cliDir := filepath.Join(dir, "internal", "cli")
 	require.NoError(t, os.MkdirAll(cliDir, 0o755))
 
-	// Define two commands via newXxxCmd() functions
+	// Define commands with cobra Use: fields (what users see in --help)
 	writeTestFile(t, filepath.Join(cliDir, "foo.go"), `package cli
-func newFooCmd() {}
+func newFooCmd() { cmd := &cobra.Command{Use: "foo"} }
 `)
 	writeTestFile(t, filepath.Join(cliDir, "bar.go"), `package cli
-func newBarCmd() {}
+func newBarCmd() { cmd := &cobra.Command{Use: "bar"} }
 `)
 
 	// Without a buildable binary, checkCommandTree can't verify registration,
@@ -347,25 +347,25 @@ func newBarCmd() {}
 	assert.Empty(t, result.Unregistered)
 }
 
-func TestCheckCommandTree_KebabConversion(t *testing.T) {
+func TestCheckCommandTree_UseFieldExtraction(t *testing.T) {
 	dir := t.TempDir()
 	cliDir := filepath.Join(dir, "internal", "cli")
 	require.NoError(t, os.MkdirAll(cliDir, 0o755))
 
-	// Define subcommands with CamelCase function names
+	// Use: fields may include positional args — we extract just the command name
 	writeTestFile(t, filepath.Join(cliDir, "api.go"), `package cli
-func newApiGetCategoryCmd() {}
-func newApiListTeamsCmd() {}
+cmd := &cobra.Command{Use: "get-category <id>"}
+cmd2 := &cobra.Command{Use: "list-teams"}
 `)
 	writeTestFile(t, filepath.Join(cliDir, "auth.go"), `package cli
-func newAuthLoginCmd() {}
+cmd := &cobra.Command{Use: "login"}
 `)
 	writeTestFile(t, filepath.Join(cliDir, "sync.go"), `package cli
-func newSyncCmd() {}
+cmd := &cobra.Command{Use: "sync"}
 `)
 
 	result := checkCommandTree(dir)
-	// Should find 4 defined commands
+	// Should find 4 defined commands: get-category, list-teams, login, sync
 	assert.Equal(t, 4, result.Defined)
 	// Without a buildable binary, all treated as registered
 	assert.Equal(t, 4, result.Registered)

← 6e2e1b3c fix(skills): comprehensive README requirements for polish ag  ·  back to Cli Printing Press  ·  fix(cli): unhide sub-resource groups when wired into promote 26fe5727 →