Skip to content

Commit 5115961

Browse files
committed
docs(cli): add cli.arguments annotations for connection get/update/delete/disable/enable/pause/unpause/upsert
Enables website generator to emit required positional placeholders in asides. Made-with: Cursor
1 parent f406d05 commit 5115961

9 files changed

Lines changed: 157 additions & 1 deletion

File tree

pkg/cmd/connection_delete.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ Examples:
3333
PreRunE: cc.validateFlags,
3434
RunE: cc.runConnectionDeleteCmd,
3535
}
36+
cc.cmd.Annotations = map[string]string{
37+
"cli.arguments": `[{"name":"connection-id","type":"string","description":"Connection ID","required":true}]`,
38+
}
3639

3740
cc.cmd.Flags().BoolVar(&cc.force, "force", false, "Force delete without confirmation")
3841

pkg/cmd/connection_disable.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ func newConnectionDisableCmd() *connectionDisableCmd {
2323
Long: LongDisableIntro(ResourceConnection),
2424
RunE: cc.runConnectionDisableCmd,
2525
}
26+
cc.cmd.Annotations = map[string]string{
27+
"cli.arguments": `[{"name":"connection-id","type":"string","description":"Connection ID","required":true}]`,
28+
}
2629

2730
return cc
2831
}

pkg/cmd/connection_enable.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ func newConnectionEnableCmd() *connectionEnableCmd {
2323
Long: LongEnableIntro(ResourceConnection),
2424
RunE: cc.runConnectionEnableCmd,
2525
}
26+
cc.cmd.Annotations = map[string]string{
27+
"cli.arguments": `[{"name":"connection-id","type":"string","description":"Connection ID","required":true}]`,
28+
}
2629

2730
return cc
2831
}

pkg/cmd/connection_get.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ Examples:
3939
hookdeck connection get my-connection`,
4040
RunE: cc.runConnectionGetCmd,
4141
}
42+
cc.cmd.Annotations = map[string]string{
43+
"cli.arguments": `[{"name":"connection-id-or-name","type":"string","description":"Connection ID or name","required":true}]`,
44+
}
4245

4346
cc.cmd.Flags().StringVar(&cc.output, "output", "", "Output format (json)")
4447
addIncludeSourceAuthFlagForConnection(cc.cmd, &cc.includeSourceAuth)

pkg/cmd/connection_pause.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ func newConnectionPauseCmd() *connectionPauseCmd {
2525
The connection will queue incoming events until unpaused.`,
2626
RunE: cc.runConnectionPauseCmd,
2727
}
28+
cc.cmd.Annotations = map[string]string{
29+
"cli.arguments": `[{"name":"connection-id","type":"string","description":"Connection ID","required":true}]`,
30+
}
2831

2932
return cc
3033
}

pkg/cmd/connection_unpause.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ func newConnectionUnpauseCmd() *connectionUnpauseCmd {
2525
The connection will start processing queued events.`,
2626
RunE: cc.runConnectionUnpauseCmd,
2727
}
28+
cc.cmd.Annotations = map[string]string{
29+
"cli.arguments": `[{"name":"connection-id","type":"string","description":"Connection ID","required":true}]`,
30+
}
2831

2932
return cc
3033
}

pkg/cmd/connection_update.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ Examples:
5858
PreRunE: cu.validateFlags,
5959
RunE: cu.runConnectionUpdateCmd,
6060
}
61+
cu.cmd.Annotations = map[string]string{
62+
"cli.arguments": `[{"name":"connection-id","type":"string","description":"Connection ID","required":true}]`,
63+
}
6164

6265
// Connection fields
6366
cu.cmd.Flags().StringVar(&cu.name, "name", "", "New connection name")

pkg/cmd/connection_upsert.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ func newConnectionUpsertCmd() *connectionUpsertCmd {
6363
PreRunE: cu.validateUpsertFlags,
6464
RunE: cu.runConnectionUpsertCmd,
6565
}
66+
cu.cmd.Annotations = map[string]string{
67+
"cli.arguments": `[{"name":"name","type":"string","description":"Connection name (create or update by name)","required":true}]`,
68+
}
6669

6770
// Reuse all flags from create command (name is now a positional argument)
6871
cu.cmd.Flags().StringVar(&cu.description, "description", "", "Connection description")

tools/generate-reference/main.go

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,8 @@ func generateHelpOutput(root *cobra.Command, path string, wrap wrapConfig) strin
282282
mainBuf.WriteString(globalFlagsTable(root))
283283
mainStr := strings.TrimRight(mainBuf.String(), "\n")
284284

285-
// Examples: available commands (comes after flags, no heading to avoid layout issues)
285+
// Examples: available commands (comes after flags, no heading to avoid layout issues).
286+
// For leaf subcommands, append placeholders for required flags/args so the line is runnable.
286287
var examplesBuf bytes.Buffer
287288
if c.HasAvailableSubCommands() {
288289
examplesBuf.WriteString("```bash\n")
@@ -292,6 +293,17 @@ func generateHelpOutput(root *cobra.Command, path string, wrap wrapConfig) strin
292293
}
293294
cmdPath := sub.CommandPath()
294295
examplesBuf.WriteString(cmdPath)
296+
// Append required flags/args placeholders for leaf commands so aside lines are runnable.
297+
if !sub.HasAvailableSubCommands() {
298+
suffix := exampleSuffixForRequired(sub)
299+
// Metrics subcommands always need --start/--end and --measures; ensure we never emit a bare line.
300+
if suffix == "" && strings.Contains(cmdPath, " metrics ") {
301+
suffix = "--start 2025-01-01T00:00:00Z --end 2025-01-02T00:00:00Z --measures count"
302+
}
303+
if suffix != "" {
304+
examplesBuf.WriteString(" " + suffix)
305+
}
306+
}
295307
if sub.Short != "" {
296308
examplesBuf.WriteString(" # " + sub.Short)
297309
}
@@ -633,6 +645,126 @@ type argSpec struct {
633645
Required bool `json:"required"`
634646
}
635647

648+
// bashCompOneRequiredFlag is the pflag annotation key Cobra sets for required flags.
649+
// Cobra uses this in ValidateRequiredFlags; we use it to discover required flags for example placeholders.
650+
const bashCompOneRequiredFlag = "cobra_annotation_bash_completion_one_required_flag"
651+
652+
// requiredFlags returns the list of required flag names for c (local flags only), in visit order.
653+
func requiredFlags(c *cobra.Command) []string {
654+
var names []string
655+
c.Flags().VisitAll(func(f *pflag.Flag) {
656+
if f.Hidden {
657+
return
658+
}
659+
ann, ok := f.Annotations[bashCompOneRequiredFlag]
660+
if !ok || len(ann) == 0 || ann[0] != "true" {
661+
return
662+
}
663+
names = append(names, f.Name)
664+
})
665+
return names
666+
}
667+
668+
// requiredArgsFromAnnotations returns required positional args from c.Annotations["cli.arguments"] in order.
669+
func requiredArgsFromAnnotations(c *cobra.Command) []argSpec {
670+
if c.Annotations == nil {
671+
return nil
672+
}
673+
raw, ok := c.Annotations["cli.arguments"]
674+
if !ok || raw == "" {
675+
return nil
676+
}
677+
var args []argSpec
678+
if err := json.Unmarshal([]byte(raw), &args); err != nil {
679+
return nil
680+
}
681+
var out []argSpec
682+
for _, a := range args {
683+
if a.Required {
684+
out = append(out, a)
685+
}
686+
}
687+
return out
688+
}
689+
690+
// exampleSuffixForRequired returns a placeholder string for required flags and args for use in example lines.
691+
// Used by the command-group aside and (optionally) per-command examples. Returns empty if no required flags/args.
692+
func exampleSuffixForRequired(c *cobra.Command) string {
693+
flags := requiredFlags(c)
694+
args := requiredArgsFromAnnotations(c)
695+
path := c.CommandPath()
696+
isMetrics := strings.Contains(path, " metrics ")
697+
// Metrics subcommands always require --start, --end, and --measures (CLI/API enforce this). If annotation wasn't found, still add them.
698+
if isMetrics && len(flags) == 0 && len(args) == 0 {
699+
return "--start 2025-01-01T00:00:00Z --end 2025-01-02T00:00:00Z --measures count"
700+
}
701+
if len(flags) == 0 && len(args) == 0 {
702+
return ""
703+
}
704+
var parts []string
705+
706+
// Time range: start and end (metrics subcommands)
707+
hasStart := false
708+
hasEnd := false
709+
for _, n := range flags {
710+
if n == "start" {
711+
hasStart = true
712+
}
713+
if n == "end" {
714+
hasEnd = true
715+
}
716+
}
717+
if hasStart && hasEnd && isMetrics {
718+
parts = append(parts, "--start 2025-01-01T00:00:00Z", "--end 2025-01-02T00:00:00Z")
719+
}
720+
// measures (metrics)
721+
for _, n := range flags {
722+
if n == "measures" && isMetrics {
723+
parts = append(parts, "--measures count")
724+
break
725+
}
726+
}
727+
// Other required flags not yet added
728+
for _, n := range flags {
729+
if (n == "start" || n == "end") && isMetrics {
730+
continue
731+
}
732+
if n == "measures" && isMetrics {
733+
continue
734+
}
735+
switch n {
736+
case "name":
737+
if !sliceContains(parts, "--name") {
738+
parts = append(parts, "--name <name>")
739+
}
740+
case "type":
741+
if !sliceContains(parts, "--type") {
742+
parts = append(parts, "--type <type>")
743+
}
744+
case "status":
745+
if !sliceContains(parts, "--status") {
746+
parts = append(parts, "--status acknowledged")
747+
}
748+
default:
749+
parts = append(parts, "--"+n+" <value>")
750+
}
751+
}
752+
// Required positionals
753+
for _, a := range args {
754+
parts = append(parts, "<"+a.Name+">")
755+
}
756+
return strings.Join(parts, " ")
757+
}
758+
759+
func sliceContains(parts []string, s string) bool {
760+
for _, p := range parts {
761+
if strings.HasPrefix(p, s+" ") || p == s {
762+
return true
763+
}
764+
}
765+
return false
766+
}
767+
636768
// renderArgumentsTable returns a markdown Arguments table if c.Annotations["cli.arguments"] contains
637769
// valid JSON array of argSpec. Used to document positional args before the Flags table.
638770
func renderArgumentsTable(c *cobra.Command) string {

0 commit comments

Comments
 (0)