|
| 1 | +package cmd |
| 2 | + |
| 3 | +import ( |
| 4 | + "sort" |
| 5 | + "strings" |
| 6 | + |
| 7 | + "github.com/spf13/cobra" |
| 8 | + "github.com/spf13/pflag" |
| 9 | +) |
| 10 | + |
| 11 | +// cobra stores flag-group constraints in cmd.Annotations under these keys. |
| 12 | +// the keys arent exported in cobra/flag_groups.go, so we hardcode the |
| 13 | +// literal strings. each value is a []string where every element is one |
| 14 | +// group with member flag names joined by a single space. |
| 15 | +const ( |
| 16 | + annotOneRequired = "cobra_annotation_one_required" |
| 17 | + annotMutuallyExclusive = "cobra_annotation_mutually_exclusive" |
| 18 | + annotRequiredTogether = "cobra_annotation_required_if_others_set" |
| 19 | +) |
| 20 | + |
| 21 | +type AgentContext struct { |
| 22 | + Name string `json:"name"` |
| 23 | + Version string `json:"version"` |
| 24 | + Commit string `json:"commit"` |
| 25 | + Short string `json:"short"` |
| 26 | + Long string `json:"long"` |
| 27 | + GlobalFlags []Flag `json:"globalFlags"` |
| 28 | + Commands []Command `json:"commands"` |
| 29 | +} |
| 30 | + |
| 31 | +type Command struct { |
| 32 | + Name string `json:"name"` |
| 33 | + Path []string `json:"path"` |
| 34 | + Use string `json:"use"` |
| 35 | + Short string `json:"short"` |
| 36 | + Long string `json:"long"` |
| 37 | + Hidden bool `json:"hidden"` |
| 38 | + Runnable bool `json:"runnable"` |
| 39 | + Args []Positional `json:"args"` |
| 40 | + Flags []Flag `json:"flags"` |
| 41 | + FlagGroups FlagGroups `json:"flagGroups"` |
| 42 | + Commands []Command `json:"commands"` |
| 43 | +} |
| 44 | + |
| 45 | +type Flag struct { |
| 46 | + Name string `json:"name"` |
| 47 | + Shorthand string `json:"shorthand"` |
| 48 | + Type string `json:"type"` |
| 49 | + Default string `json:"default"` |
| 50 | + Description string `json:"description"` |
| 51 | + Required bool `json:"required"` |
| 52 | + Persistent bool `json:"persistent"` |
| 53 | +} |
| 54 | + |
| 55 | +type FlagGroups struct { |
| 56 | + OneRequired [][]string `json:"oneRequired"` |
| 57 | + MutuallyExclusive [][]string `json:"mutuallyExclusive"` |
| 58 | + RequiredTogether [][]string `json:"requiredTogether"` |
| 59 | +} |
| 60 | + |
| 61 | +func buildAgentContext(root *cobra.Command) AgentContext { |
| 62 | + rootPersistent := persistentFlagNames(root) |
| 63 | + return AgentContext{ |
| 64 | + Name: root.Name(), |
| 65 | + Version: version, |
| 66 | + Commit: commit, |
| 67 | + Short: root.Short, |
| 68 | + Long: root.Long, |
| 69 | + GlobalFlags: collectRootPersistentFlags(root), |
| 70 | + Commands: walkChildren(root, []string{}, rootPersistent), |
| 71 | + } |
| 72 | +} |
| 73 | + |
| 74 | +func persistentFlagNames(cmd *cobra.Command) map[string]struct{} { |
| 75 | + out := map[string]struct{}{} |
| 76 | + cmd.PersistentFlags().VisitAll(func(f *pflag.Flag) { out[f.Name] = struct{}{} }) |
| 77 | + return out |
| 78 | +} |
| 79 | + |
| 80 | +func collectRootPersistentFlags(root *cobra.Command) []Flag { |
| 81 | + out := []Flag{} |
| 82 | + root.PersistentFlags().VisitAll(func(f *pflag.Flag) { |
| 83 | + out = append(out, flagFromPFlag(f, true)) |
| 84 | + }) |
| 85 | + sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name }) |
| 86 | + return out |
| 87 | +} |
| 88 | + |
| 89 | +func walkChildren(parent *cobra.Command, parentPath []string, rootPersistent map[string]struct{}) []Command { |
| 90 | + out := []Command{} |
| 91 | + for _, c := range parent.Commands() { |
| 92 | + // filter out help, completion, man commands that are added by cobra and fang |
| 93 | + switch c.Name() { |
| 94 | + case "help", "completion", "man": |
| 95 | + continue |
| 96 | + } |
| 97 | + path := append(append([]string{}, parentPath...), c.Name()) |
| 98 | + out = append(out, Command{ |
| 99 | + Name: c.Name(), |
| 100 | + Path: path, |
| 101 | + Use: c.Use, |
| 102 | + Short: c.Short, |
| 103 | + Long: c.Long, |
| 104 | + Hidden: c.Hidden, |
| 105 | + Runnable: c.RunE != nil || c.Run != nil, |
| 106 | + Args: parsePositionals(c.Use), |
| 107 | + Flags: walkLocalFlags(c, rootPersistent), |
| 108 | + FlagGroups: extractFlagGroups(c), |
| 109 | + Commands: walkChildren(c, path, rootPersistent), |
| 110 | + }) |
| 111 | + } |
| 112 | + sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name }) |
| 113 | + return out |
| 114 | +} |
| 115 | + |
| 116 | +func walkLocalFlags(cmd *cobra.Command, rootPersistent map[string]struct{}) []Flag { |
| 117 | + out := []Flag{} |
| 118 | + cmd.LocalFlags().VisitAll(func(f *pflag.Flag) { |
| 119 | + if f.Name == "help" { |
| 120 | + return |
| 121 | + } |
| 122 | + if _, skip := rootPersistent[f.Name]; skip { |
| 123 | + return |
| 124 | + } |
| 125 | + persistent := cmd.PersistentFlags().Lookup(f.Name) != nil |
| 126 | + out = append(out, flagFromPFlag(f, persistent)) |
| 127 | + }) |
| 128 | + sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name }) |
| 129 | + return out |
| 130 | +} |
| 131 | + |
| 132 | +func flagFromPFlag(f *pflag.Flag, persistent bool) Flag { |
| 133 | + _, required := f.Annotations[cobra.BashCompOneRequiredFlag] |
| 134 | + return Flag{ |
| 135 | + Name: f.Name, |
| 136 | + Shorthand: f.Shorthand, |
| 137 | + Type: f.Value.Type(), |
| 138 | + Default: f.DefValue, |
| 139 | + Description: f.Usage, |
| 140 | + Required: required, |
| 141 | + Persistent: persistent, |
| 142 | + } |
| 143 | +} |
| 144 | + |
| 145 | +func extractFlagGroups(cmd *cobra.Command) FlagGroups { |
| 146 | + groups := FlagGroups{ |
| 147 | + OneRequired: [][]string{}, |
| 148 | + MutuallyExclusive: [][]string{}, |
| 149 | + RequiredTogether: [][]string{}, |
| 150 | + } |
| 151 | + // each flag in a group carries the same annotation value. dedupe by raw |
| 152 | + // joined string before splitting back into a name slice. |
| 153 | + seen := map[string]map[string]struct{}{ |
| 154 | + annotOneRequired: {}, |
| 155 | + annotMutuallyExclusive: {}, |
| 156 | + annotRequiredTogether: {}, |
| 157 | + } |
| 158 | + cmd.LocalFlags().VisitAll(func(f *pflag.Flag) { |
| 159 | + for _, key := range []string{annotOneRequired, annotMutuallyExclusive, annotRequiredTogether} { |
| 160 | + for _, raw := range f.Annotations[key] { |
| 161 | + if _, dup := seen[key][raw]; dup { |
| 162 | + continue |
| 163 | + } |
| 164 | + seen[key][raw] = struct{}{} |
| 165 | + names := strings.Split(raw, " ") |
| 166 | + switch key { |
| 167 | + case annotOneRequired: |
| 168 | + groups.OneRequired = append(groups.OneRequired, names) |
| 169 | + case annotMutuallyExclusive: |
| 170 | + groups.MutuallyExclusive = append(groups.MutuallyExclusive, names) |
| 171 | + case annotRequiredTogether: |
| 172 | + groups.RequiredTogether = append(groups.RequiredTogether, names) |
| 173 | + } |
| 174 | + } |
| 175 | + } |
| 176 | + }) |
| 177 | + for _, g := range []*[][]string{&groups.OneRequired, &groups.MutuallyExclusive, &groups.RequiredTogether} { |
| 178 | + sort.Slice(*g, func(i, j int) bool { |
| 179 | + return strings.Join((*g)[i], " ") < strings.Join((*g)[j], " ") |
| 180 | + }) |
| 181 | + } |
| 182 | + return groups |
| 183 | +} |
| 184 | + |
| 185 | +var agentContextCmd = &cobra.Command{ |
| 186 | + Use: "agent-context", |
| 187 | + Short: "Print a JSON description of all CLI commands and flags", |
| 188 | + Args: cobra.NoArgs, |
| 189 | + RunE: func(cmd *cobra.Command, _ []string) error { |
| 190 | + return printJSON(cmd.OutOrStdout(), buildAgentContext(rootCmd)) |
| 191 | + }, |
| 192 | +} |
| 193 | + |
| 194 | +func init() { |
| 195 | + rootCmd.AddCommand(agentContextCmd) |
| 196 | +} |
0 commit comments