|
| 1 | +package cmd |
| 2 | + |
| 3 | +import ( |
| 4 | + "encoding/json" |
| 5 | + "fmt" |
| 6 | + "os" |
| 7 | + "strings" |
| 8 | + |
| 9 | + "github.com/spf13/cobra" |
| 10 | + "github.com/spf13/pflag" |
| 11 | +) |
| 12 | + |
| 13 | +// agentContextSchemaVersion is bumped whenever the agent-context JSON shape |
| 14 | +// changes in a backwards-incompatible way. |
| 15 | +const agentContextSchemaVersion = "1" |
| 16 | + |
| 17 | +// agentContextEnumAnnotationPrefix is the cobra Annotations key prefix used to |
| 18 | +// declare a known enum set for a flag. Example: |
| 19 | +// |
| 20 | +// cmd.Annotations["vers:enum:visibility"] = "public,private,unlisted" |
| 21 | +const agentContextEnumAnnotationPrefix = "vers:enum:" |
| 22 | + |
| 23 | +var agentContextPretty bool |
| 24 | + |
| 25 | +var agentContextCmd = &cobra.Command{ |
| 26 | + Use: "agent-context", |
| 27 | + Short: "Emit a versioned JSON description of the CLI for agent consumption", |
| 28 | + Long: `Emit a versioned, machine-readable JSON description of every command, |
| 29 | +subcommand, and flag exposed by the CLI. |
| 30 | +
|
| 31 | +Agents should consume this output instead of parsing --help text. The top-level |
| 32 | +"schema_version" field lets consumers detect breaking shape changes.`, |
| 33 | + SilenceUsage: true, |
| 34 | + SilenceErrors: true, |
| 35 | + Run: func(cmd *cobra.Command, args []string) { |
| 36 | + doc := buildAgentContext(cmd.Root()) |
| 37 | + |
| 38 | + var ( |
| 39 | + out []byte |
| 40 | + err error |
| 41 | + ) |
| 42 | + if agentContextPretty { |
| 43 | + out, err = json.MarshalIndent(doc, "", " ") |
| 44 | + } else { |
| 45 | + out, err = json.Marshal(doc) |
| 46 | + } |
| 47 | + if err != nil { |
| 48 | + // Per spec this command must never fail; fall back to a minimal |
| 49 | + // stub document and exit 0. |
| 50 | + fmt.Fprintln(os.Stdout, `{"schema_version":"`+agentContextSchemaVersion+`","commands":{}}`) |
| 51 | + return |
| 52 | + } |
| 53 | + fmt.Fprintln(os.Stdout, string(out)) |
| 54 | + }, |
| 55 | +} |
| 56 | + |
| 57 | +func init() { |
| 58 | + agentContextCmd.Flags().BoolVar(&agentContextPretty, "pretty", false, "Emit indented JSON instead of compact JSON") |
| 59 | + rootCmd.AddCommand(agentContextCmd) |
| 60 | +} |
| 61 | + |
| 62 | +// agentContextDoc is the top-level JSON shape emitted by `vers agent-context`. |
| 63 | +type agentContextDoc struct { |
| 64 | + SchemaVersion string `json:"schema_version"` |
| 65 | + CLI agentContextCLI `json:"cli"` |
| 66 | + Commands map[string]*agentContextCommand `json:"commands"` |
| 67 | + AvailableProfiles []string `json:"available_profiles"` |
| 68 | + Feedback agentContextFeedback `json:"feedback"` |
| 69 | +} |
| 70 | + |
| 71 | +type agentContextCLI struct { |
| 72 | + Name string `json:"name"` |
| 73 | + Version string `json:"version"` |
| 74 | + Description string `json:"description"` |
| 75 | +} |
| 76 | + |
| 77 | +type agentContextCommand struct { |
| 78 | + Use string `json:"use"` |
| 79 | + Short string `json:"short"` |
| 80 | + Long string `json:"long,omitempty"` |
| 81 | + Aliases []string `json:"aliases,omitempty"` |
| 82 | + Args agentContextArgs `json:"args"` |
| 83 | + Async bool `json:"async"` |
| 84 | + Flags map[string]*agentContextFlag `json:"flags"` |
| 85 | + Subcommands map[string]*agentContextCommand `json:"subcommands,omitempty"` |
| 86 | +} |
| 87 | + |
| 88 | +type agentContextArgs struct { |
| 89 | + Min int `json:"min"` |
| 90 | + Max int `json:"max"` |
| 91 | +} |
| 92 | + |
| 93 | +type agentContextFlag struct { |
| 94 | + Shorthand string `json:"shorthand,omitempty"` |
| 95 | + Type string `json:"type"` |
| 96 | + Default string `json:"default"` |
| 97 | + Usage string `json:"usage"` |
| 98 | + Required bool `json:"required"` |
| 99 | + Enum []string `json:"enum,omitempty"` |
| 100 | +} |
| 101 | + |
| 102 | +type agentContextFeedback struct { |
| 103 | + LocalPath string `json:"local_path"` |
| 104 | + EndpointConfigured bool `json:"endpoint_configured"` |
| 105 | +} |
| 106 | + |
| 107 | +func buildAgentContext(root *cobra.Command) *agentContextDoc { |
| 108 | + doc := &agentContextDoc{ |
| 109 | + SchemaVersion: agentContextSchemaVersion, |
| 110 | + CLI: agentContextCLI{ |
| 111 | + Name: root.Name(), |
| 112 | + Version: Version, |
| 113 | + Description: strings.TrimSpace(root.Long), |
| 114 | + }, |
| 115 | + Commands: map[string]*agentContextCommand{}, |
| 116 | + AvailableProfiles: []string{}, |
| 117 | + Feedback: agentContextFeedback{ |
| 118 | + LocalPath: "~/.vers/feedback.jsonl", |
| 119 | + EndpointConfigured: os.Getenv("VERS_FEEDBACK_ENDPOINT") != "", |
| 120 | + }, |
| 121 | + } |
| 122 | + |
| 123 | + for _, c := range root.Commands() { |
| 124 | + if shouldSkipCommand(c) { |
| 125 | + continue |
| 126 | + } |
| 127 | + doc.Commands[c.Name()] = describeCommand(c) |
| 128 | + } |
| 129 | + return doc |
| 130 | +} |
| 131 | + |
| 132 | +func shouldSkipCommand(c *cobra.Command) bool { |
| 133 | + if c.Hidden { |
| 134 | + return true |
| 135 | + } |
| 136 | + switch c.Name() { |
| 137 | + case "help", "completion": |
| 138 | + return true |
| 139 | + } |
| 140 | + return false |
| 141 | +} |
| 142 | + |
| 143 | +func describeCommand(c *cobra.Command) *agentContextCommand { |
| 144 | + out := &agentContextCommand{ |
| 145 | + Use: c.Use, |
| 146 | + Short: c.Short, |
| 147 | + Long: strings.TrimSpace(c.Long), |
| 148 | + Args: agentContextArgs{Min: 0, Max: -1}, |
| 149 | + Flags: map[string]*agentContextFlag{}, |
| 150 | + } |
| 151 | + if len(c.Aliases) > 0 { |
| 152 | + out.Aliases = append(out.Aliases, c.Aliases...) |
| 153 | + } |
| 154 | + |
| 155 | + // Collect flags (local + inherited from parents), excluding hidden & |
| 156 | + // deprecated. Inherited flags give agents a complete picture without |
| 157 | + // having to re-walk the parent chain themselves. |
| 158 | + c.Flags().VisitAll(func(f *pflag.Flag) { |
| 159 | + if entry := describeFlag(c, f); entry != nil { |
| 160 | + out.Flags["--"+f.Name] = entry |
| 161 | + } |
| 162 | + }) |
| 163 | + |
| 164 | + // async = visible --wait flag exists. |
| 165 | + if w := c.Flags().Lookup("wait"); w != nil && !w.Hidden && len(w.Deprecated) == 0 { |
| 166 | + out.Async = true |
| 167 | + } |
| 168 | + |
| 169 | + for _, sub := range c.Commands() { |
| 170 | + if shouldSkipCommand(sub) { |
| 171 | + continue |
| 172 | + } |
| 173 | + if out.Subcommands == nil { |
| 174 | + out.Subcommands = map[string]*agentContextCommand{} |
| 175 | + } |
| 176 | + out.Subcommands[sub.Name()] = describeCommand(sub) |
| 177 | + } |
| 178 | + |
| 179 | + return out |
| 180 | +} |
| 181 | + |
| 182 | +func describeFlag(parent *cobra.Command, f *pflag.Flag) *agentContextFlag { |
| 183 | + if f.Hidden || len(f.Deprecated) > 0 { |
| 184 | + return nil |
| 185 | + } |
| 186 | + entry := &agentContextFlag{ |
| 187 | + Shorthand: f.Shorthand, |
| 188 | + Type: f.Value.Type(), |
| 189 | + Default: f.DefValue, |
| 190 | + Usage: f.Usage, |
| 191 | + } |
| 192 | + if _, ok := f.Annotations[cobra.BashCompOneRequiredFlag]; ok { |
| 193 | + entry.Required = true |
| 194 | + } |
| 195 | + if parent != nil && parent.Annotations != nil { |
| 196 | + if raw, ok := parent.Annotations[agentContextEnumAnnotationPrefix+f.Name]; ok && raw != "" { |
| 197 | + parts := strings.Split(raw, ",") |
| 198 | + vals := make([]string, 0, len(parts)) |
| 199 | + for _, p := range parts { |
| 200 | + if v := strings.TrimSpace(p); v != "" { |
| 201 | + vals = append(vals, v) |
| 202 | + } |
| 203 | + } |
| 204 | + if len(vals) > 0 { |
| 205 | + entry.Enum = vals |
| 206 | + } |
| 207 | + } |
| 208 | + } |
| 209 | + return entry |
| 210 | +} |
0 commit comments