Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions cmd/help_input_schema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package cmd

import (
"strconv"
"strings"
)

func commandInputSchemaFor(args []jsonCommandArg, flags []jsonCommandFlag) jsonCommandInputSchema {
schema := jsonCommandInputSchema{
Type: "object",
AdditionalProperties: false,
Required: []string{},
Properties: map[string]jsonCommandInputProperty{},
}

for _, arg := range args {
name := commandInputPropertyName(arg.Name)
property := jsonCommandInputProperty{
Type: commandInputPropertyType(arg.ValueKind, ""),
Description: arg.Description,
XCLIKind: "arg",
XCLIName: arg.Name,
XValueKind: arg.ValueKind,
XStreamDash: arg.StreamDash,
}
if arg.Variadic {
property.Type = "array"
property.Items = &jsonCommandInputProperty{Type: commandInputPropertyType(arg.ValueKind, "")}
if arg.Required {
property.MinItems = 1
}
}
if format := commandInputPropertyFormat(arg.ValueKind); format != "" && !arg.Variadic {
property.Format = format
}
schema.Properties[name] = property
if arg.Required {
schema.Required = append(schema.Required, name)
}
}

for _, flag := range flags {
if !commandInputSchemaIncludesFlag(flag) {
continue
}
name := commandInputPropertyName(flag.Name)
propertyType := commandInputPropertyType(flag.ValueKind, flag.Type)
property := jsonCommandInputProperty{
Type: propertyType,
Description: flag.Usage,
Enum: sortedCopyStringSlice(flag.EnumValues),
XCLIKind: "flag",
XCLIName: flag.Name,
XValueKind: flag.ValueKind,
XSensitive: flag.Sensitive,
XConflicts: commandInputPropertyNames(flag.Conflicts),
XInherited: flag.Inherited,
XShorthand: flag.Shorthand,
XMayPrompt: flag.MayPrompt,
}
if format := commandInputPropertyFormat(flag.ValueKind); format != "" {
property.Format = format
}
if flag.Sensitive {
property.WriteOnly = true
}
if value, ok := commandInputFlagDefault(flag, propertyType); ok {
property.Default = value
}
schema.Properties[name] = property
if flag.Required {
schema.Required = append(schema.Required, name)
}
}

return schema
}

func commandInputSchemaIncludesFlag(flag jsonCommandFlag) bool {
switch flag.Name {
case "help", outputFlag:
return false
default:
return true
}
}

func commandInputPropertyNames(names []string) []string {
result := make([]string, 0, len(names))
for _, name := range names {
result = append(result, commandInputPropertyName(name))
}
return sortedCopyStringSlice(result)
}

func commandInputPropertyName(name string) string {
return strings.ReplaceAll(name, "-", "_")
}

func commandInputPropertyType(valueKind string, flagType string) string {
switch valueKind {
case "boolean":
return "boolean"
case "bytes", "integer":
return "integer"
case "enum", "string", "dropbox_path", "local_path", "dropbox_member_id",
"dropbox_app_key", "local_file", "secret", "rfc3339_timestamp",
"url", "email", "account_id", "auth_type", "command_path", "revision":
return "string"
}

switch flagType {
case "bool":
return "boolean"
case "int", "int64", "uint64":
return "integer"
default:
return "string"
}
}

func commandInputPropertyFormat(valueKind string) string {
switch valueKind {
case "email":
return "email"
case "rfc3339_timestamp":
return "date-time"
case "url":
return "uri"
default:
return ""
}
}

func commandInputFlagDefault(flag jsonCommandFlag, propertyType string) (any, bool) {
if flag.Default == "" {
return nil, false
}

switch propertyType {
case "boolean":
value, err := strconv.ParseBool(flag.Default)
if err != nil {
return nil, false
}
return value, true
case "integer":
value, err := strconv.ParseInt(flag.Default, 10, 64)
if err != nil {
return nil, false
}
return value, true
default:
return flag.Default, true
}
}
68 changes: 67 additions & 1 deletion cmd/help_json.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ type jsonCommandManifest struct {
ResultKinds []string `json:"result_kinds"`
WarningCodes []string `json:"warning_codes"`
MayPrompt bool `json:"may_prompt"`
InputSchema jsonCommandInputSchema `json:"input_schema"`
}

type jsonCommandFlag struct {
Expand Down Expand Up @@ -94,6 +95,34 @@ type jsonCommandStdinStdout struct {
Stderr string `json:"stderr"`
}

type jsonCommandInputSchema struct {
Type string `json:"type"`
AdditionalProperties bool `json:"additionalProperties"`
Required []string `json:"required"`
Properties map[string]jsonCommandInputProperty `json:"properties"`
}

type jsonCommandInputProperty struct {
Type string `json:"type"`
Description string `json:"description,omitempty"`
Items *jsonCommandInputProperty `json:"items,omitempty"`
Enum []string `json:"enum,omitempty"`
Default any `json:"default,omitempty"`
Format string `json:"format,omitempty"`
MinItems int `json:"minItems,omitempty"`
WriteOnly bool `json:"writeOnly,omitempty"`

XCLIKind string `json:"x-cli-kind,omitempty"`
XCLIName string `json:"x-cli-name,omitempty"`
XValueKind string `json:"x-value-kind,omitempty"`
XStreamDash bool `json:"x-stream-dash,omitempty"`
XSensitive bool `json:"x-sensitive,omitempty"`
XConflicts []string `json:"x-conflicts,omitempty"`
XInherited bool `json:"x-inherited,omitempty"`
XShorthand string `json:"x-shorthand,omitempty"`
XMayPrompt bool `json:"x-may-prompt,omitempty"`
}

func installJSONHelp(root *cobra.Command) {
defaultHelp := root.HelpFunc()
root.SetHelpFunc(func(cmd *cobra.Command, args []string) {
Expand Down Expand Up @@ -185,6 +214,41 @@ func rawArgsRequestJSONHelp(args []string) bool {
return outputJSONRequested(args) && (rawArgsHaveHelpFlag(args) || rawArgsFirstCommand(args) == "help")
}

func rawArgsHelpOutputFormatError(args []string) error {
if !rawArgsHaveHelpFlag(args) {
return nil
}
value, ok := rawArgsOutputValue(args)
if !ok {
return nil
}
_, err := parseOutputFormat(value)
return err
}

func rawArgsOutputValue(args []string) (string, bool) {
value := ""
found := false
for i := 0; i < len(args); i++ {
arg := args[i]
switch {
case arg == "--":
return value, found
case arg == "--output":
if i+1 >= len(args) {
return value, found
}
value = args[i+1]
found = true
i++
case strings.HasPrefix(arg, "--output="):
value = strings.TrimPrefix(arg, "--output=")
found = true
}
}
return value, found
}

func rawArgsHaveHelpFlag(args []string) bool {
for _, arg := range args {
switch arg {
Expand Down Expand Up @@ -312,14 +376,15 @@ func jsonCommandManifestFor(cmd *cobra.Command) jsonCommandManifest {
path := jsonManifestCommandPath(cmd)
meta := commandManifestMetadataFor(path)
supportsStructuredOutput := commandSupportsStructuredOutput(cmd)
flags := jsonCommandFlags(cmd, meta.Flags)

return jsonCommandManifest{
Path: path,
Use: cmd.UseLine(),
Short: cmd.Short,
Aliases: sortedCopyStringSlice(cmd.Aliases),
Runnable: cmd.Runnable(),
Flags: jsonCommandFlags(cmd, meta.Flags),
Flags: flags,
SupportsStructuredOutput: supportsStructuredOutput,
AuthModes: commandManifestAuthModes(cmd),
DestructiveLevel: commandManifestDestructiveLevel(cmd),
Expand All @@ -334,6 +399,7 @@ func jsonCommandManifestFor(cmd *cobra.Command) jsonCommandManifest {
ResultKinds: sortedCopyStringSlice(meta.ResultKinds),
WarningCodes: sortedCopyStringSlice(meta.WarningCodes),
MayPrompt: meta.MayPrompt,
InputSchema: commandInputSchemaFor(meta.Args, flags),
}
}

Expand Down
Loading
Loading