Skip to content

Commit e59581b

Browse files
committed
agent-context
1 parent 04b19fc commit e59581b

4 files changed

Lines changed: 511 additions & 1 deletion

File tree

cmd/agent_context.go

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
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

Comments
 (0)