Commit 4eb0f95ce5

4eb0f95ce50ad8d5e29cccf5d3b47a33b9d761df

parent: 44191b1a52

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-28 07:22 UTC

help: auth (and any future CLI-only grouping) renders with the registry layout, in the CLI's own paths

Ref #267

Layout: unified · split

cmd/gitbay/main.go +1 −1
@@ -484,7 +484,7 @@ func authCmd() *cobra.Command {
484484 pass("list", passOpts{server: []string{"token", "list"}}),
485485 pass("revoke", passOpts{server: []string{"token", "revoke"}}),
486486 )
487 return group("auth", "identity: whoami, SSH and PGP keys",
487 return group("auth", "whoami, SSH and PGP keys, email, API tokens",
488488 pass("export", passOpts{server: []string{"account", "export"}}),
489489 tokens,
490490 pass("whoami", passOpts{server: []string{"whoami"}}),
internal/control/help.go +70 −7
@@ -28,6 +28,7 @@ var nounSummaries = map[string]string{
2828 "account": "export or import your account, for instance migration",
2929 "admin": "instance administration (admins)",
3030 "audit": "instance audit log (admins)",
31 "auth": "whoami, SSH and PGP keys, email, API tokens",
3132 "build": "CI builds",
3233 "dashboard": "pinned repos, open MRs, assigned issues, recent builds",
3334 "email": "manage email addresses",
@@ -61,6 +62,35 @@ var nounSummaries = map[string]string{
6162// test (task 4.5).
6263func NounSummaries() map[string]string { return nounSummaries }
6364
65// nounAlias is one bucket of registered commands, reachable under a
66// CLI-only noun that is not itself a registry path (auth, gathering
67// several unrelated registry prefixes): Registered is what runHelp
68// matches against the registry, CLI is the path a gitbay caller
69// actually types to reach it — not always Registered with the alias's
70// own name stitched on (account export -> auth export drops a word),
71// so the two are paired explicitly rather than derived.
72type nounAlias struct {
73 Registered string
74 CLI string
75}
76
77// nounAliases groups a CLI-only noun into the real prefixes it gathers,
78// so `help auth` renders with the same layout a real noun gets instead
79// of falling back to whatever a caller does when help fails. A stock
80// ssh caller — the only one who could ever ask for a bare "auth" and
81// get nothing back from the registry — sees the Registered forms
82// unchanged; the CLI, having sent its own path, sees CLI.
83var nounAliases = map[string][]nounAlias{
84 "auth": {
85 {"account export", "auth export"},
86 {"whoami", "auth whoami"},
87 {"keys", "auth keys"},
88 {"email", "auth email"},
89 {"pgp", "auth pgp"},
90 {"token", "auth token"},
91 },
92}
93
6494// helpEntry is one row of the registry as help reports it.
6595type helpEntry struct {
6696 Path string `json:"path"`
@@ -76,11 +106,33 @@ type helpEntry struct {
76106// noun with several commands under it renders a READ/WRITE summary.
77107func runHelp(c *Ctx, args []string) int {
78108 prefix := joinPath(args)
109 prefixes := []string{prefix}
110 override := map[string]string{}
111 if aliased, ok := nounAliases[prefix]; ok {
112 prefixes = nil
113 for _, a := range aliased {
114 prefixes = append(prefixes, a.Registered)
115 }
116 if c.CLIPath != "" {
117 for _, cmd := range registry {
118 p := joinPath(cmd.Path)
119 for _, a := range aliased {
120 if p == a.Registered || strings.HasPrefix(p, a.Registered+" ") {
121 override[p] = a.CLI + strings.TrimPrefix(p, a.Registered)
122 break
123 }
124 }
125 }
126 }
127 }
79128 var matched []Command
80129 for _, cmd := range registry {
81130 p := joinPath(cmd.Path)
82 if prefix == "" || p == prefix || strings.HasPrefix(p, prefix+" ") {
83 matched = append(matched, cmd)
131 for _, pfx := range prefixes {
132 if pfx == "" || p == pfx || strings.HasPrefix(p, pfx+" ") {
133 matched = append(matched, cmd)
134 break
135 }
84136 }
85137 }
86138 if len(matched) == 0 {
@@ -106,7 +158,7 @@ func runHelp(c *Ctx, args []string) int {
106158 case joinPath(matched[0].Path) == prefix:
107159 c.helpVerb(w, matched[0], matched[1:])
108160 default:
109 c.helpNoun(w, prefix, matched)
161 c.helpNoun(w, prefix, matched, override)
110162 }
111163 })
112164}
@@ -260,16 +312,28 @@ func (c *Ctx) helpVerb(w io.Writer, cmd Command, below []Command) {
260312 }
261313}
262314
263func (c *Ctx) helpNoun(w io.Writer, prefix string, cmds []Command) {
315// helpNoun renders a noun with several commands under it. override, from
316// an aliased noun (auth), gives the full CLI path for a row that is not
317// itself under prefix (keys list, gathered under auth, becomes "auth
318// keys list"); it is empty for an ordinary noun, so every row there
319// still trims to just its own verb.
320func (c *Ctx) helpNoun(w io.Writer, prefix string, cmds []Command, override map[string]string) {
264321 head := nounSummaries[strings.Fields(prefix)[0]]
265322 fmt.Fprintln(w, head)
266323 fmt.Fprintln(w)
267324 c.heading(w, "USAGE")
268325 display := c.shownAs(prefix, prefix)
269326 fmt.Fprintf(w, " %s %s <verb> ...\n", c.program(), display)
327 rowText := func(cmd Command) string {
328 full := joinPath(cmd.Path)
329 if ov, ok := override[full]; ok {
330 return ov
331 }
332 return strings.TrimPrefix(full, prefix+" ")
333 }
270334 wide := 0
271335 for _, cmd := range cmds {
272 wide = max(wide, cells(strings.TrimPrefix(joinPath(cmd.Path), prefix+" ")))
336 wide = max(wide, cells(rowText(cmd)))
273337 }
274338 for _, section := range []struct {
275339 title string
@@ -285,8 +349,7 @@ func (c *Ctx) helpNoun(w io.Writer, prefix string, cmds []Command) {
285349 c.heading(w, section.title)
286350 first = false
287351 }
288 verb := strings.TrimPrefix(joinPath(cmd.Path), prefix+" ")
289 fmt.Fprintf(w, " %s %s\n", pad(verb, wide), cmd.Summary)
352 fmt.Fprintf(w, " %s %s\n", pad(rowText(cmd), wide), cmd.Summary)
290353 }
291354 }
292355 fmt.Fprintln(w)
internal/control/help_test.go +30
@@ -241,3 +241,33 @@ func TestHelpPrintsTheCLIPath(t *testing.T) {
241241 }
242242 }
243243}
244
245func TestHelpRendersAnAliasedNounWithTheRegistryLayout(t *testing.T) {
246 var out, errOut bytes.Buffer
247 c := &Ctx{Stdout: &out, Stderr: &errOut, Term: Term{Cols: 100}, Scope: "full", CLIPath: "auth"}
248 c.Cfg.Server.SiteURL = "https://forge.test"
249 if code := Dispatch(c, []string{"help", "auth"}); code != protocol.ExitOK {
250 t.Fatalf("exit %d: %s", code, errOut.String())
251 }
252 got := out.String()
253 for _, want := range []string{"auth whoami", "auth keys list", "auth pgp add", "auth token create", "auth export"} {
254 if !strings.Contains(got, want) {
255 t.Errorf("missing %q in:\n%s", want, got)
256 }
257 }
258 if strings.Contains(got, "no command matches") {
259 t.Errorf("auth did not resolve: %s", got)
260 }
261}
262
263func TestHelpRendersAnAliasedNounInRegisteredFormOverSSH(t *testing.T) {
264 got := helpOut(t, Term{}, "auth")
265 for _, want := range []string{"whoami", "keys list", "pgp add", "token create", "account export"} {
266 if !strings.Contains(got, want) {
267 t.Errorf("missing %q in:\n%s", want, got)
268 }
269 }
270 if strings.Contains(got, "auth keys list") {
271 t.Errorf("stock ssh should not see the CLI-only auth prefix: %s", got)
272 }
273}