|
5 | 5 | "fmt" |
6 | 6 | "os" |
7 | 7 | "os/exec" |
| 8 | + "strconv" |
8 | 9 | "strings" |
9 | 10 | "time" |
10 | 11 |
|
@@ -185,16 +186,20 @@ func registerUserCommands(root *cobra.Command, cmds []lib.Command) { |
185 | 186 | continue |
186 | 187 | } |
187 | 188 | name := cmd.Name |
188 | | - root.AddCommand(&cobra.Command{ |
189 | | - Use: name, |
| 189 | + def := cmd |
| 190 | + coCmd := &cobra.Command{ |
| 191 | + Use: buildCommandUse(name, def.Args), |
190 | 192 | Short: cmd.Usage, |
191 | 193 | Annotations: map[string]string{CommandSourceAnnotation: CommandSourceUser}, |
192 | | - RunE: func(c *cobra.Command, args []string) error { |
193 | | - return raid.WithMutationLock(func() error { |
194 | | - return raid.ExecuteCommand(name, args) |
195 | | - }) |
196 | | - }, |
197 | | - }) |
| 194 | + } |
| 195 | + attachCommandArgsAndFlags(coCmd, def) |
| 196 | + coCmd.RunE = func(c *cobra.Command, args []string) error { |
| 197 | + named := gatherCommandValues(c, def, args) |
| 198 | + return raid.WithMutationLock(func() error { |
| 199 | + return raid.ExecuteCommand(name, args, named) |
| 200 | + }) |
| 201 | + } |
| 202 | + root.AddCommand(coCmd) |
198 | 203 | } |
199 | 204 | } |
200 | 205 |
|
@@ -222,20 +227,129 @@ func registerRepoCommands(root *cobra.Command, repos []lib.Repo) { |
222 | 227 | } |
223 | 228 | for _, cmd := range repo.Commands { |
224 | 229 | cmdName := cmd.Name |
225 | | - repoCmd.AddCommand(&cobra.Command{ |
226 | | - Use: cmdName, |
| 230 | + def := cmd |
| 231 | + subCmd := &cobra.Command{ |
| 232 | + Use: buildCommandUse(cmdName, def.Args), |
227 | 233 | Short: cmd.Usage, |
228 | | - RunE: func(c *cobra.Command, args []string) error { |
229 | | - return raid.WithMutationLock(func() error { |
230 | | - return raid.ExecuteRepoCommand(repoName, cmdName, args) |
231 | | - }) |
232 | | - }, |
233 | | - }) |
| 234 | + } |
| 235 | + attachCommandArgsAndFlags(subCmd, def) |
| 236 | + subCmd.RunE = func(c *cobra.Command, args []string) error { |
| 237 | + named := gatherCommandValues(c, def, args) |
| 238 | + return raid.WithMutationLock(func() error { |
| 239 | + return raid.ExecuteRepoCommand(repoName, cmdName, args, named) |
| 240 | + }) |
| 241 | + } |
| 242 | + repoCmd.AddCommand(subCmd) |
234 | 243 | } |
235 | 244 | root.AddCommand(repoCmd) |
236 | 245 | } |
237 | 246 | } |
238 | 247 |
|
| 248 | +// buildCommandUse renders the cobra Use string with declared positional |
| 249 | +// args so `--help` shows the expected invocation shape, e.g. |
| 250 | +// `patch <ticket> [comment]`. |
| 251 | +func buildCommandUse(name string, args []lib.Arg) string { |
| 252 | + if len(args) == 0 { |
| 253 | + return name |
| 254 | + } |
| 255 | + var b strings.Builder |
| 256 | + b.WriteString(name) |
| 257 | + for _, a := range args { |
| 258 | + b.WriteByte(' ') |
| 259 | + if a.Required { |
| 260 | + b.WriteString("<") |
| 261 | + b.WriteString(a.Name) |
| 262 | + b.WriteString(">") |
| 263 | + } else { |
| 264 | + b.WriteString("[") |
| 265 | + b.WriteString(a.Name) |
| 266 | + b.WriteString("]") |
| 267 | + } |
| 268 | + } |
| 269 | + return b.String() |
| 270 | +} |
| 271 | + |
| 272 | +// attachCommandArgsAndFlags configures a cobra subcommand from the lib.Command |
| 273 | +// definition: positional-arg cardinality validators and declared flags. Type |
| 274 | +// defaults to "string" when unset; "bool" / "int" are also recognised. A |
| 275 | +// missing or wrong-typed Default falls back to the zero value rather than |
| 276 | +// failing — the schema's `oneOf` already guards the YAML side. |
| 277 | +func attachCommandArgsAndFlags(co *cobra.Command, cmd lib.Command) { |
| 278 | + if n := len(cmd.Args); n > 0 { |
| 279 | + req := 0 |
| 280 | + for _, a := range cmd.Args { |
| 281 | + if a.Required { |
| 282 | + req++ |
| 283 | + } |
| 284 | + } |
| 285 | + switch { |
| 286 | + case req == n: |
| 287 | + co.Args = cobra.ExactArgs(n) |
| 288 | + case req == 0: |
| 289 | + co.Args = cobra.MaximumNArgs(n) |
| 290 | + default: |
| 291 | + co.Args = cobra.RangeArgs(req, n) |
| 292 | + } |
| 293 | + } |
| 294 | + |
| 295 | + for _, f := range cmd.Flags { |
| 296 | + switch f.Type { |
| 297 | + case "bool": |
| 298 | + d, _ := f.Default.(bool) |
| 299 | + co.Flags().BoolP(f.Name, f.Short, d, f.Usage) |
| 300 | + case "int": |
| 301 | + d := 0 |
| 302 | + switch v := f.Default.(type) { |
| 303 | + case int: |
| 304 | + d = v |
| 305 | + case float64: |
| 306 | + // YAML numbers unmarshal into float64 through interface{}. |
| 307 | + d = int(v) |
| 308 | + } |
| 309 | + co.Flags().IntP(f.Name, f.Short, d, f.Usage) |
| 310 | + default: |
| 311 | + d, _ := f.Default.(string) |
| 312 | + co.Flags().StringP(f.Name, f.Short, d, f.Usage) |
| 313 | + } |
| 314 | + if f.Required { |
| 315 | + _ = co.MarkFlagRequired(f.Name) |
| 316 | + } |
| 317 | + } |
| 318 | +} |
| 319 | + |
| 320 | +// gatherCommandValues builds the name → value map that ExecuteCommand binds |
| 321 | +// to env vars. Returns nil when the command has no declared args/flags so |
| 322 | +// the legacy positional-only path stays untouched. |
| 323 | +func gatherCommandValues(co *cobra.Command, cmd lib.Command, posArgs []string) map[string]string { |
| 324 | + if len(cmd.Args) == 0 && len(cmd.Flags) == 0 { |
| 325 | + return nil |
| 326 | + } |
| 327 | + out := make(map[string]string, len(cmd.Args)+len(cmd.Flags)) |
| 328 | + for i, a := range cmd.Args { |
| 329 | + if i < len(posArgs) { |
| 330 | + out[a.Name] = posArgs[i] |
| 331 | + } |
| 332 | + } |
| 333 | + for _, f := range cmd.Flags { |
| 334 | + switch f.Type { |
| 335 | + case "bool": |
| 336 | + v, _ := co.Flags().GetBool(f.Name) |
| 337 | + if v { |
| 338 | + out[f.Name] = "true" |
| 339 | + } else { |
| 340 | + out[f.Name] = "false" |
| 341 | + } |
| 342 | + case "int": |
| 343 | + v, _ := co.Flags().GetInt(f.Name) |
| 344 | + out[f.Name] = strconv.Itoa(v) |
| 345 | + default: |
| 346 | + v, _ := co.Flags().GetString(f.Name) |
| 347 | + out[f.Name] = v |
| 348 | + } |
| 349 | + } |
| 350 | + return out |
| 351 | +} |
| 352 | + |
239 | 353 | func hasCommand(root *cobra.Command, name string) bool { |
240 | 354 | for _, c := range root.Commands() { |
241 | 355 | if c.Name() == name { |
|
0 commit comments