From 7d62881a5c921fde779aa916603e1c52703cc02e Mon Sep 17 00:00:00 2001 From: JackJin <1037461232@qq.com> Date: Fri, 5 Jun 2026 14:07:39 +0800 Subject: [PATCH] feat(compat): surface dynamic-command flag aliases in --help MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Envelope overlays can declare flag aliases (alias + aliases), which the dynamic command builder registers as hidden cobra flags. They work at runtime but never appear in --help, so users and agents inspecting `--help` cannot self-discover them and must read the source JSON. After schema enrichment, annotate each visible primary flag whose envelope override declares aliases with a " [别名: --x, --y]" suffix. Aliases stay hidden; the hint is deduped against the primary and reserved names (json, params) and is idempotent. Running post-enrichment avoids clobbering the detail-schema usage-fill (which keys off usage == paramName). Closes #371 --- internal/compat/dynamic_commands.go | 59 ++++++++++++++++ internal/compat/dynamic_commands_test.go | 87 ++++++++++++++++++++++++ 2 files changed, 146 insertions(+) diff --git a/internal/compat/dynamic_commands.go b/internal/compat/dynamic_commands.go index 66fcb870..bbf2a515 100644 --- a/internal/compat/dynamic_commands.go +++ b/internal/compat/dynamic_commands.go @@ -203,6 +203,12 @@ func BuildDynamicCommands(servers []market.ServerDescriptor, runner executor.Run buildFlagsFromDetailSchema(cmd, dt.ToolRequest, override.Flags) } + // §371: surface envelope-declared flag aliases in --help. Aliases are + // registered as hidden cobra flags, so without this they are invisible + // to users/agents reading `--help`. Runs after schema enrichment so the + // final usage text is what receives the alias suffix. + annotateFlagAliases(cmd, override.Flags) + // §P2.flagconstraints: must run AFTER schema enrichment because the // target flags may be registered lazily by buildFlagsFromDetailSchema. applyFlagConstraints(cmd, override) @@ -402,6 +408,59 @@ func buildFlagsFromDetailSchema(cmd *cobra.Command, schemaJSON string, flagOverr } } +// aliasHintMarker prefixes the alias suffix appended to a flag's usage, used +// both to format and to detect (idempotency) an existing alias hint. +const aliasHintMarker = "[别名: " + +// aliasUsageHint renders a " [别名: --x, --y]" suffix from the hidden alias +// names of a flag, deduped against the primary flag name and reserved names. +// Returns "" when there are no surfaceable aliases. +func aliasUsageHint(aliases []string, primary string) string { + seen := map[string]bool{primary: true, "json": true, "params": true} + names := make([]string, 0, len(aliases)) + for _, a := range aliases { + a = strings.TrimSpace(a) + if a == "" || seen[a] { + continue + } + seen[a] = true + names = append(names, "--"+a) + } + if len(names) == 0 { + return "" + } + return " " + aliasHintMarker + strings.Join(names, ", ") + "]" +} + +// annotateFlagAliases appends an alias hint to the --help usage of each visible +// primary flag that has envelope-declared aliases (#371). Aliases are otherwise +// registered as hidden cobra flags, so they never appear in --help and users / +// agents cannot self-discover them. The primary flag name is derived the same +// way buildOverrideBindings derives it, so the lookup matches the registered flag. +func annotateFlagAliases(cmd *cobra.Command, overrides map[string]market.CLIFlagOverride) { + for paramName, ov := range overrides { + if len(ov.Aliases) == 0 || ov.Hidden { + continue + } + flagName := strings.TrimSpace(ov.Alias) + if flagName == "" && !ov.Positional { + flagName = compatFlagName(paramName) + } + if flagName == "" { + continue + } + hint := aliasUsageHint(ov.Aliases, flagName) + if hint == "" { + continue + } + f := cmd.Flags().Lookup(flagName) + if f == nil || f.Hidden || strings.Contains(f.Usage, aliasHintMarker) { + continue + } + f.Usage = strings.TrimRight(f.Usage, " ") + hint + } +} + // toKebabCase converts camelCase or snake_case identifiers to kebab-case. // Examples: "parentDentryUuid" → "parent-dentry-uuid", "report_id" → "report-id" func toKebabCase(s string) string { diff --git a/internal/compat/dynamic_commands_test.go b/internal/compat/dynamic_commands_test.go index cde63cbd..b5f4275b 100644 --- a/internal/compat/dynamic_commands_test.go +++ b/internal/compat/dynamic_commands_test.go @@ -1389,6 +1389,93 @@ func TestBuildDynamicCommands_MultipleAliases_Dedup(t *testing.T) { } } +// TestAliasUsageHint covers the pure formatting + dedup of the #371 alias hint. +func TestAliasUsageHint(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + aliases []string + primary string + want string + }{ + {"single", []string{"query"}, "find", " [别名: --query]"}, + {"multiple", []string{"query", "keyword"}, "find", " [别名: --query, --keyword]"}, + {"dedup reserved/primary/dup", []string{"query", "json", "params", "find", "query"}, "find", " [别名: --query]"}, + {"none", nil, "find", ""}, + {"only reserved", []string{"json", "params"}, "find", ""}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := aliasUsageHint(tc.aliases, tc.primary); got != tc.want { + t.Errorf("aliasUsageHint(%v, %q) = %q, want %q", tc.aliases, tc.primary, got, tc.want) + } + }) + } +} + +// TestBuildDynamicCommands_AliasesShownInHelp is the integration regression for +// #371: envelope-declared aliases are registered as hidden flags, so without +// the help annotation they are invisible in --help. The primary flag's usage +// must now surface them, while the alias flags themselves stay hidden. +func TestBuildDynamicCommands_AliasesShownInHelp(t *testing.T) { + t.Parallel() + + runner := &captureRunner{} + servers := []market.ServerDescriptor{ + { + Endpoint: "https://endpoint-sheet", + CLI: market.CLIOverlay{ + ID: "sheet", + Command: "sheet", + ToolOverrides: map[string]market.CLIToolOverride{ + "find_cells": { + CLIName: "find", + Flags: map[string]market.CLIFlagOverride{ + "text": { + Alias: "find", // primary flag name + Aliases: []string{"query"}, + Description: "搜索文本", + }, + }, + }, + }, + }, + }, + } + + cmds := BuildDynamicCommands(servers, runner, nil) + + var checked bool + for _, sub := range cmds[0].Commands() { + if sub.Name() != "find" { + continue + } + checked = true + primary := sub.Flags().Lookup("find") + if primary == nil { + t.Fatal("primary --find flag was not registered") + } + if !strings.Contains(primary.Usage, "搜索文本") { + t.Errorf("primary usage should keep its description, got %q", primary.Usage) + } + if !strings.Contains(primary.Usage, "--query") || !strings.Contains(primary.Usage, "别名") { + t.Errorf("primary usage should surface alias --query, got %q", primary.Usage) + } + // The alias itself must remain a hidden flag (still usable, not listed). + alias := sub.Flags().Lookup("query") + if alias == nil { + t.Fatal("--query alias flag missing") + } + if !alias.Hidden { + t.Error("--query should remain hidden; only the hint surfaces it") + } + } + if !checked { + t.Fatal("find subcommand was not built") + } +} + func TestBuildDynamicCommands_NoParent(t *testing.T) { t.Parallel()