diff --git a/cmd/help_input_schema.go b/cmd/help_input_schema.go index 339b050..a78084e 100644 --- a/cmd/help_input_schema.go +++ b/cmd/help_input_schema.go @@ -3,6 +3,8 @@ package cmd import ( "strconv" "strings" + + "github.com/spf13/cobra" ) func commandInputSchemaFor(args []jsonCommandArg, flags []jsonCommandFlag) jsonCommandInputSchema { @@ -18,6 +20,7 @@ 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, @@ -25,7 +28,11 @@ func commandInputSchemaFor(args []jsonCommandArg, flags []jsonCommandFlag) jsonC } 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 } @@ -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: diff --git a/cmd/help_json.go b/cmd/help_json.go index 4d3d7eb..d79af60 100644 --- a/cmd/help_json.go +++ b/cmd/help_json.go @@ -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 { @@ -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, @@ -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), @@ -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)), } } diff --git a/cmd/help_json_test.go b/cmd/help_json_test.go index 000f19e..dd19cbb 100644 --- a/cmd/help_json_test.go +++ b/cmd/help_json_test.go @@ -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) @@ -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) { diff --git a/cmd/help_manifest.go b/cmd/help_manifest.go index faff373..41eccd5 100644 --- a/cmd/help_manifest.go +++ b/cmd/help_manifest.go @@ -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, @@ -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 diff --git a/cmd/testdata/json_contract/success_outputs.json b/cmd/testdata/json_contract/success_outputs.json index 2bcca43..92cfbbc 100644 --- a/cmd/testdata/json_contract/success_outputs.json +++ b/cmd/testdata/json_contract/success_outputs.json @@ -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": [ diff --git a/cmd/testdata/json_contract/success_schemas.json b/cmd/testdata/json_contract/success_schemas.json index df805b7..6fa2756 100644 --- a/cmd/testdata/json_contract/success_schemas.json +++ b/cmd/testdata/json_contract/success_schemas.json @@ -39,6 +39,7 @@ ], "command_arg": [ "description", + "enum_values", "name", "placement", "required", diff --git a/docs/automation.md b/docs/automation.md index e14f9b2..8acf736 100644 --- a/docs/automation.md +++ b/docs/automation.md @@ -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 diff --git a/docs/commands/dbxcli_login.md b/docs/commands/dbxcli_login.md index bb23904..08785ea 100644 --- a/docs/commands/dbxcli_login.md +++ b/docs/commands/dbxcli_login.md @@ -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`) diff --git a/docs/json-schema/v1/README.md b/docs/json-schema/v1/README.md index ebddd7b..8240b00 100644 --- a/docs/json-schema/v1/README.md +++ b/docs/json-schema/v1/README.md @@ -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` @@ -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; diff --git a/docs/json-schema/v1/commands.json b/docs/json-schema/v1/commands.json index df805b7..6fa2756 100644 --- a/docs/json-schema/v1/commands.json +++ b/docs/json-schema/v1/commands.json @@ -39,6 +39,7 @@ ], "command_arg": [ "description", + "enum_values", "name", "placement", "required", diff --git a/docs/json-schema/v1/manifest.schema.json b/docs/json-schema/v1/manifest.schema.json index 6d83565..f79ab77 100644 --- a/docs/json-schema/v1/manifest.schema.json +++ b/docs/json-schema/v1/manifest.schema.json @@ -135,7 +135,8 @@ "placement", "value_kind", "description", - "stream_dash" + "stream_dash", + "enum_values" ], "properties": { "name": { @@ -158,6 +159,12 @@ }, "stream_dash": { "type": "boolean" + }, + "enum_values": { + "type": "array", + "items": { + "type": "string" + } } } }, diff --git a/tools/gen-docs/main.go b/tools/gen-docs/main.go index 93fce33..a7960ea 100644 --- a/tools/gen-docs/main.go +++ b/tools/gen-docs/main.go @@ -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") }