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
31 changes: 30 additions & 1 deletion cmd/help_input_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package cmd
import (
"strconv"
"strings"

"github.com/spf13/cobra"
)

func commandInputSchemaFor(args []jsonCommandArg, flags []jsonCommandFlag) jsonCommandInputSchema {
Expand All @@ -18,14 +20,19 @@ func commandInputSchemaFor(args []jsonCommandArg, flags []jsonCommandFlag) jsonC
property := jsonCommandInputProperty{
Type: commandInputPropertyType(arg.ValueKind, ""),
Description: arg.Description,
Enum: sortedCopyStringSlice(arg.EnumValues),
XCLIKind: "arg",
XCLIName: arg.Name,
XValueKind: arg.ValueKind,
XStreamDash: arg.StreamDash,
}
if arg.Variadic {
property.Type = "array"
property.Items = &jsonCommandInputProperty{Type: commandInputPropertyType(arg.ValueKind, "")}
property.Items = &jsonCommandInputProperty{
Type: commandInputPropertyType(arg.ValueKind, ""),
Enum: sortedCopyStringSlice(arg.EnumValues),
}
property.Enum = nil
if arg.Required {
property.MinItems = 1
}
Expand Down Expand Up @@ -76,6 +83,28 @@ func commandInputSchemaFor(args []jsonCommandArg, flags []jsonCommandFlag) jsonC
return schema
}

func commandInputSchemaFlags(cmd *cobra.Command, flags []jsonCommandFlag) []jsonCommandFlag {
filtered := make([]jsonCommandFlag, 0, len(flags))
excludeAsMember := commandInputSchemaExcludesAsMember(cmd)
for _, flag := range flags {
if excludeAsMember && flag.Name == "as-member" {
continue
}
filtered = append(filtered, flag)
}
return filtered
}

func commandInputSchemaExcludesAsMember(cmd *cobra.Command) bool {
if cmd == nil {
return false
}
return cmd.Root() == cmd ||
commandSkipsAuth(cmd) ||
isNoAuthCommand(cmd) ||
commandHasTopLevelName(cmd, "completion")
}

func commandInputSchemaIncludesFlag(flag jsonCommandFlag) bool {
switch flag.Name {
case "help", outputFlag:
Expand Down
20 changes: 11 additions & 9 deletions cmd/help_json.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,14 @@ type jsonCommandFlag struct {
}

type jsonCommandArg struct {
Name string `json:"name"`
Required bool `json:"required"`
Variadic bool `json:"variadic"`
Placement string `json:"placement"`
ValueKind string `json:"value_kind"`
Description string `json:"description"`
StreamDash bool `json:"stream_dash"`
Name string `json:"name"`
Required bool `json:"required"`
Variadic bool `json:"variadic"`
Placement string `json:"placement"`
ValueKind string `json:"value_kind"`
Description string `json:"description"`
StreamDash bool `json:"stream_dash"`
EnumValues []string `json:"enum_values"`
}

type jsonCommandExample struct {
Expand Down Expand Up @@ -377,6 +378,7 @@ func jsonCommandManifestFor(cmd *cobra.Command) jsonCommandManifest {
meta := commandManifestMetadataFor(path)
supportsStructuredOutput := commandSupportsStructuredOutput(cmd)
flags := jsonCommandFlags(cmd, meta.Flags)
args := normalizeJSONCommandArgs(meta.Args)

return jsonCommandManifest{
Path: path,
Expand All @@ -389,7 +391,7 @@ func jsonCommandManifestFor(cmd *cobra.Command) jsonCommandManifest {
AuthModes: commandManifestAuthModes(cmd),
DestructiveLevel: commandManifestDestructiveLevel(cmd),
ManifestVersion: commandManifestVersion,
Args: normalizeJSONCommandArgs(meta.Args),
Args: args,
Examples: normalizeJSONCommandExamples(meta.Examples),
SchemaRefs: commandManifestSchemaRefs(path, supportsStructuredOutput),
DropboxScopes: sortedCopyStringSlice(meta.DropboxScopes),
Expand All @@ -399,7 +401,7 @@ func jsonCommandManifestFor(cmd *cobra.Command) jsonCommandManifest {
ResultKinds: sortedCopyStringSlice(meta.ResultKinds),
WarningCodes: sortedCopyStringSlice(meta.WarningCodes),
MayPrompt: meta.MayPrompt,
InputSchema: commandInputSchemaFor(meta.Args, flags),
InputSchema: commandInputSchemaFor(args, commandInputSchemaFlags(cmd, flags)),
}
}

Expand Down
37 changes: 36 additions & 1 deletion cmd/help_json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,19 @@ func TestJSONHelpManifestFields(t *testing.T) {
if !strings.Contains(helpFromRoot.Use, "[flags]") {
t.Fatalf("help use from root manifest = %q, want flags in use line", helpFromRoot.Use)
}
if _, ok := helpFromRoot.InputSchema.Properties["as_member"]; ok {
t.Fatal("help input_schema contains as_member, want no auth-only global")
}
if _, ok := helpFromRoot.InputSchema.Properties["verbose"]; !ok {
t.Fatal("help input_schema missing verbose")
}
rootManifest := jsonHelpManifestByPath(t, got, "dbxcli")
if _, ok := rootManifest.InputSchema.Properties["as_member"]; ok {
t.Fatal("root input_schema contains as_member, want no auth-only global")
}
if _, ok := rootManifest.InputSchema.Properties["verbose"]; !ok {
t.Fatal("root input_schema missing verbose")
}
stdout, _, err = executeJSONHelpTestRoot(t, []string{"help", "help", "--output=json"})
if err != nil {
t.Fatalf("Execute help help returned error: %v", err)
Expand Down Expand Up @@ -374,17 +387,39 @@ func TestJSONHelpManifestV1SelectedCommandMetadata(t *testing.T) {
if !login.MayPrompt {
t.Fatal("login may_prompt = false, want true")
}
tokenType := jsonHelpArgByName(t, login.Args, "token-type")
assertStringSliceEqual(t, "login token-type enum", tokenType.EnumValues, []string{"personal", "team-access", "team-manage"})
tokenTypeSchema := assertJSONHelpInputProperty(t, login.InputSchema, "token_type", "string", "arg", "token-type", "auth_type")
assertStringSliceEqual(t, "login token_type input_schema enum", tokenTypeSchema.Enum, []string{"personal", "team-access", "team-manage"})
if _, ok := login.InputSchema.Properties["as_member"]; ok {
t.Fatal("login input_schema contains as_member, want no auth-only global")
}
if _, ok := login.InputSchema.Properties["verbose"]; !ok {
t.Fatal("login input_schema missing verbose")
}
if jsonHelpFlagByName(t, login.Flags, "app-key").ValueKind != "dropbox_app_key" {
t.Fatalf("login app-key value_kind = %q", jsonHelpFlagByName(t, login.Flags, "app-key").ValueKind)
}

completion := jsonCommandManifestFor(newCompletionCmd())
root := newJSONHelpTestRoot(t)
root.AddCommand(newCompletionCmd())
completionCmd, _, err := root.Find([]string{"completion"})
if err != nil {
t.Fatalf("find completion command: %v", err)
}
completion := jsonCommandManifestFor(completionCmd)
if completion.SupportsStructuredOutput {
t.Fatal("completion supports_structured_output = true, want false")
}
if len(completion.AuthModes) != 0 || len(completion.DropboxScopes) != 0 {
t.Fatalf("completion auth/scopes = %v/%v, want none", completion.AuthModes, completion.DropboxScopes)
}
if _, ok := completion.InputSchema.Properties["as_member"]; ok {
t.Fatal("completion input_schema contains as_member, want no auth-only global")
}
if _, ok := completion.InputSchema.Properties["verbose"]; !ok {
t.Fatal("completion input_schema missing verbose")
}
}

func TestJSONHelpManifestRegistryAudit(t *testing.T) {
Expand Down
9 changes: 8 additions & 1 deletion cmd/help_manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ var commandManifestRegistry = map[string]jsonCommandManifestMetadata{
Known: true,
},
"login": {
Args: []jsonCommandArg{commandArg("token-type", false, false, "auth_type", "Credential type: personal, team-access, or team-manage")},
Args: []jsonCommandArg{enumCommandArg("token-type", false, false, "auth_type", "Credential type: personal, team-access, or team-manage", "personal", "team-access", "team-manage")},
Examples: []jsonCommandExample{{Description: "Log in with the personal Dropbox app key", Command: "dbxcli login"}},
Flags: map[string]jsonCommandFlagMetadata{"app-key": {ValueKind: "dropbox_app_key"}},
MayPrompt: true,
Expand Down Expand Up @@ -374,9 +374,16 @@ func commandArg(name string, required bool, variadic bool, valueKind string, des
Placement: "positional",
ValueKind: valueKind,
Description: description,
EnumValues: []string{},
}
}

func enumCommandArg(name string, required bool, variadic bool, valueKind string, description string, enumValues ...string) jsonCommandArg {
arg := commandArg(name, required, variadic, valueKind, description)
arg.EnumValues = sortedCopyStringSlice(enumValues)
return arg
}

func streamCommandArg(name string, required bool, variadic bool, valueKind string, description string) jsonCommandArg {
arg := commandArg(name, required, variadic, valueKind, description)
arg.StreamDash = true
Expand Down
3 changes: 2 additions & 1 deletion cmd/testdata/json_contract/success_outputs.json
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,8 @@
"placement": "positional",
"value_kind": "dropbox_path",
"description": "Dropbox folder or file path",
"stream_dash": false
"stream_dash": false,
"enum_values": []
}
],
"examples": [
Expand Down
1 change: 1 addition & 0 deletions cmd/testdata/json_contract/success_schemas.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
],
"command_arg": [
"description",
"enum_values",
"name",
"placement",
"required",
Expand Down
7 changes: 4 additions & 3 deletions docs/automation.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,10 @@ stdin/stdout behavior, schema refs, and whether normal structured command
output is supported.

Each manifest result includes `input_schema`, a JSON Schema object for the
command's CLI inputs. It uses JSON-friendly names such as `if_exists` while
preserving original CLI names in `x-cli-name`, so tools can validate structured
input and then build the correct argument/flag invocation.
command's CLI inputs. It uses JSON-friendly names such as `if_exists`, includes
enum values for bounded arguments and flags, and preserves original CLI names in
`x-cli-name`, so tools can validate structured input and then build the correct
argument/flag invocation.

## Safe scripting patterns

Expand Down
2 changes: 1 addition & 1 deletion docs/commands/dbxcli_login.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ dbxcli login [personal|team-access|team-manage] [flags]
* Manifest version: `1`
* Auth modes: none
* Dropbox scopes: none
* Arguments: `token-type` (optional, auth_type)
* Arguments: `token-type` (optional, auth_type; values: `personal`, `team-access`, `team-manage`)
* Flag metadata: `--output` (values: `json`, `text`)


Expand Down
9 changes: 6 additions & 3 deletions docs/json-schema/v1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ metadata:
- structured `args`
- enriched `flags`, including enum values, conflicts, prompt behavior, and
sensitive inputs
- generated `input_schema` for command arguments and flags
- generated `input_schema` for command arguments and flags, including enum
values when an argument or flag has a bounded value set
- `examples`
- `schema_refs`
- best-effort audited `dropbox_scopes`
Expand All @@ -90,8 +91,10 @@ structured positional arguments and flags. It is intended for tool callers,
MCP-style integrations, and automation planners that need to validate command
inputs before building a CLI invocation. It excludes `--help` and `--output`,
uses JSON-friendly property names such as `if_exists`, and preserves the
original CLI names in `x-cli-name`. Sensitive inputs are marked with
`writeOnly` and `x-sensitive`; flag conflicts are listed in `x-conflicts`.
original CLI names in `x-cli-name`. Flags that are accepted globally but do not
affect a no-auth command may be omitted from that command's `input_schema`.
Sensitive inputs are marked with `writeOnly` and `x-sensitive`; flag conflicts
are listed in `x-conflicts`.

`scope_accuracy` is currently `audited_best_effort` for commands with audited
manifest metadata. Scope metadata is intended for planning and diagnostics;
Expand Down
1 change: 1 addition & 0 deletions docs/json-schema/v1/commands.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
],
"command_arg": [
"description",
"enum_values",
"name",
"placement",
"required",
Expand Down
9 changes: 8 additions & 1 deletion docs/json-schema/v1/manifest.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@
"placement",
"value_kind",
"description",
"stream_dash"
"stream_dash",
"enum_values"
],
"properties": {
"name": {
Expand All @@ -158,6 +159,12 @@
},
"stream_dash": {
"type": "boolean"
},
"enum_values": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
Expand Down
6 changes: 5 additions & 1 deletion tools/gen-docs/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,11 @@ func commandMetadataSection(command *cobra.Command) []byte {
if arg.StreamDash {
streamDash = ", `-` stream operand"
}
buf.WriteString(fmt.Sprintf("`%s` (%s, %s%s%s)", arg.Name, requirement, arg.ValueKind, variadic, streamDash))
enumValues := ""
if len(arg.EnumValues) > 0 {
enumValues = "; values: " + markdownValueList(arg.EnumValues)
}
buf.WriteString(fmt.Sprintf("`%s` (%s, %s%s%s%s)", arg.Name, requirement, arg.ValueKind, variadic, streamDash, enumValues))
}
buf.WriteString("\n")
}
Expand Down
Loading