diff --git a/README.md b/README.md index 14e32b0..816d424 100644 --- a/README.md +++ b/README.md @@ -192,10 +192,13 @@ JSON error responses use stable `error.code` values: | `unknown_flag` | Cobra could not resolve a flag. | | `command_failed` | Fallback for failures without a more specific stable code. | -Successful JSON responses for migrated commands return an `input` object, a `results` array, and a `warnings` array. Result payloads are command-specific. For commands such as `mkdir`, each result reports what happened to the requested path: +Successful JSON responses for migrated commands return `ok: true`, `schema_version: "1"`, `command`, an `input` object, a `results` array, and a `warnings` array. Result payloads are command-specific. Public top-level schemas live under [docs/json-schema/v1](docs/json-schema/v1/). If a multi-target or recursive command fails after some side effects have already happened, dbxcli returns a JSON error envelope and does not include partial success results. For commands such as `mkdir`, each result reports what happened to the requested path: ```json { + "ok": true, + "schema_version": "1", + "command": "mkdir", "input": { "path": "/new-folder", "parents": false @@ -224,9 +227,14 @@ For `cp` and `mv`, each result input object uses `from_path` and `to_path`: ```json { + "ok": true, + "schema_version": "1", + "command": "cp", "input": {}, "results": [ { + "status": "copied", + "kind": "file", "input": { "from_path": "/Reports/old.pdf", "to_path": "/Reports/copy.pdf" @@ -249,9 +257,14 @@ For commands such as `rm`, `input` uses command-specific path and flag fields: ```json { + "ok": true, + "schema_version": "1", + "command": "rm", "input": {}, "results": [ { + "status": "deleted", + "kind": "file", "input": { "path": "/old-file.txt", "permanent": false, @@ -276,6 +289,9 @@ For commands such as `rm`, `input` uses command-specific path and flag fields: ```json { + "ok": true, + "schema_version": "1", + "command": "put", "input": { "source": "README.md", "target": "/README.md", @@ -311,6 +327,9 @@ Entry-list commands such as `ls`, `search`, and `revs` use the operation-style w ```json { + "ok": true, + "schema_version": "1", + "command": "ls", "input": { "path": "/Reports", "recursive": false, @@ -324,6 +343,7 @@ Entry-list commands such as `ls`, `search`, and `revs` use the operation-style w { "status": "listed", "kind": "file", + "input": {}, "result": { "type": "file", "path_display": "/Reports/q1.pdf", @@ -342,9 +362,13 @@ Version, account, and usage commands use the operation-style wrapper with a sing ```json { + "ok": true, + "schema_version": "1", + "command": "version", "input": {}, "results": [ { + "status": "reported", "kind": "version", "input": {}, "result": { @@ -360,9 +384,13 @@ Version, account, and usage commands use the operation-style wrapper with a sing ```json { + "ok": true, + "schema_version": "1", + "command": "account", "input": {}, "results": [ { + "status": "found", "kind": "account", "input": {}, "result": { @@ -380,9 +408,13 @@ Version, account, and usage commands use the operation-style wrapper with a sing ```json { + "ok": true, + "schema_version": "1", + "command": "du", "input": {}, "results": [ { + "status": "reported", "kind": "space_usage", "input": {}, "result": { @@ -402,6 +434,9 @@ Shared-link commands use the same operation-style wrapper. `share-link create`, ```json { + "ok": true, + "schema_version": "1", + "command": "share-link create", "input": { "path": "/Reports/old.pdf" }, @@ -409,6 +444,7 @@ Shared-link commands use the same operation-style wrapper. `share-link create`, { "status": "created", "kind": "file", + "input": {}, "result": { "type": "file", "url": "https://www.dropbox.com/s/...", @@ -429,11 +465,15 @@ The legacy `share list folder` command also supports operation-style JSON. It us ```json { + "ok": true, + "schema_version": "1", + "command": "share list folder", "input": {}, "results": [ { "status": "listed", "kind": "shared_folder", + "input": {}, "result": { "type": "shared_folder", "name": "Reports", @@ -454,11 +494,15 @@ Team commands use the same operation-style wrapper. `team info` returns a single ```json { + "ok": true, + "schema_version": "1", + "command": "team list-members", "input": {}, "results": [ { "status": "listed", "kind": "team_member", + "input": {}, "result": { "type": "team_member", "team_member_id": "dbmid:...", @@ -480,6 +524,8 @@ In JSON mode, command errors are written to stdout as JSON, including errors fro ```json { "ok": false, + "schema_version": "1", + "command": "rm", "error": { "message": "path exists and is not a folder: /old-file.txt", "code": "path_conflict" diff --git a/cmd/account.go b/cmd/account.go index b81984b..c5e23ed 100644 --- a/cmd/account.go +++ b/cmd/account.go @@ -58,7 +58,10 @@ type jsonAccountTeam struct { MemberID string `json:"member_id,omitempty"` } -const accountKindAccount = "account" +const ( + accountJSONStatusFound = "found" + accountKindAccount = "account" +) // renderFullAccount prints the account details returned by GetCurrentAccount. func renderFullAccount(out io.Writer, fa *users.FullAccount) error { @@ -116,7 +119,7 @@ func account(cmd *cobra.Command, args []string) error { input := accountInput{} return out.Render(func(w io.Writer) error { return renderFullAccount(w, res) - }, newAccountOperationOutput(input, jsonFullAccount(res))) + }, withJSONCommand(cmd, newAccountOperationOutput(input, jsonFullAccount(res)))) } // Otherwise look up an account with the provided ID @@ -130,12 +133,12 @@ func account(cmd *cobra.Command, args []string) error { } return out.Render(func(w io.Writer) error { return renderBasicAccount(w, res) - }, newAccountOperationOutput(input, jsonBasicAccount(res))) + }, withJSONCommand(cmd, newAccountOperationOutput(input, jsonBasicAccount(res)))) } func newAccountOperationOutput(input accountInput, account jsonAccount) jsonOperationOutput { return newJSONOperationOutput(input, []jsonOperationResult{ - newJSONOperationResult("", accountKindAccount, input, account), + newJSONOperationResult(accountJSONStatusFound, accountKindAccount, input, account), }, nil) } diff --git a/cmd/add-member.go b/cmd/add-member.go index ab161ac..fdd6693 100644 --- a/cmd/add-member.go +++ b/cmd/add-member.go @@ -46,7 +46,7 @@ func addMember(cmd *cobra.Command, args []string) (err error) { } return commandOutput(cmd).Render(func(w io.Writer) error { return renderTeamMemberAdd(w, res) - }, teamMemberAddOperationOutput(input, res)) + }, withJSONCommand(cmd, teamMemberAddOperationOutput(input, res))) } func renderTeamMemberAdd(out io.Writer, res *team.MembersAddLaunch) error { diff --git a/cmd/cp.go b/cmd/cp.go index 19c9a3f..b8ef34e 100644 --- a/cmd/cp.go +++ b/cmd/cp.go @@ -71,7 +71,7 @@ func cp(cmd *cobra.Command, args []string) error { return fmt.Errorf("cp: %d error(s)", len(cpErrors)) } - return renderJSONOperationOutput(cmd, nil, relocationOperationResults(results)) + return renderJSONOperationOutput(cmd, nil, relocationOperationResults(relocationJSONStatusCopied, results)) } // cpCmd represents the cp command diff --git a/cmd/du.go b/cmd/du.go index f311a36..dcc5f11 100644 --- a/cmd/du.go +++ b/cmd/du.go @@ -39,7 +39,10 @@ type duAllocation struct { UserWithinTeamSpaceLimitType string `json:"user_within_team_space_limit_type,omitempty"` } -const duKindSpaceUsage = "space_usage" +const ( + duJSONStatusReported = "reported" + duKindSpaceUsage = "space_usage" +) func du(cmd *cobra.Command, args []string) (err error) { dbx := usersNewFunc(config) @@ -50,7 +53,7 @@ func du(cmd *cobra.Command, args []string) (err error) { return commandOutput(cmd).Render(func(w io.Writer) error { return renderUsage(w, usage) - }, newDuOperationOutput(usage)) + }, withJSONCommand(cmd, newDuOperationOutput(usage))) } func renderUsage(out io.Writer, usage *users.SpaceUsage) error { @@ -81,7 +84,7 @@ func newDuOutput(usage *users.SpaceUsage) duOutput { func newDuOperationOutput(usage *users.SpaceUsage) jsonOperationOutput { input := duInput{} return newJSONOperationOutput(input, []jsonOperationResult{ - newJSONOperationResult("", duKindSpaceUsage, input, newDuOutput(usage)), + newJSONOperationResult(duJSONStatusReported, duKindSpaceUsage, input, newDuOutput(usage)), }, nil) } diff --git a/cmd/info.go b/cmd/info.go index 11121e5..db34797 100644 --- a/cmd/info.go +++ b/cmd/info.go @@ -32,7 +32,7 @@ func info(cmd *cobra.Command, args []string) (err error) { return commandOutput(cmd).Render(func(w io.Writer) error { return renderTeamInfo(w, res) - }, teamInfoOperationOutput(res)) + }, withJSONCommand(cmd, teamInfoOperationOutput(res))) } func renderTeamInfo(out io.Writer, res *team.TeamGetInfoResult) error { diff --git a/cmd/json_contract_test.go b/cmd/json_contract_test.go index e8b84c3..07f1940 100644 --- a/cmd/json_contract_test.go +++ b/cmd/json_contract_test.go @@ -92,12 +92,14 @@ func TestStructuredOutputGoldenSchemaAudit(t *testing.T) { } assertGoldenCommandSchemaEqual(t, command, gotSchema, wantSchema) assertGoldenCommandSchemaReferences(t, command, gotSchema, contract.Definitions) + assertGoldenCommandStatuses(t, command, gotSchema) } for command, schema := range contract.Commands { if !structuredSet[command] { t.Errorf("golden schema includes non-structured command %q", command) } assertGoldenCommandSchemaReferences(t, command, schema, contract.Definitions) + assertGoldenCommandStatuses(t, command, schema) } for command := range want { if !structuredSet[command] { @@ -141,6 +143,58 @@ func TestStructuredOutputGoldenSuccessOutputAudit(t *testing.T) { } } +func TestPublicJSONSchemaFiles(t *testing.T) { + tests := []struct { + file string + ok bool + required []string + properties []string + }{ + { + file: "../docs/json-schema/v1/success.schema.json", + ok: true, + required: []string{"ok", "schema_version", "command", "input", "results", "warnings"}, + properties: []string{"ok", "schema_version", "command", "input", "results", "warnings"}, + }, + { + file: "../docs/json-schema/v1/error.schema.json", + ok: false, + required: []string{"ok", "schema_version", "command", "error", "warnings"}, + properties: []string{"ok", "schema_version", "command", "error", "warnings"}, + }, + } + + for _, tt := range tests { + t.Run(tt.file, func(t *testing.T) { + schema := loadPublicJSONSchema(t, tt.file) + if schema.Schema == "" { + t.Fatalf("%s has no $schema", tt.file) + } + if schema.ID == "" { + t.Fatalf("%s has no $id", tt.file) + } + if schema.Type != "object" { + t.Fatalf("%s type = %q, want object", tt.file, schema.Type) + } + if got, want := schema.Properties["ok"].Const, tt.ok; got != want { + t.Fatalf("%s ok const = %v, want %v", tt.file, got, want) + } + if got, want := schema.Properties["schema_version"].Const, jsonSchemaVersion; got != want { + t.Fatalf("%s schema_version const = %v, want %v", tt.file, got, want) + } + assertStringSliceEqual(t, tt.file+" required", schema.Required, tt.required) + assertStringSliceEqual(t, tt.file+" properties", mapKeys(schema.Properties), tt.properties) + if tt.ok { + assertStringSliceEqual(t, tt.file+" result required", schema.Defs["result"].Required, []string{"status", "kind", "input", "result"}) + } else { + errorSchema := schema.Properties["error"] + codeSchema := errorSchema.Properties["code"] + assertStringSliceEqual(t, tt.file+" error code enum", codeSchema.Enum, expectedJSONErrorCodes()) + } + }) + } +} + func structuredOutputCommandPathsWithVersion() []string { paths := structuredOutputCommandPaths(RootCmd) return append(paths, NewVersionCommand("test").Name()) @@ -198,6 +252,21 @@ type jsonGoldenCommandSchema struct { Warnings []string `json:"warnings"` } +type publicJSONSchema struct { + Schema string `json:"$schema"` + ID string `json:"$id"` + Type string `json:"type"` + Required []string `json:"required"` + Properties map[string]publicJSONSchemaProperty `json:"properties"` + Defs map[string]publicJSONSchema `json:"$defs"` +} + +type publicJSONSchemaProperty struct { + Const any `json:"const"` + Enum []string `json:"enum"` + Properties map[string]publicJSONSchemaProperty `json:"properties"` +} + func jsonSuccessFixtureCoverage() map[string]jsonSuccessFixture { return map[string]jsonSuccessFixture{ "account": { @@ -372,6 +441,41 @@ func loadJSONGoldenSuccessOutputs(t *testing.T) map[string]json.RawMessage { return fixtures } +func loadPublicJSONSchema(t *testing.T, file string) publicJSONSchema { + t.Helper() + + data, err := os.ReadFile(file) + if err != nil { + t.Fatalf("read public JSON schema %s: %v", file, err) + } + + var schema publicJSONSchema + if err := json.Unmarshal(data, &schema); err != nil { + t.Fatalf("decode public JSON schema %s: %v", file, err) + } + return schema +} + +func expectedJSONErrorCodes() []string { + return []string{ + jsonErrorCodeAppKeyRequired, + jsonErrorCodeAuthExchangeFailed, + jsonErrorCodeAuthRefreshFailed, + jsonErrorCodeAuthRequired, + jsonErrorCodeCommandFailed, + jsonErrorCodeDropboxAPIError, + jsonErrorCodeInvalidArguments, + jsonErrorCodeNotFound, + jsonErrorCodePathConflict, + jsonErrorCodePermissionDenied, + jsonErrorCodeRateLimited, + jsonErrorCodeStructuredOutputUnsupported, + jsonErrorCodeUnknownCommand, + jsonErrorCodeUnknownFlag, + jsonErrorCodeUnsupportedOutputFormat, + } +} + func assertGoldenJSONEqual(t *testing.T, command string, fixture json.RawMessage, actual any) { t.Helper() @@ -404,15 +508,15 @@ func jsonGoldenSuccessOutputExamples() map[string]jsonOperationOutput { sharedLink := sampleShareLinkJSONMetadata() teamMember := sampleTeamMemberJSON() - return map[string]jsonOperationOutput{ + examples := map[string]jsonOperationOutput{ "account": newJSONOperationOutput(accountInput{AccountID: "dbid:lookup"}, []jsonOperationResult{ - newJSONOperationResult("", accountKindAccount, accountInput{AccountID: "dbid:lookup"}, sampleJSONAccount()), + newJSONOperationResult(accountJSONStatusFound, accountKindAccount, accountInput{AccountID: "dbid:lookup"}, sampleJSONAccount()), }, nil), "cp": newJSONOperationOutput(nil, []jsonOperationResult{ - newJSONOperationResult("", "", relocationInput{FromPath: "/Reports/old.pdf", ToPath: "/Reports/copy.pdf"}, copyFile), + newJSONOperationResult(relocationJSONStatusCopied, copyFile.Type, relocationInput{FromPath: "/Reports/old.pdf", ToPath: "/Reports/copy.pdf"}, copyFile), }, nil), "du": newJSONOperationOutput(duInput{}, []jsonOperationResult{ - newJSONOperationResult("", duKindSpaceUsage, duInput{}, duOutput{ + newJSONOperationResult(duJSONStatusReported, duKindSpaceUsage, duInput{}, duOutput{ Used: 2048, Allocation: duAllocation{ Type: "team", @@ -434,7 +538,7 @@ func jsonGoldenSuccessOutputExamples() map[string]jsonOperationOutput { newJSONOperationResult(mkdirStatusCreated, mkdirKindFolder, mkdirInput{Path: "/Reports/new", Parents: true}, sampleJSONFolderMetadata("/Reports/new")), }, nil), "mv": newJSONOperationOutput(nil, []jsonOperationResult{ - newJSONOperationResult("", "", relocationInput{FromPath: "/Reports/copy.pdf", ToPath: "/Reports/moved.pdf"}, sampleJSONFileMetadata("/Reports/moved.pdf")), + newJSONOperationResult(relocationJSONStatusMoved, "file", relocationInput{FromPath: "/Reports/copy.pdf", ToPath: "/Reports/moved.pdf"}, sampleJSONFileMetadata("/Reports/moved.pdf")), }, nil), "put": newJSONOperationOutput(putCommandInput{Source: "README.md", Target: "/README.md", Recursive: true, IfExists: putIfExistsOverwrite, Stdin: false}, []jsonOperationResult{ newJSONOperationResult(putStatusUploaded, putKindFile, putResultInput{Source: "README.md", Target: "/README.md"}, sampleJSONFileMetadata("/README.md")), @@ -446,7 +550,7 @@ func jsonGoldenSuccessOutputExamples() map[string]jsonOperationOutput { newJSONOperationResult(revsJSONStatusRevision, file.Type, nil, file), }, nil), "rm": newJSONOperationOutput(nil, []jsonOperationResult{ - newJSONOperationResult("", "", removeInput{Path: "/Reports/old.pdf", Permanent: false, Recursive: false, Force: false}, file), + newJSONOperationResult(removeJSONStatusDeleted, file.Type, removeInput{Path: "/Reports/old.pdf", Permanent: false, Recursive: false, Force: false}, file), }, nil), "search": newJSONOperationOutput(searchInput{Query: "report", Path: "/Reports", Long: true, Sort: "name", Reverse: false, Time: "server", TimeFormat: "2006-01-02"}, []jsonOperationResult{ newJSONOperationResult(searchJSONStatusFound, folder.Type, nil, folder), @@ -499,9 +603,16 @@ func jsonGoldenSuccessOutputExamples() map[string]jsonOperationOutput { newJSONOperationResult(teamJSONStatusRemoved, teamJSONKindTeamMember, teamMemberRemoveInput{Email: "ada@example.com"}, teamMemberMutationJSON{Type: teamJSONTypeMemberRemove, Tag: "complete", AsyncJobID: "async-job-id"}), }, nil), "version": newJSONOperationOutput(versionInput{}, []jsonOperationResult{ - newJSONOperationResult("", versionKindVersion, versionInput{}, versionOutput{Version: "1.2.3", SDKVersion: "sdk-version", SpecVersion: "spec-version"}), + newJSONOperationResult(versionJSONStatusReported, versionKindVersion, versionInput{}, versionOutput{Version: "1.2.3", SDKVersion: "sdk-version", SpecVersion: "spec-version"}), }, nil), } + for command, example := range examples { + example.OK = true + example.SchemaVersion = jsonSchemaVersion + example.Command = command + examples[command] = example + } + return examples } func sampleJSONAccount() jsonAccount { @@ -669,39 +780,39 @@ func jsonContractDefinitions() map[string][]string { func jsonCommandSchemas() map[string]jsonGoldenCommandSchema { return map[string]jsonGoldenCommandSchema{ - "account": operationSchema("account_input", schemaRef("account_input"), "account", nil, []string{accountKindAccount}, nil), - "cp": operationSchema("empty", schemaRef("relocation_input"), "metadata", nil, nil, nil), - "du": operationSchema("empty", schemaRef("empty"), "du_output", nil, []string{duKindSpaceUsage}, nil), + "account": operationSchema("account_input", schemaRef("account_input"), "account", []string{accountJSONStatusFound}, []string{accountKindAccount}, nil), + "cp": operationSchema("empty", schemaRef("relocation_input"), "metadata", []string{relocationJSONStatusCopied}, metadataKinds(), nil), + "du": operationSchema("empty", schemaRef("empty"), "du_output", []string{duJSONStatusReported}, []string{duKindSpaceUsage}, nil), "get": operationSchema("get_input", schemaRef("get_result_input"), "metadata", []string{getStatusCreated, getStatusDownloaded, getStatusExisting}, []string{getKindFile, getKindFolder}, nil), - "ls": operationSchema("ls_input", nil, "metadata", []string{lsJSONStatusListed}, metadataKinds(), nil), + "ls": operationSchema("ls_input", schemaRef("empty"), "metadata", []string{lsJSONStatusListed}, metadataKinds(), nil), "mkdir": operationSchema("mkdir_input", schemaRef("mkdir_input"), "metadata", []string{mkdirStatusCreated, mkdirStatusExisting}, []string{mkdirKindFolder}, nil), - "mv": operationSchema("empty", schemaRef("relocation_input"), "metadata", nil, nil, nil), + "mv": operationSchema("empty", schemaRef("relocation_input"), "metadata", []string{relocationJSONStatusMoved}, metadataKinds(), nil), "put": operationSchema("put_input", schemaRef("put_result_input"), "metadata", []string{putStatusCreated, putStatusExisting, putStatusSkipped, putStatusUploaded}, []string{putKindFile, putKindFolder}, []string{jsonWarningCodeSkippedSymlink}), "restore": operationSchema("restore_input", schemaRef("restore_input"), "metadata", []string{restoreStatusRestored}, []string{restoreKindFile}, nil), - "revs": operationSchema("revs_input", nil, "metadata", []string{revsJSONStatusRevision}, []string{"file", "unknown"}, nil), - "rm": operationSchema("empty", schemaRef("remove_input"), "metadata", nil, nil, nil), - "search": operationSchema("search_input", nil, "metadata", []string{searchJSONStatusFound}, metadataKinds(), nil), - "share list folder": operationSchema("empty", nil, "share_folder", []string{shareFolderJSONStatusListed}, []string{shareFolderJSONKindFolder}, nil), - "share list link": operationSchema("share_link_list_input", nil, "share_link_metadata", []string{shareLinkJSONStatusListed}, shareLinkKinds(), []string{jsonWarningCodeDeprecatedCommand}), - "share-link create": operationSchema("share_link_create_input", nil, "share_link_metadata", []string{shareLinkJSONStatusCreated, shareLinkJSONStatusExisting}, shareLinkKinds(), nil), + "revs": operationSchema("revs_input", schemaRef("empty"), "metadata", []string{revsJSONStatusRevision}, []string{"file", "unknown"}, nil), + "rm": operationSchema("empty", schemaRef("remove_input"), "metadata", []string{removeJSONStatusDeleted, removeJSONStatusPermanentlyDeleted}, metadataKinds(), nil), + "search": operationSchema("search_input", schemaRef("empty"), "metadata", []string{searchJSONStatusFound}, metadataKinds(), nil), + "share list folder": operationSchema("empty", schemaRef("empty"), "share_folder", []string{shareFolderJSONStatusListed}, []string{shareFolderJSONKindFolder}, nil), + "share list link": operationSchema("share_link_list_input", schemaRef("empty"), "share_link_metadata", []string{shareLinkJSONStatusListed}, shareLinkKinds(), []string{jsonWarningCodeDeprecatedCommand}), + "share-link create": operationSchema("share_link_create_input", schemaRef("empty"), "share_link_metadata", []string{shareLinkJSONStatusCreated, shareLinkJSONStatusExisting}, shareLinkKinds(), nil), "share-link download": operationSchema( "share_link_download_input", - nil, + schemaRef("empty"), "share_link_download_result", []string{shareLinkJSONStatusDownloaded}, shareLinkKinds(), nil, ), - "share-link info": operationSchema("share_link_info_input", nil, "share_link_metadata", []string{shareLinkJSONStatusFound}, shareLinkKinds(), nil), - "share-link list": operationSchema("share_link_list_input", nil, "share_link_metadata", []string{shareLinkJSONStatusListed}, shareLinkKinds(), nil), - "share-link revoke": operationSchema("share_link_revoke_input", nil, "share_link_revoke_result", []string{shareLinkJSONStatusRevoked}, append(shareLinkKinds(), shareLinkJSONKindSharedLink), nil), - "share-link update": operationSchema("share_link_update_input", nil, "share_link_metadata", []string{shareLinkJSONStatusUpdated}, shareLinkKinds(), nil), + "share-link info": operationSchema("share_link_info_input", schemaRef("empty"), "share_link_metadata", []string{shareLinkJSONStatusFound}, shareLinkKinds(), nil), + "share-link list": operationSchema("share_link_list_input", schemaRef("empty"), "share_link_metadata", []string{shareLinkJSONStatusListed}, shareLinkKinds(), nil), + "share-link revoke": operationSchema("share_link_revoke_input", schemaRef("empty"), "share_link_revoke_result", []string{shareLinkJSONStatusRevoked}, append(shareLinkKinds(), shareLinkJSONKindSharedLink), nil), + "share-link update": operationSchema("share_link_update_input", schemaRef("empty"), "share_link_metadata", []string{shareLinkJSONStatusUpdated}, shareLinkKinds(), nil), "team add-member": operationSchema("team_member_add_input", schemaRef("team_member_add_input"), "team_member_mutation", []string{teamJSONStatusAdded, teamJSONStatusCompleted, teamJSONStatusStarted}, []string{teamJSONKindTeamMember}, nil), "team info": operationSchema("empty", schemaRef("empty"), "team_info", []string{teamJSONStatusFound}, []string{teamJSONKindTeam}, nil), - "team list-groups": operationSchema("empty", nil, "team_group", []string{teamJSONStatusListed}, []string{teamJSONKindTeamGroup}, nil), - "team list-members": operationSchema("empty", nil, "team_member", []string{teamJSONStatusListed}, []string{teamJSONKindTeamMember}, nil), + "team list-groups": operationSchema("empty", schemaRef("empty"), "team_group", []string{teamJSONStatusListed}, []string{teamJSONKindTeamGroup}, nil), + "team list-members": operationSchema("empty", schemaRef("empty"), "team_member", []string{teamJSONStatusListed}, []string{teamJSONKindTeamMember}, nil), "team remove-member": operationSchema("team_member_remove_input", schemaRef("team_member_remove_input"), "team_member_mutation", []string{teamJSONStatusCompleted, teamJSONStatusRemoved, teamJSONStatusStarted}, []string{teamJSONKindTeamMember}, nil), - "version": operationSchema("empty", schemaRef("empty"), "version", nil, []string{versionKindVersion}, nil), + "version": operationSchema("empty", schemaRef("empty"), "version", []string{versionJSONStatusReported}, []string{versionKindVersion}, nil), } } @@ -772,6 +883,17 @@ func assertStringSliceMapEqual(t *testing.T, label string, got, want map[string] t.Fatalf("%s = %s, want %s", label, gotJSON, wantJSON) } +func assertStringSliceEqual(t *testing.T, label string, got, want []string) { + t.Helper() + + got = sortedCopy(got) + want = sortedCopy(want) + if reflect.DeepEqual(got, want) { + return + } + t.Fatalf("%s = %v, want %v", label, got, want) +} + func assertGoldenCommandSchemaEqual(t *testing.T, command string, got, want jsonGoldenCommandSchema) { t.Helper() @@ -800,6 +922,19 @@ func assertGoldenCommandSchemaReferences(t *testing.T, command string, schema js } } +func assertGoldenCommandStatuses(t *testing.T, command string, schema jsonGoldenCommandSchema) { + t.Helper() + + if len(schema.Statuses) == 0 { + t.Errorf("golden schema for %q has no result statuses", command) + } + for _, status := range schema.Statuses { + if status == "unknown" { + t.Errorf("golden schema for %q must not allow unknown result status", command) + } + } +} + func normalizeGoldenContract(contract jsonGoldenContract) jsonGoldenContract { contract.Definitions = normalizeStringSliceMap(contract.Definitions) commands := make(map[string]jsonGoldenCommandSchema, len(contract.Commands)) @@ -834,6 +969,14 @@ func sortedCopy(values []string) []string { return copied } +func mapKeys[V any](values map[string]V) []string { + keys := make([]string, 0, len(values)) + for key := range values { + keys = append(keys, key) + } + return keys +} + func structuredOutputCommandPaths(root *cobra.Command) []string { var paths []string var walk func(*cobra.Command, []string) @@ -863,13 +1006,13 @@ func TestJSONOperationOutputContractShape(t *testing.T) { if err := json.Unmarshal(encoded, &raw); err != nil { t.Fatalf("decode operation output: %v", err) } - for _, key := range []string{"input", "results", "warnings"} { + for _, key := range []string{"ok", "schema_version", "command", "input", "results", "warnings"} { if _, ok := raw[key]; !ok { t.Fatalf("operation output = %s, missing %q", encoded, key) } } - if len(raw) != 3 { - t.Fatalf("operation output = %s, want only input/results/warnings", encoded) + if len(raw) != 6 { + t.Fatalf("operation output = %s, want only ok/schema_version/command/input/results/warnings", encoded) } } diff --git a/cmd/json_output.go b/cmd/json_output.go index 85486ec..147a1ad 100644 --- a/cmd/json_output.go +++ b/cmd/json_output.go @@ -1,11 +1,17 @@ package cmd -import "github.com/spf13/cobra" +import ( + "strings" + + "github.com/spf13/cobra" +) type jsonErrorResponse struct { - OK bool `json:"ok"` - Error jsonError `json:"error"` - Warnings []jsonWarning `json:"warnings"` + OK bool `json:"ok"` + SchemaVersion string `json:"schema_version"` + Command string `json:"command"` + Error jsonError `json:"error"` + Warnings []jsonWarning `json:"warnings"` } type jsonError struct { @@ -25,21 +31,28 @@ const ( ) type jsonOperationOutput struct { - Input any `json:"input"` - Results []jsonOperationResult `json:"results"` - Warnings []jsonWarning `json:"warnings"` + OK bool `json:"ok"` + SchemaVersion string `json:"schema_version"` + Command string `json:"command"` + Input any `json:"input"` + Results []jsonOperationResult `json:"results"` + Warnings []jsonWarning `json:"warnings"` } type jsonOperationResult struct { - Status string `json:"status,omitempty"` - Kind string `json:"kind,omitempty"` - Input any `json:"input,omitempty"` - Result any `json:"result,omitempty"` + Status string `json:"status"` + Kind string `json:"kind"` + Input any `json:"input"` + Result any `json:"result"` } -func newJSONErrorResponse(err error) jsonErrorResponse { +const jsonSchemaVersion = "1" + +func newJSONErrorResponse(cmd *cobra.Command, err error) jsonErrorResponse { return jsonErrorResponse{ - OK: false, + OK: false, + SchemaVersion: jsonSchemaVersion, + Command: jsonCommandPath(cmd), Error: jsonError{ Message: err.Error(), Code: jsonErrorCode(err), @@ -50,27 +63,40 @@ func newJSONErrorResponse(err error) jsonErrorResponse { func newJSONOperationOutput(input any, results []jsonOperationResult, warnings []jsonWarning) jsonOperationOutput { return jsonOperationOutput{ - Input: normalizeJSONInput(input), - Results: normalizeJSONOperationResults(results), - Warnings: normalizeJSONWarnings(warnings), + OK: true, + SchemaVersion: jsonSchemaVersion, + Input: normalizeJSONInput(input), + Results: normalizeJSONOperationResults(results), + Warnings: normalizeJSONWarnings(warnings), } } +func newJSONCommandOperationOutput(cmd *cobra.Command, input any, results []jsonOperationResult, warnings []jsonWarning) jsonOperationOutput { + return withJSONCommand(cmd, newJSONOperationOutput(input, results, warnings)) +} + +func withJSONCommand(cmd *cobra.Command, out jsonOperationOutput) jsonOperationOutput { + out.OK = true + out.SchemaVersion = jsonSchemaVersion + out.Command = jsonCommandPath(cmd) + return out +} + func renderJSONOperationOutput(cmd *cobra.Command, input any, results []jsonOperationResult) error { return renderJSONOperationOutputWithWarnings(cmd, input, results, nil) } func renderJSONOperationOutputWithWarnings(cmd *cobra.Command, input any, results []jsonOperationResult, warnings []jsonWarning) error { - return commandOutput(cmd).Render(nil, newJSONOperationOutput(input, results, warnings)) + return commandOutput(cmd).Render(nil, newJSONCommandOperationOutput(cmd, input, results, warnings)) } func newJSONOperationResult(status, kind string, input any, result any) jsonOperationResult { - return jsonOperationResult{ + return normalizeJSONOperationResult(jsonOperationResult{ Status: status, Kind: kind, Input: input, Result: result, - } + }) } func newJSONMetadataOperationResults(status string, entries []jsonMetadata) []jsonOperationResult { @@ -82,19 +108,38 @@ func newJSONMetadataOperationResults(status string, entries []jsonMetadata) []js } func normalizeJSONInput(input any) any { - if input == nil { - return struct{}{} - } - return input + return normalizeJSONObject(input) } func normalizeJSONOperationResults(results []jsonOperationResult) []jsonOperationResult { if results == nil { return []jsonOperationResult{} } + for i := range results { + results[i] = normalizeJSONOperationResult(results[i]) + } return results } +func normalizeJSONOperationResult(result jsonOperationResult) jsonOperationResult { + if result.Status == "" { + result.Status = "unknown" + } + if result.Kind == "" { + result.Kind = "unknown" + } + result.Input = normalizeJSONObject(result.Input) + result.Result = normalizeJSONObject(result.Result) + return result +} + +func normalizeJSONObject(value any) any { + if value == nil { + return struct{}{} + } + return value +} + func emptyJSONWarnings() []jsonWarning { return []jsonWarning{} } @@ -105,3 +150,20 @@ func normalizeJSONWarnings(warnings []jsonWarning) []jsonWarning { } return warnings } + +func jsonCommandPath(cmd *cobra.Command) string { + if cmd == nil { + return "" + } + if cmd.Parent() == nil { + return cmd.Name() + } + + var parts []string + for c := cmd; c != nil && c.Parent() != nil; c = c.Parent() { + if name := c.Name(); name != "" { + parts = append([]string{name}, parts...) + } + } + return strings.Join(parts, " ") +} diff --git a/cmd/list-groups.go b/cmd/list-groups.go index 620de4b..31ce0a6 100644 --- a/cmd/list-groups.go +++ b/cmd/list-groups.go @@ -37,7 +37,7 @@ func listGroups(cmd *cobra.Command, args []string) (err error) { return commandOutput(cmd).Render(func(w io.Writer) error { return renderTeamGroups(w, groups) - }, newJSONOperationOutput(teamInfoInput{}, teamGroupOperationResults(groups), nil)) + }, newJSONCommandOperationOutput(cmd, teamInfoInput{}, teamGroupOperationResults(groups), nil)) } func listTeamGroups(dbx teamClient, arg *team.GroupsListArg) ([]*team_common.GroupSummary, error) { diff --git a/cmd/list-members.go b/cmd/list-members.go index 1804f54..699ef75 100644 --- a/cmd/list-members.go +++ b/cmd/list-members.go @@ -36,7 +36,7 @@ func listMembers(cmd *cobra.Command, args []string) (err error) { return commandOutput(cmd).Render(func(w io.Writer) error { return renderTeamMembers(w, members) - }, newJSONOperationOutput(teamInfoInput{}, teamMemberOperationResults(members), nil)) + }, newJSONCommandOperationOutput(cmd, teamInfoInput{}, teamMemberOperationResults(members), nil)) } func listTeamMembers(dbx teamClient, arg *team.MembersListArg) ([]*team.TeamMemberInfo, error) { diff --git a/cmd/mv.go b/cmd/mv.go index 74bbb9a..f3a481c 100644 --- a/cmd/mv.go +++ b/cmd/mv.go @@ -70,7 +70,7 @@ func mv(cmd *cobra.Command, args []string) error { return fmt.Errorf("mv: %d error(s)", len(mvErrors)) } - return renderJSONOperationOutput(cmd, nil, relocationOperationResults(results)) + return renderJSONOperationOutput(cmd, nil, relocationOperationResults(relocationJSONStatusMoved, results)) } // mvCmd represents the mv command diff --git a/cmd/output.go b/cmd/output.go index 4e4b9f4..2fd230b 100644 --- a/cmd/output.go +++ b/cmd/output.go @@ -213,7 +213,7 @@ func renderCommandErrorWithJSON(cmd *cobra.Command, err error, forceJSON bool) { } if forceJSON || commandOutputFormat(cmd) == output.FormatJSON { - renderErr := output.New(cmd.OutOrStdout(), cmd.ErrOrStderr(), output.FormatJSON).Render(nil, newJSONErrorResponse(err)) + renderErr := output.New(cmd.OutOrStdout(), cmd.ErrOrStderr(), output.FormatJSON).Render(nil, newJSONErrorResponse(cmd, err)) if renderErr == nil { return } diff --git a/cmd/output_test.go b/cmd/output_test.go index 4303d30..86b6e9c 100644 --- a/cmd/output_test.go +++ b/cmd/output_test.go @@ -278,7 +278,7 @@ func TestRenderCommandErrorTextUnknownCommandIncludesUsageHint(t *testing.T) { func TestRenderCommandErrorWritesJSONErrorToStdout(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer - cmd := &cobra.Command{} + cmd := &cobra.Command{Use: "rm"} cmd.SetOut(&stdout) cmd.SetErr(&stderr) cmd.Flags().String(outputFlag, "json", "") @@ -293,6 +293,12 @@ func TestRenderCommandErrorWritesJSONErrorToStdout(t *testing.T) { if got.OK { t.Fatalf("ok = true, want false") } + if got.SchemaVersion != jsonSchemaVersion { + t.Fatalf("schema_version = %q, want %q", got.SchemaVersion, jsonSchemaVersion) + } + if got.Command != "rm" { + t.Fatalf("command = %q, want rm", got.Command) + } if got.Error.Message != "failed" { t.Fatalf("message = %q, want failed", got.Error.Message) } @@ -406,7 +412,9 @@ func TestNewJSONOperationOutputNormalizesResults(t *testing.T) { func TestRenderJSONOperationOutput(t *testing.T) { var stdout bytes.Buffer - cmd := &cobra.Command{} + root := &cobra.Command{Use: "dbxcli"} + cmd := &cobra.Command{Use: "get"} + root.AddCommand(cmd) cmd.SetOut(&stdout) cmd.Flags().String(outputFlag, "json", "") @@ -434,6 +442,9 @@ func TestRenderJSONOperationOutput(t *testing.T) { rendered := stdout.String() for _, want := range []string{ + `"ok":true`, + `"schema_version":"1"`, + `"command":"get"`, `"input":{"path":"/file.txt"}`, `"results":[{"status":"downloaded","kind":"file"`, `"warnings":[]`, @@ -444,7 +455,7 @@ func TestRenderJSONOperationOutput(t *testing.T) { } } -func TestNewJSONOperationResultOmitsEmptyFields(t *testing.T) { +func TestNewJSONOperationResultNormalizesEmptyFields(t *testing.T) { got := newJSONOperationResult( "", "", @@ -459,14 +470,16 @@ func TestNewJSONOperationResultOmitsEmptyFields(t *testing.T) { t.Fatalf("marshal operation result: %v", err) } rendered := string(encoded) - for _, unwanted := range []string{`"status"`, `"kind"`, `"result"`} { - if strings.Contains(rendered, unwanted) { - t.Fatalf("encoded output = %s, did not expect %s", rendered, unwanted) + for _, want := range []string{ + `"status":"unknown"`, + `"kind":"unknown"`, + `"input":{"path":"/file.txt"}`, + `"result":{}`, + } { + if !strings.Contains(rendered, want) { + t.Fatalf("encoded output = %s, want %s", rendered, want) } } - if !strings.Contains(rendered, `"input":{"path":"/file.txt"}`) { - t.Fatalf("encoded output = %s, want input", rendered) - } } func TestNewJSONMetadataOperationResults(t *testing.T) { @@ -486,8 +499,11 @@ func TestNewJSONMetadataOperationResults(t *testing.T) { if len(results) != 2 { t.Fatalf("results = %d, want 2", len(results)) } - if results[0].Status != "listed" || results[0].Kind != "file" || results[0].Input != nil { - t.Fatalf("first result = %#v, want listed file with no per-result input", results[0]) + if results[0].Status != "listed" || results[0].Kind != "file" { + t.Fatalf("first result = %#v, want listed file", results[0]) + } + if _, ok := results[0].Input.(struct{}); !ok { + t.Fatalf("first result input = %#v, want empty object", results[0].Input) } first, ok := results[0].Result.(jsonMetadata) if !ok { diff --git a/cmd/relocation_output.go b/cmd/relocation_output.go index 8d9b08e..86b2dcb 100644 --- a/cmd/relocation_output.go +++ b/cmd/relocation_output.go @@ -2,6 +2,11 @@ package cmd import "github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/files" +const ( + relocationJSONStatusCopied = "copied" + relocationJSONStatusMoved = "moved" +) + type relocationInput struct { FromPath string `json:"from_path"` ToPath string `json:"to_path"` @@ -27,10 +32,10 @@ func newRelocationResult(arg *files.RelocationArg, res *files.RelocationResult) } } -func relocationOperationResults(results []relocationResult) []jsonOperationResult { +func relocationOperationResults(status string, results []relocationResult) []jsonOperationResult { operationResults := make([]jsonOperationResult, 0, len(results)) for _, result := range results { - operationResults = append(operationResults, newJSONOperationResult("", "", result.Input, result.Result)) + operationResults = append(operationResults, newJSONOperationResult(status, result.Result.Type, result.Input, result.Result)) } return operationResults } diff --git a/cmd/remove-member.go b/cmd/remove-member.go index d1c384d..98c2db7 100644 --- a/cmd/remove-member.go +++ b/cmd/remove-member.go @@ -40,7 +40,7 @@ func removeMember(cmd *cobra.Command, args []string) (err error) { input := teamMemberRemoveInput{Email: email} return commandOutput(cmd).Render(func(w io.Writer) error { return renderTeamMemberRemove(w, res) - }, teamMemberRemoveOperationOutput(input, res)) + }, withJSONCommand(cmd, teamMemberRemoveOperationOutput(input, res))) } func renderTeamMemberRemove(out io.Writer, res *async.LaunchEmptyResult) error { diff --git a/cmd/restore.go b/cmd/restore.go index 1100f2d..3d3146a 100644 --- a/cmd/restore.go +++ b/cmd/restore.go @@ -68,7 +68,7 @@ func restore(cmd *cobra.Command, args []string) (err error) { return nil } return renderRestoreResult(w, result) - }, newJSONOperationOutput(result.Input, []jsonOperationResult{restoreOperationResult(result)}, nil)) + }, newJSONCommandOperationOutput(cmd, result.Input, []jsonOperationResult{restoreOperationResult(result)}, nil)) } func newRestoreResult(path, revision string, metadata *files.FileMetadata) restoreResult { diff --git a/cmd/rm.go b/cmd/rm.go index 741f49f..f666991 100644 --- a/cmd/rm.go +++ b/cmd/rm.go @@ -47,6 +47,11 @@ type removeResult struct { Result jsonMetadata `json:"result"` } +const ( + removeJSONStatusDeleted = "deleted" + removeJSONStatusPermanentlyDeleted = "permanently_deleted" +) + func rm(cmd *cobra.Command, args []string) error { if len(args) < 1 { return invalidArgumentsError("rm: missing operand") @@ -74,17 +79,24 @@ func rm(cmd *cobra.Command, args []string) error { return nil } return renderRemoveResults(w, results) - }, newJSONOperationOutput(nil, removeOperationResults(results), nil)) + }, newJSONCommandOperationOutput(cmd, nil, removeOperationResults(results), nil)) } func removeOperationResults(results []removeResult) []jsonOperationResult { operationResults := make([]jsonOperationResult, 0, len(results)) for _, result := range results { - operationResults = append(operationResults, newJSONOperationResult("", "", result.Input, result.Result)) + operationResults = append(operationResults, newJSONOperationResult(removeJSONStatus(result), result.Result.Type, result.Input, result.Result)) } return operationResults } +func removeJSONStatus(result removeResult) string { + if result.Input.Permanent { + return removeJSONStatusPermanentlyDeleted + } + return removeJSONStatusDeleted +} + func parseRemoveOptions(cmd *cobra.Command) (removeOptions, error) { force, err := cmd.Flags().GetBool("force") if err != nil { diff --git a/cmd/share-list-folders.go b/cmd/share-list-folders.go index 4b6bb00..99d0e08 100644 --- a/cmd/share-list-folders.go +++ b/cmd/share-list-folders.go @@ -68,7 +68,8 @@ func shareListFolders(cmd *cobra.Command, args []string) (err error) { return commandOutput(cmd).Render(func(w io.Writer) error { return renderSharedFolders(w, entries) - }, newJSONOperationOutput( + }, newJSONCommandOperationOutput( + cmd, shareFolderListInput{}, shareFolderJSONOperationResults(shareFolderJSONMetadataListFromDropbox(entries)), nil, diff --git a/cmd/share-list-links.go b/cmd/share-list-links.go index 95623bc..063e53d 100644 --- a/cmd/share-list-links.go +++ b/cmd/share-list-links.go @@ -73,7 +73,8 @@ func shareLinkListWithWarnings(cmd *cobra.Command, args []string, warnings []jso return commandOutput(cmd).Render(func(w io.Writer) error { return renderSharedLinks(w, links) - }, newJSONOperationOutput( + }, newJSONCommandOperationOutput( + cmd, shareLinkListInput{ Path: arg.Path, DirectOnly: arg.DirectOnly, diff --git a/cmd/share_link_create.go b/cmd/share_link_create.go index 6322ac5..938aa9d 100644 --- a/cmd/share_link_create.go +++ b/cmd/share_link_create.go @@ -105,7 +105,8 @@ func shareLinkCreate(cmd *cobra.Command, args []string) error { return out.Render(func(w io.Writer) error { _, err := fmt.Fprintln(w, url) return err - }, newJSONOperationOutput( + }, newJSONCommandOperationOutput( + cmd, newShareLinkCreateInput(path, opts), []jsonOperationResult{shareLinkJSONOperationResult(status, result)}, nil, diff --git a/cmd/share_link_info.go b/cmd/share_link_info.go index e452469..453cef8 100644 --- a/cmd/share_link_info.go +++ b/cmd/share_link_info.go @@ -73,7 +73,8 @@ func shareLinkInfo(cmd *cobra.Command, args []string) error { return commandOutput(cmd).Render(func(w io.Writer) error { return renderSharedLinkInfo(w, link) - }, newJSONOperationOutput( + }, newJSONCommandOperationOutput( + cmd, shareLinkInfoInput{ URL: url, Path: opts.path, diff --git a/cmd/testdata/json_contract/success_outputs.json b/cmd/testdata/json_contract/success_outputs.json index ec87a4e..38e8c55 100644 --- a/cmd/testdata/json_contract/success_outputs.json +++ b/cmd/testdata/json_contract/success_outputs.json @@ -1,28 +1,28 @@ { - "account": {"input":{"account_id":"dbid:lookup"},"results":[{"kind":"account","input":{"account_id":"dbid:lookup"},"result":{"type":"full","account_id":"dbid:account","name":{"given_name":"Ada","surname":"Lovelace","familiar_name":"Ada","display_name":"Ada Lovelace","abbreviated_name":"AL"},"email":"ada@example.com","email_verified":true,"disabled":false,"profile_photo_url":"https://example.com/profile.jpg","locale":"en","referral_link":"https://example.com/referral","is_paired":false,"account_type":"basic","is_teammate":true,"team_member_id":"dbmid:team-member","team":{"id":"team-id","name":"Engineering","member_id":"dbmid:team-member"}}}],"warnings":[]}, - "cp": {"input":{},"results":[{"input":{"from_path":"/Reports/old.pdf","to_path":"/Reports/copy.pdf"},"result":{"type":"file","path_display":"/Reports/copy.pdf","path_lower":"/reports/copy.pdf","id":"id:file","rev":"015f","size":123,"server_modified":"2026-06-25T12:00:00Z","client_modified":"2026-06-25T11:00:00Z"}}],"warnings":[]}, - "du": {"input":{},"results":[{"kind":"space_usage","input":{},"result":{"used":2048,"allocation":{"type":"team","allocated":1000000,"used":2048,"user_within_team_space_allocated":500000,"user_within_team_space_used_cached":1024,"user_within_team_space_limit_type":"fixed"}}}],"warnings":[]}, - "get": {"input":{"source":"/Reports/old.pdf","target":"old.pdf","recursive":false,"stdout":false},"results":[{"status":"downloaded","kind":"file","input":{"source":"/Reports/old.pdf","target":"old.pdf"},"result":{"type":"file","path_display":"/Reports/old.pdf","path_lower":"/reports/old.pdf","id":"id:file","rev":"015f","size":123,"server_modified":"2026-06-25T12:00:00Z","client_modified":"2026-06-25T11:00:00Z"}}],"warnings":[]}, - "ls": {"input":{"path":"/Reports","recursive":false,"include_deleted":true,"only_deleted":false,"long":true,"sort":"name","reverse":false,"time":"server","time_format":"2006-01-02"},"results":[{"status":"listed","kind":"file","result":{"type":"file","path_display":"/Reports/old.pdf","path_lower":"/reports/old.pdf","id":"id:file","rev":"015f","size":123,"server_modified":"2026-06-25T12:00:00Z","client_modified":"2026-06-25T11:00:00Z"}}],"warnings":[]}, - "mkdir": {"input":{"path":"/Reports/new","parents":true},"results":[{"status":"created","kind":"folder","input":{"path":"/Reports/new","parents":true},"result":{"type":"folder","path_display":"/Reports/new","path_lower":"/reports/new","id":"id:folder"}}],"warnings":[]}, - "mv": {"input":{},"results":[{"input":{"from_path":"/Reports/copy.pdf","to_path":"/Reports/moved.pdf"},"result":{"type":"file","path_display":"/Reports/moved.pdf","path_lower":"/reports/moved.pdf","id":"id:file","rev":"015f","size":123,"server_modified":"2026-06-25T12:00:00Z","client_modified":"2026-06-25T11:00:00Z"}}],"warnings":[]}, - "put": {"input":{"source":"README.md","target":"/README.md","recursive":true,"if_exists":"overwrite","stdin":false},"results":[{"status":"uploaded","kind":"file","input":{"source":"README.md","target":"/README.md"},"result":{"type":"file","path_display":"/README.md","path_lower":"/readme.md","id":"id:file","rev":"015f","size":123,"server_modified":"2026-06-25T12:00:00Z","client_modified":"2026-06-25T11:00:00Z"}}],"warnings":[{"code":"skipped_symlink","message":"skipped symlink","path":"docs/link"}]}, - "restore": {"input":{"path":"/Reports/old.pdf","revision":"015f"},"results":[{"status":"restored","kind":"file","input":{"path":"/Reports/old.pdf","revision":"015f"},"result":{"type":"file","path_display":"/Reports/old.pdf","path_lower":"/reports/old.pdf","id":"id:file","rev":"015f","size":123,"server_modified":"2026-06-25T12:00:00Z","client_modified":"2026-06-25T11:00:00Z"}}],"warnings":[]}, - "revs": {"input":{"path":"/Reports/old.pdf","long":true,"time":"server","time_format":"2006-01-02"},"results":[{"status":"revision","kind":"file","result":{"type":"file","path_display":"/Reports/old.pdf","path_lower":"/reports/old.pdf","id":"id:file","rev":"015f","size":123,"server_modified":"2026-06-25T12:00:00Z","client_modified":"2026-06-25T11:00:00Z"}}],"warnings":[]}, - "rm": {"input":{},"results":[{"input":{"path":"/Reports/old.pdf","permanent":false,"recursive":false,"force":false},"result":{"type":"file","path_display":"/Reports/old.pdf","path_lower":"/reports/old.pdf","id":"id:file","rev":"015f","size":123,"server_modified":"2026-06-25T12:00:00Z","client_modified":"2026-06-25T11:00:00Z"}}],"warnings":[]}, - "search": {"input":{"query":"report","path":"/Reports","long":true,"sort":"name","reverse":false,"time":"server","time_format":"2006-01-02"},"results":[{"status":"found","kind":"folder","result":{"type":"folder","path_display":"/Reports","path_lower":"/reports","id":"id:folder"}}],"warnings":[]}, - "share list folder": {"input":{},"results":[{"status":"listed","kind":"shared_folder","result":{"type":"shared_folder","name":"Reports","path_lower":"/reports","shared_folder_id":"sfid:reports","preview_url":"https://www.dropbox.com/preview","access_type":"owner","is_inside_team_folder":false,"is_team_folder":true,"owner_display_names":["Ada Lovelace"],"parent_shared_folder_id":"sfid:parent","parent_folder_name":"Parent","time_invited":"2026-06-25T10:00:00Z","access_inheritance":"inherit"}}],"warnings":[]}, - "share list link": {"input":{"path":"/Reports/old.pdf","direct_only":true},"results":[{"status":"listed","kind":"file","result":{"type":"file","url":"https://www.dropbox.com/s/example/old.pdf","name":"old.pdf","path_lower":"/reports/old.pdf","id":"id:shared-file","expires":"2026-07-01T00:00:00Z","rev":"015f","size":123,"server_modified":"2026-06-25T12:00:00Z","client_modified":"2026-06-25T11:00:00Z","permissions":{"resolved_visibility":"public","requested_visibility":"public","effective_audience":"public","access_level":"viewer","can_revoke":true,"allow_download":true,"can_set_expiry":true,"can_remove_expiry":true,"can_allow_download":true,"can_disallow_download":true,"allow_comments":false,"can_set_password":true,"can_remove_password":true,"require_password":true,"can_use_extended_sharing_controls":true}}}],"warnings":[{"code":"deprecated_command","message":"use `dbxcli share-link list` instead"}]}, - "share-link create": {"input":{"path":"/Reports/old.pdf","access":"viewer","audience":"public","expires":"2026-07-01T00:00:00Z","allow_download":true,"password":true},"results":[{"status":"created","kind":"file","result":{"type":"file","url":"https://www.dropbox.com/s/example/old.pdf","name":"old.pdf","path_lower":"/reports/old.pdf","id":"id:shared-file","expires":"2026-07-01T00:00:00Z","rev":"015f","size":123,"server_modified":"2026-06-25T12:00:00Z","client_modified":"2026-06-25T11:00:00Z","permissions":{"resolved_visibility":"public","requested_visibility":"public","effective_audience":"public","access_level":"viewer","can_revoke":true,"allow_download":true,"can_set_expiry":true,"can_remove_expiry":true,"can_allow_download":true,"can_disallow_download":true,"allow_comments":false,"can_set_password":true,"can_remove_password":true,"require_password":true,"can_use_extended_sharing_controls":true}}}],"warnings":[]}, - "share-link download": {"input":{"url":"https://www.dropbox.com/s/example/old.pdf","target":"old.pdf","path":"/old.pdf","password":true},"results":[{"status":"downloaded","kind":"file","result":{"target":"old.pdf","link":{"type":"file","url":"https://www.dropbox.com/s/example/old.pdf","name":"old.pdf","path_lower":"/reports/old.pdf","id":"id:shared-file","expires":"2026-07-01T00:00:00Z","rev":"015f","size":123,"server_modified":"2026-06-25T12:00:00Z","client_modified":"2026-06-25T11:00:00Z","permissions":{"resolved_visibility":"public","requested_visibility":"public","effective_audience":"public","access_level":"viewer","can_revoke":true,"allow_download":true,"can_set_expiry":true,"can_remove_expiry":true,"can_allow_download":true,"can_disallow_download":true,"allow_comments":false,"can_set_password":true,"can_remove_password":true,"require_password":true,"can_use_extended_sharing_controls":true}}}}],"warnings":[]}, - "share-link info": {"input":{"url":"https://www.dropbox.com/s/example/old.pdf","path":"/old.pdf","password":true},"results":[{"status":"found","kind":"file","result":{"type":"file","url":"https://www.dropbox.com/s/example/old.pdf","name":"old.pdf","path_lower":"/reports/old.pdf","id":"id:shared-file","expires":"2026-07-01T00:00:00Z","rev":"015f","size":123,"server_modified":"2026-06-25T12:00:00Z","client_modified":"2026-06-25T11:00:00Z","permissions":{"resolved_visibility":"public","requested_visibility":"public","effective_audience":"public","access_level":"viewer","can_revoke":true,"allow_download":true,"can_set_expiry":true,"can_remove_expiry":true,"can_allow_download":true,"can_disallow_download":true,"allow_comments":false,"can_set_password":true,"can_remove_password":true,"require_password":true,"can_use_extended_sharing_controls":true}}}],"warnings":[]}, - "share-link list": {"input":{"path":"/Reports/old.pdf","direct_only":true},"results":[{"status":"listed","kind":"file","result":{"type":"file","url":"https://www.dropbox.com/s/example/old.pdf","name":"old.pdf","path_lower":"/reports/old.pdf","id":"id:shared-file","expires":"2026-07-01T00:00:00Z","rev":"015f","size":123,"server_modified":"2026-06-25T12:00:00Z","client_modified":"2026-06-25T11:00:00Z","permissions":{"resolved_visibility":"public","requested_visibility":"public","effective_audience":"public","access_level":"viewer","can_revoke":true,"allow_download":true,"can_set_expiry":true,"can_remove_expiry":true,"can_allow_download":true,"can_disallow_download":true,"allow_comments":false,"can_set_password":true,"can_remove_password":true,"require_password":true,"can_use_extended_sharing_controls":true}}}],"warnings":[]}, - "share-link revoke": {"input":{"path":"/Reports/old.pdf"},"results":[{"status":"revoked","kind":"file","result":{"url":"https://www.dropbox.com/s/example/old.pdf","link":{"type":"file","url":"https://www.dropbox.com/s/example/old.pdf","name":"old.pdf","path_lower":"/reports/old.pdf","id":"id:shared-file","expires":"2026-07-01T00:00:00Z","rev":"015f","size":123,"server_modified":"2026-06-25T12:00:00Z","client_modified":"2026-06-25T11:00:00Z","permissions":{"resolved_visibility":"public","requested_visibility":"public","effective_audience":"public","access_level":"viewer","can_revoke":true,"allow_download":true,"can_set_expiry":true,"can_remove_expiry":true,"can_allow_download":true,"can_disallow_download":true,"allow_comments":false,"can_set_password":true,"can_remove_password":true,"require_password":true,"can_use_extended_sharing_controls":true}}}}],"warnings":[]}, - "share-link update": {"input":{"url":"https://www.dropbox.com/s/example/old.pdf","audience":"public","expires":"2026-07-01T00:00:00Z","allow_download":true,"password":true},"results":[{"status":"updated","kind":"file","result":{"type":"file","url":"https://www.dropbox.com/s/example/old.pdf","name":"old.pdf","path_lower":"/reports/old.pdf","id":"id:shared-file","expires":"2026-07-01T00:00:00Z","rev":"015f","size":123,"server_modified":"2026-06-25T12:00:00Z","client_modified":"2026-06-25T11:00:00Z","permissions":{"resolved_visibility":"public","requested_visibility":"public","effective_audience":"public","access_level":"viewer","can_revoke":true,"allow_download":true,"can_set_expiry":true,"can_remove_expiry":true,"can_allow_download":true,"can_disallow_download":true,"allow_comments":false,"can_set_password":true,"can_remove_password":true,"require_password":true,"can_use_extended_sharing_controls":true}}}],"warnings":[]}, - "team add-member": {"input":{"email":"ada@example.com","first_name":"Ada","last_name":"Lovelace"},"results":[{"status":"added","kind":"team_member","input":{"email":"ada@example.com","first_name":"Ada","last_name":"Lovelace"},"result":{"type":"team_member_add","tag":"complete","results":[{"tag":"success","email":"ada@example.com","member":{"type":"team_member","team_member_id":"dbmid:team-member","external_id":"external-member","account_id":"dbid:account","email":"ada@example.com","email_verified":true,"status":"active","name":{"given_name":"Ada","surname":"Lovelace","familiar_name":"Ada","display_name":"Ada Lovelace","abbreviated_name":"AL"},"role":"member_only","groups":["g:dev"],"member_folder_id":"ns:member-folder","membership_type":"full","invited_on":"2026-06-24T12:00:00Z","joined_on":"2026-06-25T12:00:00Z","suspended_on":"2026-06-26T12:00:00Z","persistent_id":"persistent-id","is_directory_restricted":true,"profile_photo_url":"https://example.com/member.jpg"}}]}}],"warnings":[]}, - "team info": {"input":{},"results":[{"status":"found","kind":"team","input":{},"result":{"type":"team","name":"Engineering","team_id":"team-id","num_licensed_users":10,"num_provisioned_users":8}}],"warnings":[]}, - "team list-groups": {"input":{},"results":[{"status":"listed","kind":"team_group","result":{"type":"team_group","group_name":"Developers","group_id":"g:dev","group_external_id":"external-dev","member_count":3,"group_management_type":"company_managed"}}],"warnings":[]}, - "team list-members": {"input":{},"results":[{"status":"listed","kind":"team_member","result":{"type":"team_member","team_member_id":"dbmid:team-member","external_id":"external-member","account_id":"dbid:account","email":"ada@example.com","email_verified":true,"status":"active","name":{"given_name":"Ada","surname":"Lovelace","familiar_name":"Ada","display_name":"Ada Lovelace","abbreviated_name":"AL"},"role":"member_only","groups":["g:dev"],"member_folder_id":"ns:member-folder","membership_type":"full","invited_on":"2026-06-24T12:00:00Z","joined_on":"2026-06-25T12:00:00Z","suspended_on":"2026-06-26T12:00:00Z","persistent_id":"persistent-id","is_directory_restricted":true,"profile_photo_url":"https://example.com/member.jpg"}}],"warnings":[]}, - "team remove-member": {"input":{"email":"ada@example.com"},"results":[{"status":"removed","kind":"team_member","input":{"email":"ada@example.com"},"result":{"type":"team_member_remove","tag":"complete","async_job_id":"async-job-id"}}],"warnings":[]}, - "version": {"input":{},"results":[{"kind":"version","input":{},"result":{"version":"1.2.3","sdk_version":"sdk-version","spec_version":"spec-version"}}],"warnings":[]} + "account": {"ok":true,"schema_version":"1","command":"account","input":{"account_id":"dbid:lookup"},"results":[{"kind":"account","input":{"account_id":"dbid:lookup"},"result":{"type":"full","account_id":"dbid:account","name":{"given_name":"Ada","surname":"Lovelace","familiar_name":"Ada","display_name":"Ada Lovelace","abbreviated_name":"AL"},"email":"ada@example.com","email_verified":true,"disabled":false,"profile_photo_url":"https://example.com/profile.jpg","locale":"en","referral_link":"https://example.com/referral","is_paired":false,"account_type":"basic","is_teammate":true,"team_member_id":"dbmid:team-member","team":{"id":"team-id","name":"Engineering","member_id":"dbmid:team-member"}},"status":"found"}],"warnings":[]}, + "cp": {"ok":true,"schema_version":"1","command":"cp","input":{},"results":[{"input":{"from_path":"/Reports/old.pdf","to_path":"/Reports/copy.pdf"},"result":{"type":"file","path_display":"/Reports/copy.pdf","path_lower":"/reports/copy.pdf","id":"id:file","rev":"015f","size":123,"server_modified":"2026-06-25T12:00:00Z","client_modified":"2026-06-25T11:00:00Z"},"status":"copied","kind":"file"}],"warnings":[]}, + "du": {"ok":true,"schema_version":"1","command":"du","input":{},"results":[{"kind":"space_usage","input":{},"result":{"used":2048,"allocation":{"type":"team","allocated":1000000,"used":2048,"user_within_team_space_allocated":500000,"user_within_team_space_used_cached":1024,"user_within_team_space_limit_type":"fixed"}},"status":"reported"}],"warnings":[]}, + "get": {"ok":true,"schema_version":"1","command":"get","input":{"source":"/Reports/old.pdf","target":"old.pdf","recursive":false,"stdout":false},"results":[{"status":"downloaded","kind":"file","input":{"source":"/Reports/old.pdf","target":"old.pdf"},"result":{"type":"file","path_display":"/Reports/old.pdf","path_lower":"/reports/old.pdf","id":"id:file","rev":"015f","size":123,"server_modified":"2026-06-25T12:00:00Z","client_modified":"2026-06-25T11:00:00Z"}}],"warnings":[]}, + "ls": {"ok":true,"schema_version":"1","command":"ls","input":{"path":"/Reports","recursive":false,"include_deleted":true,"only_deleted":false,"long":true,"sort":"name","reverse":false,"time":"server","time_format":"2006-01-02"},"results":[{"status":"listed","kind":"file","result":{"type":"file","path_display":"/Reports/old.pdf","path_lower":"/reports/old.pdf","id":"id:file","rev":"015f","size":123,"server_modified":"2026-06-25T12:00:00Z","client_modified":"2026-06-25T11:00:00Z"},"input":{}}],"warnings":[]}, + "mkdir": {"ok":true,"schema_version":"1","command":"mkdir","input":{"path":"/Reports/new","parents":true},"results":[{"status":"created","kind":"folder","input":{"path":"/Reports/new","parents":true},"result":{"type":"folder","path_display":"/Reports/new","path_lower":"/reports/new","id":"id:folder"}}],"warnings":[]}, + "mv": {"ok":true,"schema_version":"1","command":"mv","input":{},"results":[{"input":{"from_path":"/Reports/copy.pdf","to_path":"/Reports/moved.pdf"},"result":{"type":"file","path_display":"/Reports/moved.pdf","path_lower":"/reports/moved.pdf","id":"id:file","rev":"015f","size":123,"server_modified":"2026-06-25T12:00:00Z","client_modified":"2026-06-25T11:00:00Z"},"status":"moved","kind":"file"}],"warnings":[]}, + "put": {"ok":true,"schema_version":"1","command":"put","input":{"source":"README.md","target":"/README.md","recursive":true,"if_exists":"overwrite","stdin":false},"results":[{"status":"uploaded","kind":"file","input":{"source":"README.md","target":"/README.md"},"result":{"type":"file","path_display":"/README.md","path_lower":"/readme.md","id":"id:file","rev":"015f","size":123,"server_modified":"2026-06-25T12:00:00Z","client_modified":"2026-06-25T11:00:00Z"}}],"warnings":[{"code":"skipped_symlink","message":"skipped symlink","path":"docs/link"}]}, + "restore": {"ok":true,"schema_version":"1","command":"restore","input":{"path":"/Reports/old.pdf","revision":"015f"},"results":[{"status":"restored","kind":"file","input":{"path":"/Reports/old.pdf","revision":"015f"},"result":{"type":"file","path_display":"/Reports/old.pdf","path_lower":"/reports/old.pdf","id":"id:file","rev":"015f","size":123,"server_modified":"2026-06-25T12:00:00Z","client_modified":"2026-06-25T11:00:00Z"}}],"warnings":[]}, + "revs": {"ok":true,"schema_version":"1","command":"revs","input":{"path":"/Reports/old.pdf","long":true,"time":"server","time_format":"2006-01-02"},"results":[{"status":"revision","kind":"file","result":{"type":"file","path_display":"/Reports/old.pdf","path_lower":"/reports/old.pdf","id":"id:file","rev":"015f","size":123,"server_modified":"2026-06-25T12:00:00Z","client_modified":"2026-06-25T11:00:00Z"},"input":{}}],"warnings":[]}, + "rm": {"ok":true,"schema_version":"1","command":"rm","input":{},"results":[{"input":{"path":"/Reports/old.pdf","permanent":false,"recursive":false,"force":false},"result":{"type":"file","path_display":"/Reports/old.pdf","path_lower":"/reports/old.pdf","id":"id:file","rev":"015f","size":123,"server_modified":"2026-06-25T12:00:00Z","client_modified":"2026-06-25T11:00:00Z"},"status":"deleted","kind":"file"}],"warnings":[]}, + "search": {"ok":true,"schema_version":"1","command":"search","input":{"query":"report","path":"/Reports","long":true,"sort":"name","reverse":false,"time":"server","time_format":"2006-01-02"},"results":[{"status":"found","kind":"folder","result":{"type":"folder","path_display":"/Reports","path_lower":"/reports","id":"id:folder"},"input":{}}],"warnings":[]}, + "share list folder": {"ok":true,"schema_version":"1","command":"share list folder","input":{},"results":[{"status":"listed","kind":"shared_folder","result":{"type":"shared_folder","name":"Reports","path_lower":"/reports","shared_folder_id":"sfid:reports","preview_url":"https://www.dropbox.com/preview","access_type":"owner","is_inside_team_folder":false,"is_team_folder":true,"owner_display_names":["Ada Lovelace"],"parent_shared_folder_id":"sfid:parent","parent_folder_name":"Parent","time_invited":"2026-06-25T10:00:00Z","access_inheritance":"inherit"},"input":{}}],"warnings":[]}, + "share list link": {"ok":true,"schema_version":"1","command":"share list link","input":{"path":"/Reports/old.pdf","direct_only":true},"results":[{"status":"listed","kind":"file","result":{"type":"file","url":"https://www.dropbox.com/s/example/old.pdf","name":"old.pdf","path_lower":"/reports/old.pdf","id":"id:shared-file","expires":"2026-07-01T00:00:00Z","rev":"015f","size":123,"server_modified":"2026-06-25T12:00:00Z","client_modified":"2026-06-25T11:00:00Z","permissions":{"resolved_visibility":"public","requested_visibility":"public","effective_audience":"public","access_level":"viewer","can_revoke":true,"allow_download":true,"can_set_expiry":true,"can_remove_expiry":true,"can_allow_download":true,"can_disallow_download":true,"allow_comments":false,"can_set_password":true,"can_remove_password":true,"require_password":true,"can_use_extended_sharing_controls":true}},"input":{}}],"warnings":[{"code":"deprecated_command","message":"use `dbxcli share-link list` instead"}]}, + "share-link create": {"ok":true,"schema_version":"1","command":"share-link create","input":{"path":"/Reports/old.pdf","access":"viewer","audience":"public","expires":"2026-07-01T00:00:00Z","allow_download":true,"password":true},"results":[{"status":"created","kind":"file","result":{"type":"file","url":"https://www.dropbox.com/s/example/old.pdf","name":"old.pdf","path_lower":"/reports/old.pdf","id":"id:shared-file","expires":"2026-07-01T00:00:00Z","rev":"015f","size":123,"server_modified":"2026-06-25T12:00:00Z","client_modified":"2026-06-25T11:00:00Z","permissions":{"resolved_visibility":"public","requested_visibility":"public","effective_audience":"public","access_level":"viewer","can_revoke":true,"allow_download":true,"can_set_expiry":true,"can_remove_expiry":true,"can_allow_download":true,"can_disallow_download":true,"allow_comments":false,"can_set_password":true,"can_remove_password":true,"require_password":true,"can_use_extended_sharing_controls":true}},"input":{}}],"warnings":[]}, + "share-link download": {"ok":true,"schema_version":"1","command":"share-link download","input":{"url":"https://www.dropbox.com/s/example/old.pdf","target":"old.pdf","path":"/old.pdf","password":true},"results":[{"status":"downloaded","kind":"file","result":{"target":"old.pdf","link":{"type":"file","url":"https://www.dropbox.com/s/example/old.pdf","name":"old.pdf","path_lower":"/reports/old.pdf","id":"id:shared-file","expires":"2026-07-01T00:00:00Z","rev":"015f","size":123,"server_modified":"2026-06-25T12:00:00Z","client_modified":"2026-06-25T11:00:00Z","permissions":{"resolved_visibility":"public","requested_visibility":"public","effective_audience":"public","access_level":"viewer","can_revoke":true,"allow_download":true,"can_set_expiry":true,"can_remove_expiry":true,"can_allow_download":true,"can_disallow_download":true,"allow_comments":false,"can_set_password":true,"can_remove_password":true,"require_password":true,"can_use_extended_sharing_controls":true}}},"input":{}}],"warnings":[]}, + "share-link info": {"ok":true,"schema_version":"1","command":"share-link info","input":{"url":"https://www.dropbox.com/s/example/old.pdf","path":"/old.pdf","password":true},"results":[{"status":"found","kind":"file","result":{"type":"file","url":"https://www.dropbox.com/s/example/old.pdf","name":"old.pdf","path_lower":"/reports/old.pdf","id":"id:shared-file","expires":"2026-07-01T00:00:00Z","rev":"015f","size":123,"server_modified":"2026-06-25T12:00:00Z","client_modified":"2026-06-25T11:00:00Z","permissions":{"resolved_visibility":"public","requested_visibility":"public","effective_audience":"public","access_level":"viewer","can_revoke":true,"allow_download":true,"can_set_expiry":true,"can_remove_expiry":true,"can_allow_download":true,"can_disallow_download":true,"allow_comments":false,"can_set_password":true,"can_remove_password":true,"require_password":true,"can_use_extended_sharing_controls":true}},"input":{}}],"warnings":[]}, + "share-link list": {"ok":true,"schema_version":"1","command":"share-link list","input":{"path":"/Reports/old.pdf","direct_only":true},"results":[{"status":"listed","kind":"file","result":{"type":"file","url":"https://www.dropbox.com/s/example/old.pdf","name":"old.pdf","path_lower":"/reports/old.pdf","id":"id:shared-file","expires":"2026-07-01T00:00:00Z","rev":"015f","size":123,"server_modified":"2026-06-25T12:00:00Z","client_modified":"2026-06-25T11:00:00Z","permissions":{"resolved_visibility":"public","requested_visibility":"public","effective_audience":"public","access_level":"viewer","can_revoke":true,"allow_download":true,"can_set_expiry":true,"can_remove_expiry":true,"can_allow_download":true,"can_disallow_download":true,"allow_comments":false,"can_set_password":true,"can_remove_password":true,"require_password":true,"can_use_extended_sharing_controls":true}},"input":{}}],"warnings":[]}, + "share-link revoke": {"ok":true,"schema_version":"1","command":"share-link revoke","input":{"path":"/Reports/old.pdf"},"results":[{"status":"revoked","kind":"file","result":{"url":"https://www.dropbox.com/s/example/old.pdf","link":{"type":"file","url":"https://www.dropbox.com/s/example/old.pdf","name":"old.pdf","path_lower":"/reports/old.pdf","id":"id:shared-file","expires":"2026-07-01T00:00:00Z","rev":"015f","size":123,"server_modified":"2026-06-25T12:00:00Z","client_modified":"2026-06-25T11:00:00Z","permissions":{"resolved_visibility":"public","requested_visibility":"public","effective_audience":"public","access_level":"viewer","can_revoke":true,"allow_download":true,"can_set_expiry":true,"can_remove_expiry":true,"can_allow_download":true,"can_disallow_download":true,"allow_comments":false,"can_set_password":true,"can_remove_password":true,"require_password":true,"can_use_extended_sharing_controls":true}}},"input":{}}],"warnings":[]}, + "share-link update": {"ok":true,"schema_version":"1","command":"share-link update","input":{"url":"https://www.dropbox.com/s/example/old.pdf","audience":"public","expires":"2026-07-01T00:00:00Z","allow_download":true,"password":true},"results":[{"status":"updated","kind":"file","result":{"type":"file","url":"https://www.dropbox.com/s/example/old.pdf","name":"old.pdf","path_lower":"/reports/old.pdf","id":"id:shared-file","expires":"2026-07-01T00:00:00Z","rev":"015f","size":123,"server_modified":"2026-06-25T12:00:00Z","client_modified":"2026-06-25T11:00:00Z","permissions":{"resolved_visibility":"public","requested_visibility":"public","effective_audience":"public","access_level":"viewer","can_revoke":true,"allow_download":true,"can_set_expiry":true,"can_remove_expiry":true,"can_allow_download":true,"can_disallow_download":true,"allow_comments":false,"can_set_password":true,"can_remove_password":true,"require_password":true,"can_use_extended_sharing_controls":true}},"input":{}}],"warnings":[]}, + "team add-member": {"ok":true,"schema_version":"1","command":"team add-member","input":{"email":"ada@example.com","first_name":"Ada","last_name":"Lovelace"},"results":[{"status":"added","kind":"team_member","input":{"email":"ada@example.com","first_name":"Ada","last_name":"Lovelace"},"result":{"type":"team_member_add","tag":"complete","results":[{"tag":"success","email":"ada@example.com","member":{"type":"team_member","team_member_id":"dbmid:team-member","external_id":"external-member","account_id":"dbid:account","email":"ada@example.com","email_verified":true,"status":"active","name":{"given_name":"Ada","surname":"Lovelace","familiar_name":"Ada","display_name":"Ada Lovelace","abbreviated_name":"AL"},"role":"member_only","groups":["g:dev"],"member_folder_id":"ns:member-folder","membership_type":"full","invited_on":"2026-06-24T12:00:00Z","joined_on":"2026-06-25T12:00:00Z","suspended_on":"2026-06-26T12:00:00Z","persistent_id":"persistent-id","is_directory_restricted":true,"profile_photo_url":"https://example.com/member.jpg"}}]}}],"warnings":[]}, + "team info": {"ok":true,"schema_version":"1","command":"team info","input":{},"results":[{"status":"found","kind":"team","input":{},"result":{"type":"team","name":"Engineering","team_id":"team-id","num_licensed_users":10,"num_provisioned_users":8}}],"warnings":[]}, + "team list-groups": {"ok":true,"schema_version":"1","command":"team list-groups","input":{},"results":[{"status":"listed","kind":"team_group","result":{"type":"team_group","group_name":"Developers","group_id":"g:dev","group_external_id":"external-dev","member_count":3,"group_management_type":"company_managed"},"input":{}}],"warnings":[]}, + "team list-members": {"ok":true,"schema_version":"1","command":"team list-members","input":{},"results":[{"status":"listed","kind":"team_member","result":{"type":"team_member","team_member_id":"dbmid:team-member","external_id":"external-member","account_id":"dbid:account","email":"ada@example.com","email_verified":true,"status":"active","name":{"given_name":"Ada","surname":"Lovelace","familiar_name":"Ada","display_name":"Ada Lovelace","abbreviated_name":"AL"},"role":"member_only","groups":["g:dev"],"member_folder_id":"ns:member-folder","membership_type":"full","invited_on":"2026-06-24T12:00:00Z","joined_on":"2026-06-25T12:00:00Z","suspended_on":"2026-06-26T12:00:00Z","persistent_id":"persistent-id","is_directory_restricted":true,"profile_photo_url":"https://example.com/member.jpg"},"input":{}}],"warnings":[]}, + "team remove-member": {"ok":true,"schema_version":"1","command":"team remove-member","input":{"email":"ada@example.com"},"results":[{"status":"removed","kind":"team_member","input":{"email":"ada@example.com"},"result":{"type":"team_member_remove","tag":"complete","async_job_id":"async-job-id"}}],"warnings":[]}, + "version": {"ok":true,"schema_version":"1","command":"version","input":{},"results":[{"kind":"version","input":{},"result":{"version":"1.2.3","sdk_version":"sdk-version","spec_version":"spec-version"},"status":"reported"}],"warnings":[]} } diff --git a/cmd/testdata/json_contract/success_schemas.json b/cmd/testdata/json_contract/success_schemas.json index cba66fd..459b8a9 100644 --- a/cmd/testdata/json_contract/success_schemas.json +++ b/cmd/testdata/json_contract/success_schemas.json @@ -81,8 +81,11 @@ "path" ], "operation_output": [ + "command", "input", + "ok", "results", + "schema_version", "warnings" ], "operation_result": [ @@ -291,7 +294,9 @@ "input": "account_input", "result_input": "account_input", "result": "account", - "statuses": [], + "statuses": [ + "found" + ], "kinds": [ "account" ], @@ -303,8 +308,15 @@ "input": "empty", "result_input": "relocation_input", "result": "metadata", - "statuses": [], - "kinds": [], + "statuses": [ + "copied" + ], + "kinds": [ + "deleted", + "file", + "folder", + "unknown" + ], "warnings": [] }, "du": { @@ -313,7 +325,9 @@ "input": "empty", "result_input": "empty", "result": "du_output", - "statuses": [], + "statuses": [ + "reported" + ], "kinds": [ "space_usage" ], @@ -340,7 +354,7 @@ "top_level": "operation_output", "result_wrapper": "operation_result", "input": "ls_input", - "result_input": null, + "result_input": "empty", "result": "metadata", "statuses": [ "listed" @@ -374,8 +388,15 @@ "input": "empty", "result_input": "relocation_input", "result": "metadata", - "statuses": [], - "kinds": [], + "statuses": [ + "moved" + ], + "kinds": [ + "deleted", + "file", + "folder", + "unknown" + ], "warnings": [] }, "put": { @@ -416,7 +437,7 @@ "top_level": "operation_output", "result_wrapper": "operation_result", "input": "revs_input", - "result_input": null, + "result_input": "empty", "result": "metadata", "statuses": [ "revision" @@ -433,15 +454,23 @@ "input": "empty", "result_input": "remove_input", "result": "metadata", - "statuses": [], - "kinds": [], + "statuses": [ + "deleted", + "permanently_deleted" + ], + "kinds": [ + "deleted", + "file", + "folder", + "unknown" + ], "warnings": [] }, "search": { "top_level": "operation_output", "result_wrapper": "operation_result", "input": "search_input", - "result_input": null, + "result_input": "empty", "result": "metadata", "statuses": [ "found" @@ -458,7 +487,7 @@ "top_level": "operation_output", "result_wrapper": "operation_result", "input": "empty", - "result_input": null, + "result_input": "empty", "result": "share_folder", "statuses": [ "listed" @@ -472,7 +501,7 @@ "top_level": "operation_output", "result_wrapper": "operation_result", "input": "share_link_list_input", - "result_input": null, + "result_input": "empty", "result": "share_link_metadata", "statuses": [ "listed" @@ -490,7 +519,7 @@ "top_level": "operation_output", "result_wrapper": "operation_result", "input": "share_link_create_input", - "result_input": null, + "result_input": "empty", "result": "share_link_metadata", "statuses": [ "created", @@ -507,7 +536,7 @@ "top_level": "operation_output", "result_wrapper": "operation_result", "input": "share_link_download_input", - "result_input": null, + "result_input": "empty", "result": "share_link_download_result", "statuses": [ "downloaded" @@ -523,7 +552,7 @@ "top_level": "operation_output", "result_wrapper": "operation_result", "input": "share_link_info_input", - "result_input": null, + "result_input": "empty", "result": "share_link_metadata", "statuses": [ "found" @@ -539,7 +568,7 @@ "top_level": "operation_output", "result_wrapper": "operation_result", "input": "share_link_list_input", - "result_input": null, + "result_input": "empty", "result": "share_link_metadata", "statuses": [ "listed" @@ -555,7 +584,7 @@ "top_level": "operation_output", "result_wrapper": "operation_result", "input": "share_link_revoke_input", - "result_input": null, + "result_input": "empty", "result": "share_link_revoke_result", "statuses": [ "revoked" @@ -572,7 +601,7 @@ "top_level": "operation_output", "result_wrapper": "operation_result", "input": "share_link_update_input", - "result_input": null, + "result_input": "empty", "result": "share_link_metadata", "statuses": [ "updated" @@ -618,7 +647,7 @@ "top_level": "operation_output", "result_wrapper": "operation_result", "input": "empty", - "result_input": null, + "result_input": "empty", "result": "team_group", "statuses": [ "listed" @@ -632,7 +661,7 @@ "top_level": "operation_output", "result_wrapper": "operation_result", "input": "empty", - "result_input": null, + "result_input": "empty", "result": "team_member", "statuses": [ "listed" @@ -664,7 +693,9 @@ "input": "empty", "result_input": "empty", "result": "version", - "statuses": [], + "statuses": [ + "reported" + ], "kinds": [ "version" ], diff --git a/cmd/version.go b/cmd/version.go index 269950e..6c53c78 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -30,7 +30,10 @@ type versionOutput struct { SpecVersion string `json:"spec_version"` } -const versionKindVersion = "version" +const ( + versionJSONStatusReported = "reported" + versionKindVersion = "version" +) var dropboxVersionFunc = dropbox.Version @@ -52,7 +55,7 @@ func versionCommand(cmd *cobra.Command, version string) error { info := newVersionOutput(version) return commandOutput(cmd).Render(func(w io.Writer) error { return renderVersion(w, info) - }, newVersionOperationOutput(info)) + }, withJSONCommand(cmd, newVersionOperationOutput(info))) } func renderVersion(out io.Writer, info versionOutput) error { @@ -74,6 +77,6 @@ func newVersionOutput(version string) versionOutput { func newVersionOperationOutput(info versionOutput) jsonOperationOutput { input := versionInput{} return newJSONOperationOutput(input, []jsonOperationResult{ - newJSONOperationResult("", versionKindVersion, input, info), + newJSONOperationResult(versionJSONStatusReported, versionKindVersion, input, info), }, nil) } diff --git a/docs/json-schema/v1/README.md b/docs/json-schema/v1/README.md new file mode 100644 index 0000000..66cc723 --- /dev/null +++ b/docs/json-schema/v1/README.md @@ -0,0 +1,30 @@ +# dbxcli JSON schema v1 + +These schemas describe the stable top-level JSON envelopes emitted by +`dbxcli --output=json`. + +- `success.schema.json` validates successful command responses. +- `error.schema.json` validates command error responses. + +Successful responses always include: + +- `ok: true` +- `schema_version: "1"` +- `command`: command path without the binary name, such as `ls` or + `share-link create` +- `input`: command-specific request fields +- `results`: command-specific result objects; every result includes `status`, + `kind`, `input`, and `result` +- `warnings`: machine-actionable warnings, or `[]` + +Error responses always include: + +- `ok: false` +- `schema_version: "1"` +- `command`: command path when available, or `dbxcli` for root/pre-parse errors +- `error.message`: human-readable error text +- `error.code`: stable machine-readable error code +- `warnings`: machine-actionable warnings, or `[]` + +Command-specific `input` and `result` payloads are documented in the README and +locked by the golden contract fixtures under `cmd/testdata/json_contract/`. diff --git a/docs/json-schema/v1/error.schema.json b/docs/json-schema/v1/error.schema.json new file mode 100644 index 0000000..755bc98 --- /dev/null +++ b/docs/json-schema/v1/error.schema.json @@ -0,0 +1,86 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/dropbox/dbxcli/docs/json-schema/v1/error.schema.json", + "title": "dbxcli JSON error response", + "type": "object", + "additionalProperties": false, + "required": [ + "ok", + "schema_version", + "command", + "error", + "warnings" + ], + "properties": { + "ok": { + "const": false + }, + "schema_version": { + "const": "1" + }, + "command": { + "type": "string", + "description": "The dbxcli command path when available, or dbxcli for root/pre-parse errors." + }, + "error": { + "type": "object", + "additionalProperties": false, + "required": [ + "message", + "code" + ], + "properties": { + "message": { + "type": "string" + }, + "code": { + "type": "string", + "enum": [ + "invalid_arguments", + "path_conflict", + "auth_required", + "auth_refresh_failed", + "app_key_required", + "auth_exchange_failed", + "not_found", + "permission_denied", + "rate_limited", + "dropbox_api_error", + "structured_output_unsupported", + "unsupported_output_format", + "unknown_command", + "unknown_flag", + "command_failed" + ] + } + } + }, + "warnings": { + "type": "array", + "items": { + "$ref": "#/$defs/warning" + } + } + }, + "$defs": { + "warning": { + "type": "object", + "additionalProperties": false, + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "path": { + "type": "string" + } + } + } + } +} diff --git a/docs/json-schema/v1/success.schema.json b/docs/json-schema/v1/success.schema.json new file mode 100644 index 0000000..00b9c97 --- /dev/null +++ b/docs/json-schema/v1/success.schema.json @@ -0,0 +1,86 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/dropbox/dbxcli/docs/json-schema/v1/success.schema.json", + "title": "dbxcli JSON success response", + "type": "object", + "additionalProperties": false, + "required": [ + "ok", + "schema_version", + "command", + "input", + "results", + "warnings" + ], + "properties": { + "ok": { + "const": true + }, + "schema_version": { + "const": "1" + }, + "command": { + "type": "string", + "description": "The dbxcli command path without the binary name, for example ls or share-link create." + }, + "input": { + "type": "object", + "description": "Command-specific request fields." + }, + "results": { + "type": "array", + "items": { + "$ref": "#/$defs/result" + } + }, + "warnings": { + "type": "array", + "items": { + "$ref": "#/$defs/warning" + } + } + }, + "$defs": { + "result": { + "type": "object", + "additionalProperties": false, + "required": [ + "status", + "kind", + "input", + "result" + ], + "properties": { + "status": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "input": { + "type": "object" + }, + "result": {} + } + }, + "warning": { + "type": "object", + "additionalProperties": false, + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "path": { + "type": "string" + } + } + } + } +}