From 38bf3f51215fc1354b4f287c1be8e3b82f8dc794 Mon Sep 17 00:00:00 2001 From: Mateusz Wasiak Date: Thu, 12 Nov 2026 16:07:03 +0100 Subject: [PATCH 1/2] feat: add Shuttle CLI commands via box-node-sdk 4.19 Expose all 39 Shuttle V2026R0 operations as oclif commands using tsClient.shuttle, bump box-node-sdk to ^4.19.0, and cover each command with nock smoke tests plus module helper/SDK surface checks. --- package-lock.json | 8 +- package.json | 2 +- .../shuttle/discovered-subjects/index.js | 39 + src/commands/shuttle/job-schedules/create.js | 71 ++ src/commands/shuttle/job-schedules/delete.js | 33 + src/commands/shuttle/job-schedules/get.js | 35 + src/commands/shuttle/job-schedules/index.js | 35 + src/commands/shuttle/job-schedules/update.js | 79 ++ src/commands/shuttle/jobs/create.js | 89 ++ src/commands/shuttle/jobs/delete.js | 33 + src/commands/shuttle/jobs/get.js | 31 + src/commands/shuttle/jobs/index.js | 26 + .../shuttle/jobs/prepare-migration.js | 36 + src/commands/shuttle/jobs/start.js | 54 + src/commands/shuttle/jobs/stop.js | 32 + src/commands/shuttle/jobs/update.js | 86 ++ src/commands/shuttle/mapping-errors/index.js | 35 + src/commands/shuttle/path-entries/index.js | 66 ++ .../shuttle/path-mapping-imports/create.js | 50 + src/commands/shuttle/path-mappings/create.js | 77 ++ src/commands/shuttle/path-mappings/delete.js | 33 + src/commands/shuttle/path-mappings/get.js | 35 + src/commands/shuttle/path-mappings/index.js | 35 + src/commands/shuttle/path-mappings/update.js | 85 ++ src/commands/shuttle/reports/create.js | 60 + src/commands/shuttle/reports/get.js | 33 + src/commands/shuttle/reports/index.js | 35 + src/commands/shuttle/runs/get.js | 33 + src/commands/shuttle/runs/index.js | 34 + .../shuttle/subject-mapping-imports/create.js | 50 + .../shuttle/subject-mappings/create.js | 72 ++ .../shuttle/subject-mappings/delete.js | 35 + src/commands/shuttle/subject-mappings/get.js | 36 + .../shuttle/subject-mappings/index.js | 35 + .../shuttle/subject-mappings/update.js | 70 ++ src/commands/shuttle/systems/create.js | 44 + src/commands/shuttle/systems/delete.js | 33 + src/commands/shuttle/systems/get.js | 31 + src/commands/shuttle/systems/index.js | 26 + .../systems/issue-registration-token.js | 38 + src/commands/shuttle/systems/update.js | 49 + src/modules/shuttle.js | 455 ++++++++ test/commands/shuttle.test.js | 1040 +++++++++++++++++ test/modules/shuttle.test.js | 315 +++++ 44 files changed, 3624 insertions(+), 5 deletions(-) create mode 100644 src/commands/shuttle/discovered-subjects/index.js create mode 100644 src/commands/shuttle/job-schedules/create.js create mode 100644 src/commands/shuttle/job-schedules/delete.js create mode 100644 src/commands/shuttle/job-schedules/get.js create mode 100644 src/commands/shuttle/job-schedules/index.js create mode 100644 src/commands/shuttle/job-schedules/update.js create mode 100644 src/commands/shuttle/jobs/create.js create mode 100644 src/commands/shuttle/jobs/delete.js create mode 100644 src/commands/shuttle/jobs/get.js create mode 100644 src/commands/shuttle/jobs/index.js create mode 100644 src/commands/shuttle/jobs/prepare-migration.js create mode 100644 src/commands/shuttle/jobs/start.js create mode 100644 src/commands/shuttle/jobs/stop.js create mode 100644 src/commands/shuttle/jobs/update.js create mode 100644 src/commands/shuttle/mapping-errors/index.js create mode 100644 src/commands/shuttle/path-entries/index.js create mode 100644 src/commands/shuttle/path-mapping-imports/create.js create mode 100644 src/commands/shuttle/path-mappings/create.js create mode 100644 src/commands/shuttle/path-mappings/delete.js create mode 100644 src/commands/shuttle/path-mappings/get.js create mode 100644 src/commands/shuttle/path-mappings/index.js create mode 100644 src/commands/shuttle/path-mappings/update.js create mode 100644 src/commands/shuttle/reports/create.js create mode 100644 src/commands/shuttle/reports/get.js create mode 100644 src/commands/shuttle/reports/index.js create mode 100644 src/commands/shuttle/runs/get.js create mode 100644 src/commands/shuttle/runs/index.js create mode 100644 src/commands/shuttle/subject-mapping-imports/create.js create mode 100644 src/commands/shuttle/subject-mappings/create.js create mode 100644 src/commands/shuttle/subject-mappings/delete.js create mode 100644 src/commands/shuttle/subject-mappings/get.js create mode 100644 src/commands/shuttle/subject-mappings/index.js create mode 100644 src/commands/shuttle/subject-mappings/update.js create mode 100644 src/commands/shuttle/systems/create.js create mode 100644 src/commands/shuttle/systems/delete.js create mode 100644 src/commands/shuttle/systems/get.js create mode 100644 src/commands/shuttle/systems/index.js create mode 100644 src/commands/shuttle/systems/issue-registration-token.js create mode 100644 src/commands/shuttle/systems/update.js create mode 100644 src/modules/shuttle.js create mode 100644 test/commands/shuttle.test.js create mode 100644 test/modules/shuttle.test.js diff --git a/package-lock.json b/package-lock.json index f5743d70..5e0e21dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "@oclif/table": "0.5.4", "@octokit/rest": "^22.0.0", "archiver": "^8.0.0", - "box-node-sdk": "^4.11.1", + "box-node-sdk": "^4.19.0", "chalk": "^2.4.1", "cli-progress": "^2.1.0", "csv": "^6.3.3", @@ -4589,9 +4589,9 @@ "license": "MIT" }, "node_modules/box-node-sdk": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/box-node-sdk/-/box-node-sdk-4.11.1.tgz", - "integrity": "sha512-4A/SchEZqcpnzE+3JU5NoIME4ZPzIic1tKSmW0HjJOu0lygEmSs0o4ZSpsP5vwNlB21kO5NOFN7ismR+pvLP3g==", + "version": "4.19.0", + "resolved": "https://box.jfrog.io/box/api/npm/boxnpm/box-node-sdk/-/box-node-sdk-4.19.0.tgz", + "integrity": "sha512-WlxALVmOuF5w06zCqGrkSfpMWnDNfZ/Yhhj8bUpsZZIF9B5CA1ygJzKEcAmGhZrNBY7aWQxRVjIolS3DkI/X7A==", "license": "Apache-2.0", "dependencies": { "@cypress/request": "^4.0.1", diff --git a/package.json b/package.json index fafe6b9d..52990736 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "@oclif/table": "0.5.4", "@octokit/rest": "^22.0.0", "archiver": "^8.0.0", - "box-node-sdk": "^4.11.1", + "box-node-sdk": "^4.19.0", "chalk": "^2.4.1", "cli-progress": "^2.1.0", "csv": "^6.3.3", diff --git a/src/commands/shuttle/discovered-subjects/index.js b/src/commands/shuttle/discovered-subjects/index.js new file mode 100644 index 00000000..05ed58be --- /dev/null +++ b/src/commands/shuttle/discovered-subjects/index.js @@ -0,0 +1,39 @@ +'use strict'; + +const BoxCommand = require('../../../box-command'); +const { Flags } = require('@oclif/core'); +const PaginationUtilities = require('../../../pagination-utils'); + +class ShuttleDiscoveredSubjectsListCommand extends BoxCommand { + async run() { + const { flags } = await this.parse(ShuttleDiscoveredSubjectsListCommand); + const subjects = await this.markerPagination( + (pageQueryParams) => + this.tsClient.shuttle.getShuttleDiscoveredSubjectsV2026R0(pageQueryParams), + { jobId: flags['job-id'] } + ); + await this.output(subjects); + } +} + +ShuttleDiscoveredSubjectsListCommand.aliases = [ + 'shuttle:discovered-subjects:list', +]; +ShuttleDiscoveredSubjectsListCommand.description = + 'List discovered subjects for shuttle jobs'; +ShuttleDiscoveredSubjectsListCommand.examples = [ + 'box shuttle:discovered-subjects --job-id 6a315c0754833bfc5df067bf', +]; +ShuttleDiscoveredSubjectsListCommand._endpoint = + 'get_shuttle_discovered_subjects'; + +ShuttleDiscoveredSubjectsListCommand.flags = { + ...BoxCommand.flags, + ...PaginationUtilities.flags, + 'job-id': Flags.string({ + description: 'Shuttle job ID whose discovered subjects to list', + required: true, + }), +}; + +module.exports = ShuttleDiscoveredSubjectsListCommand; diff --git a/src/commands/shuttle/job-schedules/create.js b/src/commands/shuttle/job-schedules/create.js new file mode 100644 index 00000000..3ea8ab61 --- /dev/null +++ b/src/commands/shuttle/job-schedules/create.js @@ -0,0 +1,71 @@ +'use strict'; + +const BoxCommand = require('../../../box-command'); +const { Flags } = require('@oclif/core'); +const { + resolveBody, + bodyFlags, + scheduleTriggerFlags, + buildScheduleTrigger, +} = require('../../../modules/shuttle'); + +class ShuttleJobSchedulesCreateCommand extends BoxCommand { + async run() { + const { flags } = await this.parse(ShuttleJobSchedulesCreateCommand); + const requestBody = resolveBody(flags, () => { + if (!flags['job-id'] || !flags['run-mode']) { + this.error( + '--job-id and --run-mode are required unless --body / --body-file is set.' + ); + } + const body = { + jobId: flags['job-id'], + runMode: flags['run-mode'], + trigger: buildScheduleTrigger(flags, { + required: true, + command: this, + }), + }; + if (flags.state !== undefined) { + body.state = flags.state; + } + if (flags.description !== undefined) { + body.description = flags.description; + } + return body; + }); + + const schedule = + await this.tsClient.shuttle.createShuttleJobScheduleV2026R0(requestBody); + await this.output(schedule.rawData ?? schedule); + } +} + +ShuttleJobSchedulesCreateCommand.description = 'Create a shuttle job schedule'; +ShuttleJobSchedulesCreateCommand.examples = [ + 'box shuttle:job-schedules:create --job-id 6a315c0754833bfc5df067bf --run-mode transfer --trigger-type by_time --start-at 2026-03-16T09:00:00 --time-zone America/Los_Angeles --repeat daily', + 'box shuttle:job-schedules:create --job-id 6a315c0754833bfc5df067bf --run-mode simulation --trigger-type by_job --triggered-by-job-id 8d2ca68e810d9156194530c3 --last-run-condition on_success', +]; +ShuttleJobSchedulesCreateCommand._endpoint = 'post_shuttle_job_schedules'; + +ShuttleJobSchedulesCreateCommand.flags = { + ...BoxCommand.flags, + ...bodyFlags, + 'job-id': Flags.string({ + description: 'Shuttle job ID (required unless --body / --body-file)', + }), + 'run-mode': Flags.string({ + description: 'Schedule run mode (required unless --body / --body-file)', + options: ['transfer', 'simulation', 'analysis'], + }), + state: Flags.string({ + description: 'Schedule state', + options: ['enabled', 'paused'], + }), + description: Flags.string({ + description: 'Optional schedule description', + }), + ...scheduleTriggerFlags, +}; + +module.exports = ShuttleJobSchedulesCreateCommand; diff --git a/src/commands/shuttle/job-schedules/delete.js b/src/commands/shuttle/job-schedules/delete.js new file mode 100644 index 00000000..cf3ad24c --- /dev/null +++ b/src/commands/shuttle/job-schedules/delete.js @@ -0,0 +1,33 @@ +'use strict'; + +const { Args } = require('@oclif/core'); +const BoxCommand = require('../../../box-command'); + +class ShuttleJobSchedulesDeleteCommand extends BoxCommand { + async run() { + const { args } = await this.parse(ShuttleJobSchedulesDeleteCommand); + await this.tsClient.shuttle.deleteShuttleJobScheduleByIdV2026R0(args.id); + this.info(`Deleted shuttle job schedule ${args.id}`); + } +} + +ShuttleJobSchedulesDeleteCommand.description = 'Delete a shuttle job schedule'; +ShuttleJobSchedulesDeleteCommand.examples = [ + 'box shuttle:job-schedules:delete 6a315c0754833bfc5df067bf', +]; +ShuttleJobSchedulesDeleteCommand._endpoint = 'delete_shuttle_job_schedules_id'; + +ShuttleJobSchedulesDeleteCommand.flags = { + ...BoxCommand.flags, +}; + +ShuttleJobSchedulesDeleteCommand.args = { + id: Args.string({ + name: 'id', + required: true, + hidden: false, + description: 'ID of the shuttle job schedule to delete', + }), +}; + +module.exports = ShuttleJobSchedulesDeleteCommand; diff --git a/src/commands/shuttle/job-schedules/get.js b/src/commands/shuttle/job-schedules/get.js new file mode 100644 index 00000000..f51f5d54 --- /dev/null +++ b/src/commands/shuttle/job-schedules/get.js @@ -0,0 +1,35 @@ +'use strict'; + +const { Args } = require('@oclif/core'); +const BoxCommand = require('../../../box-command'); + +class ShuttleJobSchedulesGetCommand extends BoxCommand { + async run() { + const { args } = await this.parse(ShuttleJobSchedulesGetCommand); + const schedule = await this.tsClient.shuttle.getShuttleJobScheduleByIdV2026R0( + args.id + ); + await this.output(schedule.rawData ?? schedule); + } +} + +ShuttleJobSchedulesGetCommand.description = 'Get a shuttle job schedule by ID'; +ShuttleJobSchedulesGetCommand.examples = [ + 'box shuttle:job-schedules:get 6a315c0754833bfc5df067bf', +]; +ShuttleJobSchedulesGetCommand._endpoint = 'get_shuttle_job_schedules_id'; + +ShuttleJobSchedulesGetCommand.flags = { + ...BoxCommand.flags, +}; + +ShuttleJobSchedulesGetCommand.args = { + id: Args.string({ + name: 'id', + required: true, + hidden: false, + description: 'ID of the shuttle job schedule', + }), +}; + +module.exports = ShuttleJobSchedulesGetCommand; diff --git a/src/commands/shuttle/job-schedules/index.js b/src/commands/shuttle/job-schedules/index.js new file mode 100644 index 00000000..48fbab53 --- /dev/null +++ b/src/commands/shuttle/job-schedules/index.js @@ -0,0 +1,35 @@ +'use strict'; + +const BoxCommand = require('../../../box-command'); +const { Flags } = require('@oclif/core'); +const PaginationUtilities = require('../../../pagination-utils'); + +class ShuttleJobSchedulesListCommand extends BoxCommand { + async run() { + const { flags } = await this.parse(ShuttleJobSchedulesListCommand); + const schedules = await this.markerPagination( + (pageQueryParams) => + this.tsClient.shuttle.getShuttleJobSchedulesV2026R0(pageQueryParams), + { jobId: flags['job-id'] } + ); + await this.output(schedules); + } +} + +ShuttleJobSchedulesListCommand.aliases = ['shuttle:job-schedules:list']; +ShuttleJobSchedulesListCommand.description = 'List shuttle job schedules'; +ShuttleJobSchedulesListCommand.examples = [ + 'box shuttle:job-schedules --job-id 6a315c0754833bfc5df067bf', +]; +ShuttleJobSchedulesListCommand._endpoint = 'get_shuttle_job_schedules'; + +ShuttleJobSchedulesListCommand.flags = { + ...BoxCommand.flags, + ...PaginationUtilities.flags, + 'job-id': Flags.string({ + description: 'Shuttle job ID whose schedules to list', + required: true, + }), +}; + +module.exports = ShuttleJobSchedulesListCommand; diff --git a/src/commands/shuttle/job-schedules/update.js b/src/commands/shuttle/job-schedules/update.js new file mode 100644 index 00000000..a75c8dd5 --- /dev/null +++ b/src/commands/shuttle/job-schedules/update.js @@ -0,0 +1,79 @@ +'use strict'; + +const BoxCommand = require('../../../box-command'); +const { Flags, Args } = require('@oclif/core'); +const { + resolveBody, + bodyFlags, + scheduleTriggerFlags, + buildScheduleTrigger, +} = require('../../../modules/shuttle'); + +class ShuttleJobSchedulesUpdateCommand extends BoxCommand { + async run() { + const { flags, args } = await this.parse(ShuttleJobSchedulesUpdateCommand); + const requestBody = resolveBody(flags, () => { + const body = {}; + if (flags.state !== undefined) { + body.state = flags.state; + } + if (flags['run-mode'] !== undefined) { + body.runMode = flags['run-mode']; + } + if (flags.description !== undefined) { + body.description = flags.description; + } + const trigger = buildScheduleTrigger(flags, { command: this }); + if (trigger !== undefined) { + body.trigger = trigger; + } + if (Object.keys(body).length === 0) { + this.error( + 'Provide at least one update flag, or use --body / --body-file.' + ); + } + return body; + }); + + const schedule = await this.tsClient.shuttle.updateShuttleJobScheduleByIdV2026R0( + args.id, + requestBody + ); + await this.output(schedule.rawData ?? schedule); + } +} + +ShuttleJobSchedulesUpdateCommand.description = 'Update a shuttle job schedule'; +ShuttleJobSchedulesUpdateCommand.examples = [ + 'box shuttle:job-schedules:update 6a315c0754833bfc5df067bf --state paused', + 'box shuttle:job-schedules:update 6a315c0754833bfc5df067bf --trigger-type by_time --start-at 2026-03-16T09:00:00 --time-zone America/Los_Angeles --repeat weekdays', +]; +ShuttleJobSchedulesUpdateCommand._endpoint = 'put_shuttle_job_schedules_id'; + +ShuttleJobSchedulesUpdateCommand.flags = { + ...BoxCommand.flags, + ...bodyFlags, + state: Flags.string({ + description: 'Schedule state', + options: ['enabled', 'paused'], + }), + 'run-mode': Flags.string({ + description: 'Schedule run mode', + options: ['transfer', 'simulation', 'analysis'], + }), + description: Flags.string({ + description: 'Updated description', + }), + ...scheduleTriggerFlags, +}; + +ShuttleJobSchedulesUpdateCommand.args = { + id: Args.string({ + name: 'id', + required: true, + hidden: false, + description: 'ID of the shuttle job schedule to update', + }), +}; + +module.exports = ShuttleJobSchedulesUpdateCommand; diff --git a/src/commands/shuttle/jobs/create.js b/src/commands/shuttle/jobs/create.js new file mode 100644 index 00000000..ae6e79c9 --- /dev/null +++ b/src/commands/shuttle/jobs/create.js @@ -0,0 +1,89 @@ +'use strict'; + +const BoxCommand = require('../../../box-command'); +const { Flags, Args } = require('@oclif/core'); +const { + resolveBody, + bodyFlags, + jobAccountMappingFlags, + buildAccountMapping, + jobSettingsFlags, + buildSettings, +} = require('../../../modules/shuttle'); + +class ShuttleJobsCreateCommand extends BoxCommand { + async run() { + const { flags, args } = await this.parse(ShuttleJobsCreateCommand); + const requestBody = resolveBody(flags, () => { + if (!args.name || !flags['job-type'] || !flags['source-system-id']) { + this.error( + 'name, --job-type, and --source-system-id are required unless --body / --body-file is set.' + ); + } + const body = { + name: args.name, + jobType: flags['job-type'], + sourceSystemId: flags['source-system-id'], + }; + if (flags.comment !== undefined) { + body.comment = flags.comment; + } + if (flags.tag) { + body.tags = flags.tag; + } + const accountMapping = buildAccountMapping(flags); + if (accountMapping) { + body.accountMapping = accountMapping; + } + const settings = buildSettings(flags); + if (settings) { + body.settings = settings; + } + return body; + }); + + const job = await this.tsClient.shuttle.createShuttleJobV2026R0(requestBody); + await this.output(job.rawData ?? job); + } +} + +ShuttleJobsCreateCommand.description = 'Create a shuttle job'; +ShuttleJobsCreateCommand.examples = [ + 'box shuttle:jobs:create "Q4 Migration" --job-type transfer --source-system-id 6a3ba68e810d9156194530a1', + 'box shuttle:jobs:create "Q4 Migration" --job-type transfer --source-system-id 6a3ba68e810d9156194530a1 --account-mapping-enabled --are-permissions-mapped --is-overwrite-enabled', +]; +ShuttleJobsCreateCommand._endpoint = 'post_shuttle_jobs'; + +ShuttleJobsCreateCommand.flags = { + ...BoxCommand.flags, + ...bodyFlags, + 'job-type': Flags.string({ + description: 'Job type (required unless --body / --body-file)', + options: ['transfer', 'analysis'], + }), + 'source-system-id': Flags.string({ + description: + 'Source shuttle system ID (required unless --body / --body-file)', + }), + comment: Flags.string({ + description: 'Optional comment for the job', + }), + tag: Flags.string({ + description: 'Tag to attach (repeatable)', + multiple: true, + }), + ...jobAccountMappingFlags, + ...jobSettingsFlags, +}; + +ShuttleJobsCreateCommand.args = { + name: Args.string({ + name: 'name', + required: false, + hidden: false, + description: + 'Display name for the new shuttle job (required unless --body / --body-file)', + }), +}; + +module.exports = ShuttleJobsCreateCommand; diff --git a/src/commands/shuttle/jobs/delete.js b/src/commands/shuttle/jobs/delete.js new file mode 100644 index 00000000..4de60e03 --- /dev/null +++ b/src/commands/shuttle/jobs/delete.js @@ -0,0 +1,33 @@ +'use strict'; + +const { Args } = require('@oclif/core'); +const BoxCommand = require('../../../box-command'); + +class ShuttleJobsDeleteCommand extends BoxCommand { + async run() { + const { args } = await this.parse(ShuttleJobsDeleteCommand); + await this.tsClient.shuttle.deleteShuttleJobByIdV2026R0(args.id); + this.info(`Deleted shuttle job ${args.id}`); + } +} + +ShuttleJobsDeleteCommand.description = 'Delete a shuttle job'; +ShuttleJobsDeleteCommand.examples = [ + 'box shuttle:jobs:delete 6a315c0754833bfc5df067bf', +]; +ShuttleJobsDeleteCommand._endpoint = 'delete_shuttle_jobs_id'; + +ShuttleJobsDeleteCommand.flags = { + ...BoxCommand.flags, +}; + +ShuttleJobsDeleteCommand.args = { + id: Args.string({ + name: 'id', + required: true, + hidden: false, + description: 'ID of the shuttle job to delete', + }), +}; + +module.exports = ShuttleJobsDeleteCommand; diff --git a/src/commands/shuttle/jobs/get.js b/src/commands/shuttle/jobs/get.js new file mode 100644 index 00000000..1569a837 --- /dev/null +++ b/src/commands/shuttle/jobs/get.js @@ -0,0 +1,31 @@ +'use strict'; + +const { Args } = require('@oclif/core'); +const BoxCommand = require('../../../box-command'); + +class ShuttleJobsGetCommand extends BoxCommand { + async run() { + const { args } = await this.parse(ShuttleJobsGetCommand); + const job = await this.tsClient.shuttle.getShuttleJobByIdV2026R0(args.id); + await this.output(job.rawData ?? job); + } +} + +ShuttleJobsGetCommand.description = 'Get a shuttle job by ID'; +ShuttleJobsGetCommand.examples = ['box shuttle:jobs:get 6a315c0754833bfc5df067bf']; +ShuttleJobsGetCommand._endpoint = 'get_shuttle_jobs_id'; + +ShuttleJobsGetCommand.flags = { + ...BoxCommand.flags, +}; + +ShuttleJobsGetCommand.args = { + id: Args.string({ + name: 'id', + required: true, + hidden: false, + description: 'ID of the shuttle job', + }), +}; + +module.exports = ShuttleJobsGetCommand; diff --git a/src/commands/shuttle/jobs/index.js b/src/commands/shuttle/jobs/index.js new file mode 100644 index 00000000..29609cba --- /dev/null +++ b/src/commands/shuttle/jobs/index.js @@ -0,0 +1,26 @@ +'use strict'; + +const BoxCommand = require('../../../box-command'); +const PaginationUtilities = require('../../../pagination-utils'); + +class ShuttleJobsListCommand extends BoxCommand { + async run() { + await this.parse(ShuttleJobsListCommand); + const jobs = await this.markerPagination((pageQueryParams) => + this.tsClient.shuttle.getShuttleJobsV2026R0(pageQueryParams) + ); + await this.output(jobs); + } +} + +ShuttleJobsListCommand.aliases = ['shuttle:jobs:list']; +ShuttleJobsListCommand.description = 'List shuttle jobs'; +ShuttleJobsListCommand.examples = ['box shuttle:jobs']; +ShuttleJobsListCommand._endpoint = 'get_shuttle_jobs'; + +ShuttleJobsListCommand.flags = { + ...BoxCommand.flags, + ...PaginationUtilities.flags, +}; + +module.exports = ShuttleJobsListCommand; diff --git a/src/commands/shuttle/jobs/prepare-migration.js b/src/commands/shuttle/jobs/prepare-migration.js new file mode 100644 index 00000000..901ee542 --- /dev/null +++ b/src/commands/shuttle/jobs/prepare-migration.js @@ -0,0 +1,36 @@ +'use strict'; + +const { Args } = require('@oclif/core'); +const BoxCommand = require('../../../box-command'); + +class ShuttleJobsPrepareMigrationCommand extends BoxCommand { + async run() { + const { args } = await this.parse(ShuttleJobsPrepareMigrationCommand); + await this.tsClient.shuttle.createShuttleJobPrepareMigrationV2026R0( + args.id + ); + } +} + +ShuttleJobsPrepareMigrationCommand.description = + 'Prepare a shuttle job for migration'; +ShuttleJobsPrepareMigrationCommand.examples = [ + 'box shuttle:jobs:prepare-migration 6a315c0754833bfc5df067bf', +]; +ShuttleJobsPrepareMigrationCommand._endpoint = + 'post_shuttle_jobs_id_prepare_migration'; + +ShuttleJobsPrepareMigrationCommand.flags = { + ...BoxCommand.flags, +}; + +ShuttleJobsPrepareMigrationCommand.args = { + id: Args.string({ + name: 'id', + required: true, + hidden: false, + description: 'ID of the shuttle job', + }), +}; + +module.exports = ShuttleJobsPrepareMigrationCommand; diff --git a/src/commands/shuttle/jobs/start.js b/src/commands/shuttle/jobs/start.js new file mode 100644 index 00000000..dd58ec22 --- /dev/null +++ b/src/commands/shuttle/jobs/start.js @@ -0,0 +1,54 @@ +'use strict'; + +const BoxCommand = require('../../../box-command'); +const { Flags, Args } = require('@oclif/core'); +const { resolveBody, bodyFlags } = require('../../../modules/shuttle'); + +class ShuttleJobsStartCommand extends BoxCommand { + async run() { + const { flags, args } = await this.parse(ShuttleJobsStartCommand); + const requestBody = resolveBody(flags, () => { + const body = {}; + if (flags.mode !== undefined) { + body.mode = flags.mode; + } + return body; + }); + + const optionals = + Object.keys(requestBody).length > 0 + ? { requestBody } + : {}; + await this.tsClient.shuttle.createShuttleJobStartV2026R0( + args.id, + optionals + ); + } +} + +ShuttleJobsStartCommand.description = 'Start a shuttle job run'; +ShuttleJobsStartCommand.examples = [ + 'box shuttle:jobs:start 6a315c0754833bfc5df067bf', + 'box shuttle:jobs:start 6a315c0754833bfc5df067bf --mode transfer', +]; +ShuttleJobsStartCommand._endpoint = 'post_shuttle_jobs_id_start'; + +ShuttleJobsStartCommand.flags = { + ...BoxCommand.flags, + ...bodyFlags, + mode: Flags.string({ + description: 'Run mode (defaults to simulation when omitted)', + options: ['transfer', 'simulation', 'scan', 'analysis'], + }), +}; + +ShuttleJobsStartCommand.args = { + id: Args.string({ + name: 'id', + required: true, + hidden: false, + description: 'ID of the shuttle job to start', + }), +}; + +module.exports = ShuttleJobsStartCommand; diff --git a/src/commands/shuttle/jobs/stop.js b/src/commands/shuttle/jobs/stop.js new file mode 100644 index 00000000..ec40ad3d --- /dev/null +++ b/src/commands/shuttle/jobs/stop.js @@ -0,0 +1,32 @@ +'use strict'; + +const { Args } = require('@oclif/core'); +const BoxCommand = require('../../../box-command'); + +class ShuttleJobsStopCommand extends BoxCommand { + async run() { + const { args } = await this.parse(ShuttleJobsStopCommand); + await this.tsClient.shuttle.createShuttleJobStopV2026R0(args.id); + } +} + +ShuttleJobsStopCommand.description = 'Stop a running shuttle job'; +ShuttleJobsStopCommand.examples = [ + 'box shuttle:jobs:stop 6a315c0754833bfc5df067bf', +]; +ShuttleJobsStopCommand._endpoint = 'post_shuttle_jobs_id_stop'; + +ShuttleJobsStopCommand.flags = { + ...BoxCommand.flags, +}; + +ShuttleJobsStopCommand.args = { + id: Args.string({ + name: 'id', + required: true, + hidden: false, + description: 'ID of the shuttle job to stop', + }), +}; + +module.exports = ShuttleJobsStopCommand; diff --git a/src/commands/shuttle/jobs/update.js b/src/commands/shuttle/jobs/update.js new file mode 100644 index 00000000..54ef90b9 --- /dev/null +++ b/src/commands/shuttle/jobs/update.js @@ -0,0 +1,86 @@ +'use strict'; + +const BoxCommand = require('../../../box-command'); +const { Flags, Args } = require('@oclif/core'); +const { + resolveBody, + bodyFlags, + jobAccountMappingFlags, + buildAccountMapping, + jobSettingsFlags, + buildSettings, +} = require('../../../modules/shuttle'); + +class ShuttleJobsUpdateCommand extends BoxCommand { + async run() { + const { flags, args } = await this.parse(ShuttleJobsUpdateCommand); + const requestBody = resolveBody(flags, () => { + const body = {}; + if (flags.name !== undefined) { + body.name = flags.name; + } + if (flags.comment !== undefined) { + body.comment = flags.comment; + } + if (flags.tag) { + body.tags = flags.tag; + } + const accountMapping = buildAccountMapping(flags); + if (accountMapping) { + body.accountMapping = accountMapping; + } + const settings = buildSettings(flags); + if (settings) { + body.settings = settings; + } + if (Object.keys(body).length === 0) { + this.error( + 'Provide at least one update flag, or use --body / --body-file.' + ); + } + return body; + }); + + const job = await this.tsClient.shuttle.updateShuttleJobByIdV2026R0( + args.id, + requestBody + ); + await this.output(job.rawData ?? job); + } +} + +ShuttleJobsUpdateCommand.description = 'Update a shuttle job'; +ShuttleJobsUpdateCommand.examples = [ + 'box shuttle:jobs:update 6a315c0754833bfc5df067bf --name "Q4 Migration (revised)"', + 'box shuttle:jobs:update 6a315c0754833bfc5df067bf --account-mapping-enabled --no-are-permissions-mapped', +]; +ShuttleJobsUpdateCommand._endpoint = 'put_shuttle_jobs_id'; + +ShuttleJobsUpdateCommand.flags = { + ...BoxCommand.flags, + ...bodyFlags, + name: Flags.string({ + char: 'n', + description: 'Updated display name', + }), + comment: Flags.string({ + description: 'Updated comment', + }), + tag: Flags.string({ + description: 'Replacement tags (repeatable)', + multiple: true, + }), + ...jobAccountMappingFlags, + ...jobSettingsFlags, +}; + +ShuttleJobsUpdateCommand.args = { + id: Args.string({ + name: 'id', + required: true, + hidden: false, + description: 'ID of the shuttle job to update', + }), +}; + +module.exports = ShuttleJobsUpdateCommand; diff --git a/src/commands/shuttle/mapping-errors/index.js b/src/commands/shuttle/mapping-errors/index.js new file mode 100644 index 00000000..5b47aeb5 --- /dev/null +++ b/src/commands/shuttle/mapping-errors/index.js @@ -0,0 +1,35 @@ +'use strict'; + +const BoxCommand = require('../../../box-command'); +const { Flags } = require('@oclif/core'); +const PaginationUtilities = require('../../../pagination-utils'); + +class ShuttleMappingErrorsListCommand extends BoxCommand { + async run() { + const { flags } = await this.parse(ShuttleMappingErrorsListCommand); + const errors = await this.markerPagination( + (pageQueryParams) => + this.tsClient.shuttle.getShuttleMappingErrorsV2026R0(pageQueryParams), + { jobId: flags['job-id'] } + ); + await this.output(errors); + } +} + +ShuttleMappingErrorsListCommand.aliases = ['shuttle:mapping-errors:list']; +ShuttleMappingErrorsListCommand.description = 'List shuttle mapping errors'; +ShuttleMappingErrorsListCommand.examples = [ + 'box shuttle:mapping-errors --job-id 6a315c0754833bfc5df067bf', +]; +ShuttleMappingErrorsListCommand._endpoint = 'get_shuttle_mapping_errors'; + +ShuttleMappingErrorsListCommand.flags = { + ...BoxCommand.flags, + ...PaginationUtilities.flags, + 'job-id': Flags.string({ + description: 'Shuttle job ID whose mapping errors to list', + required: true, + }), +}; + +module.exports = ShuttleMappingErrorsListCommand; diff --git a/src/commands/shuttle/path-entries/index.js b/src/commands/shuttle/path-entries/index.js new file mode 100644 index 00000000..66a2e199 --- /dev/null +++ b/src/commands/shuttle/path-entries/index.js @@ -0,0 +1,66 @@ +'use strict'; + +const BoxCommand = require('../../../box-command'); +const { Flags } = require('@oclif/core'); +const PaginationUtilities = require('../../../pagination-utils'); + +class ShuttlePathEntriesListCommand extends BoxCommand { + async run() { + const { flags } = await this.parse(ShuttlePathEntriesListCommand); + if (!flags['job-id'] || !flags.side) { + this.error('--job-id and --side are required.'); + } + const query = { + jobId: flags['job-id'], + side: flags.side, + }; + if (flags.path !== undefined) { + query.path = flags.path; + } + if (flags['path-id'] !== undefined) { + query.pathId = flags['path-id']; + } + if (flags['account-email'] !== undefined) { + query.accountEmail = flags['account-email']; + } + const entries = await this.markerPagination( + (pageQueryParams) => + this.tsClient.shuttle.getShuttlePathEntriesV2026R0(pageQueryParams), + query + ); + await this.output(entries); + } +} + +ShuttlePathEntriesListCommand.aliases = ['shuttle:path-entries:list']; +ShuttlePathEntriesListCommand.description = + 'List shuttle path entries for a job side'; +ShuttlePathEntriesListCommand.examples = [ + 'box shuttle:path-entries --job-id 6a315c0754833bfc5df067bf --side source', +]; +ShuttlePathEntriesListCommand._endpoint = 'get_shuttle_path_entries'; + +ShuttlePathEntriesListCommand.flags = { + ...BoxCommand.flags, + ...PaginationUtilities.flags, + 'job-id': Flags.string({ + description: 'Shuttle job ID', + required: true, + }), + side: Flags.string({ + description: 'Which side of the migration to browse', + options: ['source', 'target'], + required: true, + }), + path: Flags.string({ + description: 'Filter by path', + }), + 'path-id': Flags.string({ + description: 'Filter by path ID', + }), + 'account-email': Flags.string({ + description: 'Filter by account email', + }), +}; + +module.exports = ShuttlePathEntriesListCommand; diff --git a/src/commands/shuttle/path-mapping-imports/create.js b/src/commands/shuttle/path-mapping-imports/create.js new file mode 100644 index 00000000..a31e06eb --- /dev/null +++ b/src/commands/shuttle/path-mapping-imports/create.js @@ -0,0 +1,50 @@ +'use strict'; + +const BoxCommand = require('../../../box-command'); +const { Flags } = require('@oclif/core'); +const { resolveBody, bodyFlags } = require('../../../modules/shuttle'); + +class ShuttlePathMappingImportsCreateCommand extends BoxCommand { + async run() { + const { flags } = await this.parse( + ShuttlePathMappingImportsCreateCommand + ); + const requestBody = resolveBody(flags, () => { + if (!flags['job-id'] || !flags['file-id']) { + this.error( + '--job-id and --file-id are required unless --body / --body-file is set.' + ); + } + return { + jobId: flags['job-id'], + fileId: flags['file-id'], + }; + }); + + await this.tsClient.shuttle.createShuttlePathMappingImportV2026R0( + requestBody + ); + } +} + +ShuttlePathMappingImportsCreateCommand.description = + 'Import shuttle path mappings from a Box spreadsheet file (async; empty response on accept)'; +ShuttlePathMappingImportsCreateCommand.examples = [ + 'box shuttle:path-mapping-imports:create --job-id 6a315c0754833bfc5df067bf --file-id 12345678901', +]; +ShuttlePathMappingImportsCreateCommand._endpoint = + 'post_shuttle_path_mapping_imports'; + +ShuttlePathMappingImportsCreateCommand.flags = { + ...BoxCommand.flags, + ...bodyFlags, + 'job-id': Flags.string({ + description: 'Shuttle job ID (required unless --body / --body-file)', + }), + 'file-id': Flags.string({ + description: + 'Box file ID of the mapping workbook (required unless --body / --body-file)', + }), +}; + +module.exports = ShuttlePathMappingImportsCreateCommand; diff --git a/src/commands/shuttle/path-mappings/create.js b/src/commands/shuttle/path-mappings/create.js new file mode 100644 index 00000000..d2fbfcf8 --- /dev/null +++ b/src/commands/shuttle/path-mappings/create.js @@ -0,0 +1,77 @@ +'use strict'; + +const BoxCommand = require('../../../box-command'); +const { Flags } = require('@oclif/core'); +const { + resolveBody, + bodyFlags, + pathLocationFlags, + buildPathLocation, +} = require('../../../modules/shuttle'); + +class ShuttlePathMappingsCreateCommand extends BoxCommand { + async run() { + const { flags } = await this.parse(ShuttlePathMappingsCreateCommand); + const requestBody = resolveBody(flags, () => { + if (!flags['job-id']) { + this.error( + '--job-id is required unless --body / --body-file is set.' + ); + } + const source = buildPathLocation(flags, 'source', { + required: true, + allowIsDirectory: true, + command: this, + }); + const body = { + jobId: flags['job-id'], + source, + }; + if (flags['is-path-skipped'] !== undefined) { + body.isPathSkipped = flags['is-path-skipped']; + } + if (flags['clear-target']) { + body.target = null; + } else { + const target = buildPathLocation(flags, 'target', { + allowIsDirectory: false, + command: this, + }); + if (target !== undefined) { + body.target = target; + } + } + return body; + }); + + const mapping = + await this.tsClient.shuttle.createShuttlePathMappingV2026R0(requestBody); + await this.output(mapping.rawData ?? mapping); + } +} + +ShuttlePathMappingsCreateCommand.description = 'Create a shuttle path mapping'; +ShuttlePathMappingsCreateCommand.examples = [ + 'box shuttle:path-mappings:create --job-id 6a315c0754833bfc5df067bf --source-type account --source-path "/Finance" --source-account-email admin@company.com --target-type account --target-path "/HR Migration" --target-account-email admin@company.box.com', +]; +ShuttlePathMappingsCreateCommand._endpoint = 'post_shuttle_path_mappings'; + +ShuttlePathMappingsCreateCommand.flags = { + ...BoxCommand.flags, + ...bodyFlags, + 'job-id': Flags.string({ + description: 'Shuttle job ID (required unless --body / --body-file)', + }), + 'is-path-skipped': Flags.boolean({ + description: 'Skip this source path during migration', + allowNo: true, + }), + 'clear-target': Flags.boolean({ + description: 'Set target to null (e.g. when skipping the path)', + default: false, + }), + ...pathLocationFlags('source', { allowIsDirectory: true }), + ...pathLocationFlags('target', { allowIsDirectory: false }), +}; + +module.exports = ShuttlePathMappingsCreateCommand; diff --git a/src/commands/shuttle/path-mappings/delete.js b/src/commands/shuttle/path-mappings/delete.js new file mode 100644 index 00000000..d1bc26ea --- /dev/null +++ b/src/commands/shuttle/path-mappings/delete.js @@ -0,0 +1,33 @@ +'use strict'; + +const { Args } = require('@oclif/core'); +const BoxCommand = require('../../../box-command'); + +class ShuttlePathMappingsDeleteCommand extends BoxCommand { + async run() { + const { args } = await this.parse(ShuttlePathMappingsDeleteCommand); + await this.tsClient.shuttle.deleteShuttlePathMappingByIdV2026R0(args.id); + this.info(`Deleted shuttle path mapping ${args.id}`); + } +} + +ShuttlePathMappingsDeleteCommand.description = 'Delete a shuttle path mapping'; +ShuttlePathMappingsDeleteCommand.examples = [ + 'box shuttle:path-mappings:delete 6a315c0754833bfc5df067bf', +]; +ShuttlePathMappingsDeleteCommand._endpoint = 'delete_shuttle_path_mappings_id'; + +ShuttlePathMappingsDeleteCommand.flags = { + ...BoxCommand.flags, +}; + +ShuttlePathMappingsDeleteCommand.args = { + id: Args.string({ + name: 'id', + required: true, + hidden: false, + description: 'ID of the shuttle path mapping to delete', + }), +}; + +module.exports = ShuttlePathMappingsDeleteCommand; diff --git a/src/commands/shuttle/path-mappings/get.js b/src/commands/shuttle/path-mappings/get.js new file mode 100644 index 00000000..0563e3e5 --- /dev/null +++ b/src/commands/shuttle/path-mappings/get.js @@ -0,0 +1,35 @@ +'use strict'; + +const { Args } = require('@oclif/core'); +const BoxCommand = require('../../../box-command'); + +class ShuttlePathMappingsGetCommand extends BoxCommand { + async run() { + const { args } = await this.parse(ShuttlePathMappingsGetCommand); + const mapping = await this.tsClient.shuttle.getShuttlePathMappingByIdV2026R0( + args.id + ); + await this.output(mapping.rawData ?? mapping); + } +} + +ShuttlePathMappingsGetCommand.description = 'Get a shuttle path mapping by ID'; +ShuttlePathMappingsGetCommand.examples = [ + 'box shuttle:path-mappings:get 6a315c0754833bfc5df067bf', +]; +ShuttlePathMappingsGetCommand._endpoint = 'get_shuttle_path_mappings_id'; + +ShuttlePathMappingsGetCommand.flags = { + ...BoxCommand.flags, +}; + +ShuttlePathMappingsGetCommand.args = { + id: Args.string({ + name: 'id', + required: true, + hidden: false, + description: 'ID of the shuttle path mapping', + }), +}; + +module.exports = ShuttlePathMappingsGetCommand; diff --git a/src/commands/shuttle/path-mappings/index.js b/src/commands/shuttle/path-mappings/index.js new file mode 100644 index 00000000..163ffb4b --- /dev/null +++ b/src/commands/shuttle/path-mappings/index.js @@ -0,0 +1,35 @@ +'use strict'; + +const BoxCommand = require('../../../box-command'); +const { Flags } = require('@oclif/core'); +const PaginationUtilities = require('../../../pagination-utils'); + +class ShuttlePathMappingsListCommand extends BoxCommand { + async run() { + const { flags } = await this.parse(ShuttlePathMappingsListCommand); + const mappings = await this.markerPagination( + (pageQueryParams) => + this.tsClient.shuttle.getShuttlePathMappingsV2026R0(pageQueryParams), + { jobId: flags['job-id'] } + ); + await this.output(mappings); + } +} + +ShuttlePathMappingsListCommand.aliases = ['shuttle:path-mappings:list']; +ShuttlePathMappingsListCommand.description = 'List shuttle path mappings'; +ShuttlePathMappingsListCommand.examples = [ + 'box shuttle:path-mappings --job-id 6a315c0754833bfc5df067bf', +]; +ShuttlePathMappingsListCommand._endpoint = 'get_shuttle_path_mappings'; + +ShuttlePathMappingsListCommand.flags = { + ...BoxCommand.flags, + ...PaginationUtilities.flags, + 'job-id': Flags.string({ + description: 'Shuttle job ID whose path mappings to list', + required: true, + }), +}; + +module.exports = ShuttlePathMappingsListCommand; diff --git a/src/commands/shuttle/path-mappings/update.js b/src/commands/shuttle/path-mappings/update.js new file mode 100644 index 00000000..3360e2a8 --- /dev/null +++ b/src/commands/shuttle/path-mappings/update.js @@ -0,0 +1,85 @@ +'use strict'; + +const BoxCommand = require('../../../box-command'); +const { Flags, Args } = require('@oclif/core'); +const { + resolveBody, + bodyFlags, + pathLocationFlags, + buildPathLocation, +} = require('../../../modules/shuttle'); + +class ShuttlePathMappingsUpdateCommand extends BoxCommand { + async run() { + const { flags, args } = await this.parse(ShuttlePathMappingsUpdateCommand); + const requestBody = resolveBody(flags, () => { + const body = {}; + if (flags['is-path-skipped'] !== undefined) { + body.isPathSkipped = flags['is-path-skipped']; + } + const source = buildPathLocation(flags, 'source', { + allowIsDirectory: true, + command: this, + }); + if (source !== undefined) { + body.source = source; + } + if (flags['clear-target']) { + body.target = null; + } else { + const target = buildPathLocation(flags, 'target', { + allowIsDirectory: false, + command: this, + }); + if (target !== undefined) { + body.target = target; + } + } + if (Object.keys(body).length === 0) { + this.error( + 'Provide at least one update flag, or use --body / --body-file.' + ); + } + return body; + }); + + const mapping = await this.tsClient.shuttle.updateShuttlePathMappingByIdV2026R0( + args.id, + requestBody + ); + await this.output(mapping.rawData ?? mapping); + } +} + +ShuttlePathMappingsUpdateCommand.description = 'Update a shuttle path mapping'; +ShuttlePathMappingsUpdateCommand.examples = [ + 'box shuttle:path-mappings:update 6a315c0754833bfc5df067bf --is-path-skipped --clear-target', + 'box shuttle:path-mappings:update 6a315c0754833bfc5df067bf --target-type account --target-path "/HR" --target-account-email admin@company.box.com', +]; +ShuttlePathMappingsUpdateCommand._endpoint = 'put_shuttle_path_mappings_id'; + +ShuttlePathMappingsUpdateCommand.flags = { + ...BoxCommand.flags, + ...bodyFlags, + 'is-path-skipped': Flags.boolean({ + description: 'Skip this source path during migration', + allowNo: true, + }), + 'clear-target': Flags.boolean({ + description: 'Set target to null', + default: false, + }), + ...pathLocationFlags('source', { allowIsDirectory: true }), + ...pathLocationFlags('target', { allowIsDirectory: false }), +}; + +ShuttlePathMappingsUpdateCommand.args = { + id: Args.string({ + name: 'id', + required: true, + hidden: false, + description: 'ID of the shuttle path mapping to update', + }), +}; + +module.exports = ShuttlePathMappingsUpdateCommand; diff --git a/src/commands/shuttle/reports/create.js b/src/commands/shuttle/reports/create.js new file mode 100644 index 00000000..85aa3c4c --- /dev/null +++ b/src/commands/shuttle/reports/create.js @@ -0,0 +1,60 @@ +'use strict'; + +const BoxCommand = require('../../../box-command'); +const { Flags } = require('@oclif/core'); +const { resolveBody, bodyFlags } = require('../../../modules/shuttle'); + +class ShuttleReportsCreateCommand extends BoxCommand { + async run() { + const { flags } = await this.parse(ShuttleReportsCreateCommand); + const requestBody = resolveBody(flags, () => { + if (!flags['run-id'] || !flags.format || !flags.section) { + this.error( + '--run-id, --format, and at least one --section are required unless --body / --body-file is set.' + ); + } + const body = { + runId: flags['run-id'], + format: flags.format, + sections: flags.section, + }; + if (flags['users-scope'] !== undefined) { + body.usersScope = flags['users-scope']; + } + return body; + }); + + const report = await this.tsClient.shuttle.createShuttleReportV2026R0(requestBody); + await this.output(report.rawData ?? report); + } +} + +ShuttleReportsCreateCommand.description = + 'Create (generate) a shuttle report for a run'; +ShuttleReportsCreateCommand.examples = [ + 'box shuttle:reports:create --run-id 6a315c0754833bfc5df067bf:42 --format xlsx --section event_log --section transfer_overview', +]; +ShuttleReportsCreateCommand._endpoint = 'post_shuttle_reports'; + +ShuttleReportsCreateCommand.flags = { + ...BoxCommand.flags, + ...bodyFlags, + 'run-id': Flags.string({ + description: 'Shuttle run ID (required unless --body / --body-file)', + }), + format: Flags.string({ + description: 'Report format (required unless --body / --body-file)', + options: ['xlsx', 'csv'], + }), + section: Flags.string({ + description: + 'Report section to include (repeatable; required unless --body / --body-file)', + multiple: true, + }), + 'users-scope': Flags.string({ + description: 'Users scope for user-related sections', + options: ['migrated', 'all'], + }), +}; + +module.exports = ShuttleReportsCreateCommand; diff --git a/src/commands/shuttle/reports/get.js b/src/commands/shuttle/reports/get.js new file mode 100644 index 00000000..a9799ef3 --- /dev/null +++ b/src/commands/shuttle/reports/get.js @@ -0,0 +1,33 @@ +'use strict'; + +const { Args } = require('@oclif/core'); +const BoxCommand = require('../../../box-command'); + +class ShuttleReportsGetCommand extends BoxCommand { + async run() { + const { args } = await this.parse(ShuttleReportsGetCommand); + const report = await this.tsClient.shuttle.getShuttleReportByIdV2026R0(args.id); + await this.output(report.rawData ?? report); + } +} + +ShuttleReportsGetCommand.description = 'Get a shuttle report by ID'; +ShuttleReportsGetCommand.examples = [ + 'box shuttle:reports:get 6a315c0754833bfc5df067bf', +]; +ShuttleReportsGetCommand._endpoint = 'get_shuttle_reports_id'; + +ShuttleReportsGetCommand.flags = { + ...BoxCommand.flags, +}; + +ShuttleReportsGetCommand.args = { + id: Args.string({ + name: 'id', + required: true, + hidden: false, + description: 'ID of the shuttle report', + }), +}; + +module.exports = ShuttleReportsGetCommand; diff --git a/src/commands/shuttle/reports/index.js b/src/commands/shuttle/reports/index.js new file mode 100644 index 00000000..42b80969 --- /dev/null +++ b/src/commands/shuttle/reports/index.js @@ -0,0 +1,35 @@ +'use strict'; + +const BoxCommand = require('../../../box-command'); +const { Flags } = require('@oclif/core'); +const PaginationUtilities = require('../../../pagination-utils'); + +class ShuttleReportsListCommand extends BoxCommand { + async run() { + const { flags } = await this.parse(ShuttleReportsListCommand); + const reports = await this.markerPagination( + (pageQueryParams) => + this.tsClient.shuttle.getShuttleReportsV2026R0(pageQueryParams), + { runId: flags['run-id'] } + ); + await this.output(reports); + } +} + +ShuttleReportsListCommand.aliases = ['shuttle:reports:list']; +ShuttleReportsListCommand.description = 'List shuttle reports'; +ShuttleReportsListCommand.examples = [ + 'box shuttle:reports --run-id 6a315c0754833bfc5df067bf:42', +]; +ShuttleReportsListCommand._endpoint = 'get_shuttle_reports'; + +ShuttleReportsListCommand.flags = { + ...BoxCommand.flags, + ...PaginationUtilities.flags, + 'run-id': Flags.string({ + description: 'Shuttle run ID whose reports to list', + required: true, + }), +}; + +module.exports = ShuttleReportsListCommand; diff --git a/src/commands/shuttle/runs/get.js b/src/commands/shuttle/runs/get.js new file mode 100644 index 00000000..8c4719a6 --- /dev/null +++ b/src/commands/shuttle/runs/get.js @@ -0,0 +1,33 @@ +'use strict'; + +const { Args } = require('@oclif/core'); +const BoxCommand = require('../../../box-command'); + +class ShuttleRunsGetCommand extends BoxCommand { + async run() { + const { args } = await this.parse(ShuttleRunsGetCommand); + const run = await this.tsClient.shuttle.getShuttleRunByIdV2026R0(args.id); + await this.output(run.rawData ?? run); + } +} + +ShuttleRunsGetCommand.description = 'Get a shuttle run by ID'; +ShuttleRunsGetCommand.examples = [ + 'box shuttle:runs:get 6a315c0754833bfc5df067bf:42', +]; +ShuttleRunsGetCommand._endpoint = 'get_shuttle_runs_id'; + +ShuttleRunsGetCommand.flags = { + ...BoxCommand.flags, +}; + +ShuttleRunsGetCommand.args = { + id: Args.string({ + name: 'id', + required: true, + hidden: false, + description: 'ID of the shuttle run', + }), +}; + +module.exports = ShuttleRunsGetCommand; diff --git a/src/commands/shuttle/runs/index.js b/src/commands/shuttle/runs/index.js new file mode 100644 index 00000000..79e17ffe --- /dev/null +++ b/src/commands/shuttle/runs/index.js @@ -0,0 +1,34 @@ +'use strict'; + +const BoxCommand = require('../../../box-command'); +const { Flags } = require('@oclif/core'); +const PaginationUtilities = require('../../../pagination-utils'); + +class ShuttleRunsListCommand extends BoxCommand { + async run() { + const { flags } = await this.parse(ShuttleRunsListCommand); + const runs = await this.markerPagination( + (pageQueryParams) => this.tsClient.shuttle.getShuttleRunsV2026R0(pageQueryParams), + { jobId: flags['job-id'] } + ); + await this.output(runs); + } +} + +ShuttleRunsListCommand.aliases = ['shuttle:runs:list']; +ShuttleRunsListCommand.description = 'List shuttle runs'; +ShuttleRunsListCommand.examples = [ + 'box shuttle:runs --job-id 6a315c0754833bfc5df067bf', +]; +ShuttleRunsListCommand._endpoint = 'get_shuttle_runs'; + +ShuttleRunsListCommand.flags = { + ...BoxCommand.flags, + ...PaginationUtilities.flags, + 'job-id': Flags.string({ + description: 'Shuttle job ID whose runs to list', + required: true, + }), +}; + +module.exports = ShuttleRunsListCommand; diff --git a/src/commands/shuttle/subject-mapping-imports/create.js b/src/commands/shuttle/subject-mapping-imports/create.js new file mode 100644 index 00000000..03fb914a --- /dev/null +++ b/src/commands/shuttle/subject-mapping-imports/create.js @@ -0,0 +1,50 @@ +'use strict'; + +const BoxCommand = require('../../../box-command'); +const { Flags } = require('@oclif/core'); +const { resolveBody, bodyFlags } = require('../../../modules/shuttle'); + +class ShuttleSubjectMappingImportsCreateCommand extends BoxCommand { + async run() { + const { flags } = await this.parse( + ShuttleSubjectMappingImportsCreateCommand + ); + const requestBody = resolveBody(flags, () => { + if (!flags['job-id'] || !flags['file-id']) { + this.error( + '--job-id and --file-id are required unless --body / --body-file is set.' + ); + } + return { + jobId: flags['job-id'], + fileId: flags['file-id'], + }; + }); + + await this.tsClient.shuttle.createShuttleSubjectMappingImportV2026R0( + requestBody + ); + } +} + +ShuttleSubjectMappingImportsCreateCommand.description = + 'Import shuttle subject mappings from a Box spreadsheet file (async; empty response on accept)'; +ShuttleSubjectMappingImportsCreateCommand.examples = [ + 'box shuttle:subject-mapping-imports:create --job-id 6a315c0754833bfc5df067bf --file-id 12345678901', +]; +ShuttleSubjectMappingImportsCreateCommand._endpoint = + 'post_shuttle_subject_mapping_imports'; + +ShuttleSubjectMappingImportsCreateCommand.flags = { + ...BoxCommand.flags, + ...bodyFlags, + 'job-id': Flags.string({ + description: 'Shuttle job ID (required unless --body / --body-file)', + }), + 'file-id': Flags.string({ + description: + 'Box file ID of the mapping workbook (required unless --body / --body-file)', + }), +}; + +module.exports = ShuttleSubjectMappingImportsCreateCommand; diff --git a/src/commands/shuttle/subject-mappings/create.js b/src/commands/shuttle/subject-mappings/create.js new file mode 100644 index 00000000..0ee52d3e --- /dev/null +++ b/src/commands/shuttle/subject-mappings/create.js @@ -0,0 +1,72 @@ +'use strict'; + +const BoxCommand = require('../../../box-command'); +const { Flags } = require('@oclif/core'); +const { + resolveBody, + bodyFlags, + subjectTargetFlags, + buildSubjectTarget, +} = require('../../../modules/shuttle'); + +class ShuttleSubjectMappingsCreateCommand extends BoxCommand { + async run() { + const { flags } = await this.parse(ShuttleSubjectMappingsCreateCommand); + const requestBody = resolveBody(flags, () => { + if (!flags['job-id'] || !flags['discovered-subject-id']) { + this.error( + '--job-id and --discovered-subject-id are required unless --body / --body-file is set.' + ); + } + const body = { + jobId: flags['job-id'], + discoveredSubjectId: flags['discovered-subject-id'], + }; + if (flags.revision !== undefined) { + body.revision = flags.revision; + } + if (flags['is-subject-skipped'] !== undefined) { + body.isSubjectSkipped = flags['is-subject-skipped']; + } + const target = buildSubjectTarget(flags, { command: this }); + if (target !== undefined) { + body.target = target; + } + return body; + }); + + const mapping = + await this.tsClient.shuttle.createShuttleSubjectMappingV2026R0(requestBody); + await this.output(mapping.rawData ?? mapping); + } +} + +ShuttleSubjectMappingsCreateCommand.description = + 'Create a shuttle subject mapping'; +ShuttleSubjectMappingsCreateCommand.examples = [ + 'box shuttle:subject-mappings:create --job-id 6a315c0754833bfc5df067bf --discovered-subject-id 6a3ba7c02ea17bc9461eeacf --target-type user --target-id 12345678', + 'box shuttle:subject-mappings:create --job-id 6a315c0754833bfc5df067bf --discovered-subject-id 6a3ba7c02ea17bc9461eeacf --is-subject-skipped --clear-target', +]; +ShuttleSubjectMappingsCreateCommand._endpoint = 'post_shuttle_subject_mappings'; + +ShuttleSubjectMappingsCreateCommand.flags = { + ...BoxCommand.flags, + ...bodyFlags, + 'job-id': Flags.string({ + description: 'Shuttle job ID (required unless --body / --body-file)', + }), + 'discovered-subject-id': Flags.string({ + description: + 'Discovered subject ID (required unless --body / --body-file)', + }), + revision: Flags.integer({ + description: 'Discovery-requirements revision to resolve against', + }), + 'is-subject-skipped': Flags.boolean({ + description: 'Skip this subject during migration', + allowNo: true, + }), + ...subjectTargetFlags, +}; + +module.exports = ShuttleSubjectMappingsCreateCommand; diff --git a/src/commands/shuttle/subject-mappings/delete.js b/src/commands/shuttle/subject-mappings/delete.js new file mode 100644 index 00000000..402e0b8a --- /dev/null +++ b/src/commands/shuttle/subject-mappings/delete.js @@ -0,0 +1,35 @@ +'use strict'; + +const { Args } = require('@oclif/core'); +const BoxCommand = require('../../../box-command'); + +class ShuttleSubjectMappingsDeleteCommand extends BoxCommand { + async run() { + const { args } = await this.parse(ShuttleSubjectMappingsDeleteCommand); + await this.tsClient.shuttle.deleteShuttleSubjectMappingByIdV2026R0(args.id); + this.info(`Deleted shuttle subject mapping ${args.id}`); + } +} + +ShuttleSubjectMappingsDeleteCommand.description = + 'Delete a shuttle subject mapping'; +ShuttleSubjectMappingsDeleteCommand.examples = [ + 'box shuttle:subject-mappings:delete 6a315c0754833bfc5df067bf', +]; +ShuttleSubjectMappingsDeleteCommand._endpoint = + 'delete_shuttle_subject_mappings_id'; + +ShuttleSubjectMappingsDeleteCommand.flags = { + ...BoxCommand.flags, +}; + +ShuttleSubjectMappingsDeleteCommand.args = { + id: Args.string({ + name: 'id', + required: true, + hidden: false, + description: 'ID of the shuttle subject mapping to delete', + }), +}; + +module.exports = ShuttleSubjectMappingsDeleteCommand; diff --git a/src/commands/shuttle/subject-mappings/get.js b/src/commands/shuttle/subject-mappings/get.js new file mode 100644 index 00000000..4b663dc9 --- /dev/null +++ b/src/commands/shuttle/subject-mappings/get.js @@ -0,0 +1,36 @@ +'use strict'; + +const { Args } = require('@oclif/core'); +const BoxCommand = require('../../../box-command'); + +class ShuttleSubjectMappingsGetCommand extends BoxCommand { + async run() { + const { args } = await this.parse(ShuttleSubjectMappingsGetCommand); + const mapping = await this.tsClient.shuttle.getShuttleSubjectMappingByIdV2026R0( + args.id + ); + await this.output(mapping.rawData ?? mapping); + } +} + +ShuttleSubjectMappingsGetCommand.description = + 'Get a shuttle subject mapping by ID'; +ShuttleSubjectMappingsGetCommand.examples = [ + 'box shuttle:subject-mappings:get 6a315c0754833bfc5df067bf', +]; +ShuttleSubjectMappingsGetCommand._endpoint = 'get_shuttle_subject_mappings_id'; + +ShuttleSubjectMappingsGetCommand.flags = { + ...BoxCommand.flags, +}; + +ShuttleSubjectMappingsGetCommand.args = { + id: Args.string({ + name: 'id', + required: true, + hidden: false, + description: 'ID of the shuttle subject mapping', + }), +}; + +module.exports = ShuttleSubjectMappingsGetCommand; diff --git a/src/commands/shuttle/subject-mappings/index.js b/src/commands/shuttle/subject-mappings/index.js new file mode 100644 index 00000000..6c6ebafe --- /dev/null +++ b/src/commands/shuttle/subject-mappings/index.js @@ -0,0 +1,35 @@ +'use strict'; + +const BoxCommand = require('../../../box-command'); +const { Flags } = require('@oclif/core'); +const PaginationUtilities = require('../../../pagination-utils'); + +class ShuttleSubjectMappingsListCommand extends BoxCommand { + async run() { + const { flags } = await this.parse(ShuttleSubjectMappingsListCommand); + const mappings = await this.markerPagination( + (pageQueryParams) => + this.tsClient.shuttle.getShuttleSubjectMappingsV2026R0(pageQueryParams), + { jobId: flags['job-id'] } + ); + await this.output(mappings); + } +} + +ShuttleSubjectMappingsListCommand.aliases = ['shuttle:subject-mappings:list']; +ShuttleSubjectMappingsListCommand.description = 'List shuttle subject mappings'; +ShuttleSubjectMappingsListCommand.examples = [ + 'box shuttle:subject-mappings --job-id 6a315c0754833bfc5df067bf', +]; +ShuttleSubjectMappingsListCommand._endpoint = 'get_shuttle_subject_mappings'; + +ShuttleSubjectMappingsListCommand.flags = { + ...BoxCommand.flags, + ...PaginationUtilities.flags, + 'job-id': Flags.string({ + description: 'Shuttle job ID whose subject mappings to list', + required: true, + }), +}; + +module.exports = ShuttleSubjectMappingsListCommand; diff --git a/src/commands/shuttle/subject-mappings/update.js b/src/commands/shuttle/subject-mappings/update.js new file mode 100644 index 00000000..0ffa93ca --- /dev/null +++ b/src/commands/shuttle/subject-mappings/update.js @@ -0,0 +1,70 @@ +'use strict'; + +const BoxCommand = require('../../../box-command'); +const { Flags, Args } = require('@oclif/core'); +const { + resolveBody, + bodyFlags, + subjectTargetFlags, + buildSubjectTarget, +} = require('../../../modules/shuttle'); + +class ShuttleSubjectMappingsUpdateCommand extends BoxCommand { + async run() { + const { flags, args } = await this.parse( + ShuttleSubjectMappingsUpdateCommand + ); + const requestBody = resolveBody(flags, () => { + const body = {}; + if (flags['is-subject-skipped'] !== undefined) { + body.isSubjectSkipped = flags['is-subject-skipped']; + } + const target = buildSubjectTarget(flags, { command: this }); + if (target !== undefined) { + body.target = target; + } + if (Object.keys(body).length === 0) { + this.error( + 'Provide at least one update flag, or use --body / --body-file.' + ); + } + return body; + }); + + const mapping = await this.tsClient.shuttle.updateShuttleSubjectMappingByIdV2026R0( + args.id, + requestBody + ); + await this.output(mapping.rawData ?? mapping); + } +} + +ShuttleSubjectMappingsUpdateCommand.description = + 'Update a shuttle subject mapping'; +ShuttleSubjectMappingsUpdateCommand.examples = [ + 'box shuttle:subject-mappings:update 6a315c0754833bfc5df067bf --target-type user --target-id 12345678', + 'box shuttle:subject-mappings:update 6a315c0754833bfc5df067bf --is-subject-skipped --clear-target', +]; +ShuttleSubjectMappingsUpdateCommand._endpoint = + 'put_shuttle_subject_mappings_id'; + +ShuttleSubjectMappingsUpdateCommand.flags = { + ...BoxCommand.flags, + ...bodyFlags, + 'is-subject-skipped': Flags.boolean({ + description: 'Skip this subject during migration', + allowNo: true, + }), + ...subjectTargetFlags, +}; + +ShuttleSubjectMappingsUpdateCommand.args = { + id: Args.string({ + name: 'id', + required: true, + hidden: false, + description: 'ID of the shuttle subject mapping to update', + }), +}; + +module.exports = ShuttleSubjectMappingsUpdateCommand; diff --git a/src/commands/shuttle/systems/create.js b/src/commands/shuttle/systems/create.js new file mode 100644 index 00000000..b5e3a6c2 --- /dev/null +++ b/src/commands/shuttle/systems/create.js @@ -0,0 +1,44 @@ +'use strict'; + +const BoxCommand = require('../../../box-command'); +const { Flags, Args } = require('@oclif/core'); + +class ShuttleSystemsCreateCommand extends BoxCommand { + async run() { + const { flags, args } = await this.parse(ShuttleSystemsCreateCommand); + const requestBody = { + name: args.name, + systemType: flags['system-type'], + }; + + const system = await this.tsClient.shuttle.createShuttleSystemV2026R0(requestBody); + await this.output(system.rawData ?? system); + } +} + +ShuttleSystemsCreateCommand.description = + 'Create a shuttle system (Windows agent systems only via public API)'; +ShuttleSystemsCreateCommand.examples = [ + 'box shuttle:systems:create "NYC File Server" --system-type windows', +]; +ShuttleSystemsCreateCommand._endpoint = 'post_shuttle_systems'; + +ShuttleSystemsCreateCommand.flags = { + ...BoxCommand.flags, + 'system-type': Flags.string({ + description: 'System type. Only windows is supported through the public API.', + options: ['windows'], + required: true, + }), +}; + +ShuttleSystemsCreateCommand.args = { + name: Args.string({ + name: 'name', + required: true, + hidden: false, + description: 'Display name for the new shuttle system', + }), +}; + +module.exports = ShuttleSystemsCreateCommand; diff --git a/src/commands/shuttle/systems/delete.js b/src/commands/shuttle/systems/delete.js new file mode 100644 index 00000000..80ff1391 --- /dev/null +++ b/src/commands/shuttle/systems/delete.js @@ -0,0 +1,33 @@ +'use strict'; + +const { Args } = require('@oclif/core'); +const BoxCommand = require('../../../box-command'); + +class ShuttleSystemsDeleteCommand extends BoxCommand { + async run() { + const { args } = await this.parse(ShuttleSystemsDeleteCommand); + await this.tsClient.shuttle.deleteShuttleSystemByIdV2026R0(args.id); + this.info(`Deleted shuttle system ${args.id}`); + } +} + +ShuttleSystemsDeleteCommand.description = 'Delete a shuttle system'; +ShuttleSystemsDeleteCommand.examples = [ + 'box shuttle:systems:delete 6a315c0754833bfc5df067bf', +]; +ShuttleSystemsDeleteCommand._endpoint = 'delete_shuttle_systems_id'; + +ShuttleSystemsDeleteCommand.flags = { + ...BoxCommand.flags, +}; + +ShuttleSystemsDeleteCommand.args = { + id: Args.string({ + name: 'id', + required: true, + hidden: false, + description: 'ID of the shuttle system to delete', + }), +}; + +module.exports = ShuttleSystemsDeleteCommand; diff --git a/src/commands/shuttle/systems/get.js b/src/commands/shuttle/systems/get.js new file mode 100644 index 00000000..fa9476ea --- /dev/null +++ b/src/commands/shuttle/systems/get.js @@ -0,0 +1,31 @@ +'use strict'; + +const { Args } = require('@oclif/core'); +const BoxCommand = require('../../../box-command'); + +class ShuttleSystemsGetCommand extends BoxCommand { + async run() { + const { args } = await this.parse(ShuttleSystemsGetCommand); + const system = await this.tsClient.shuttle.getShuttleSystemByIdV2026R0(args.id); + await this.output(system.rawData ?? system); + } +} + +ShuttleSystemsGetCommand.description = 'Get a shuttle system by ID'; +ShuttleSystemsGetCommand.examples = ['box shuttle:systems:get 6a315c0754833bfc5df067bf']; +ShuttleSystemsGetCommand._endpoint = 'get_shuttle_systems_id'; + +ShuttleSystemsGetCommand.flags = { + ...BoxCommand.flags, +}; + +ShuttleSystemsGetCommand.args = { + id: Args.string({ + name: 'id', + required: true, + hidden: false, + description: 'ID of the shuttle system', + }), +}; + +module.exports = ShuttleSystemsGetCommand; diff --git a/src/commands/shuttle/systems/index.js b/src/commands/shuttle/systems/index.js new file mode 100644 index 00000000..2d2211a9 --- /dev/null +++ b/src/commands/shuttle/systems/index.js @@ -0,0 +1,26 @@ +'use strict'; + +const BoxCommand = require('../../../box-command'); +const PaginationUtilities = require('../../../pagination-utils'); + +class ShuttleSystemsListCommand extends BoxCommand { + async run() { + await this.parse(ShuttleSystemsListCommand); + const systems = await this.markerPagination((pageQueryParams) => + this.tsClient.shuttle.getShuttleSystemsV2026R0(pageQueryParams) + ); + await this.output(systems); + } +} + +ShuttleSystemsListCommand.aliases = ['shuttle:systems:list']; +ShuttleSystemsListCommand.description = 'List shuttle systems'; +ShuttleSystemsListCommand.examples = ['box shuttle:systems', 'box shuttle:systems --max-items 50']; +ShuttleSystemsListCommand._endpoint = 'get_shuttle_systems'; + +ShuttleSystemsListCommand.flags = { + ...BoxCommand.flags, + ...PaginationUtilities.flags, +}; + +module.exports = ShuttleSystemsListCommand; diff --git a/src/commands/shuttle/systems/issue-registration-token.js b/src/commands/shuttle/systems/issue-registration-token.js new file mode 100644 index 00000000..77f0a62f --- /dev/null +++ b/src/commands/shuttle/systems/issue-registration-token.js @@ -0,0 +1,38 @@ +'use strict'; + +const { Args } = require('@oclif/core'); +const BoxCommand = require('../../../box-command'); + +class ShuttleSystemsIssueRegistrationTokenCommand extends BoxCommand { + async run() { + const { args } = await this.parse( + ShuttleSystemsIssueRegistrationTokenCommand + ); + const credentials = + await this.tsClient.shuttle.createShuttleSystemIssueRegistrationTokenV2026R0(args.id); + await this.output(credentials.rawData ?? credentials); + } +} + +ShuttleSystemsIssueRegistrationTokenCommand.description = + 'Issue a registration token for a Windows shuttle agent system'; +ShuttleSystemsIssueRegistrationTokenCommand.examples = [ + 'box shuttle:systems:issue-registration-token 6a315c0754833bfc5df067bf', +]; +ShuttleSystemsIssueRegistrationTokenCommand._endpoint = + 'post_shuttle_systems_id_issue_registration_token'; + +ShuttleSystemsIssueRegistrationTokenCommand.flags = { + ...BoxCommand.flags, +}; + +ShuttleSystemsIssueRegistrationTokenCommand.args = { + id: Args.string({ + name: 'id', + required: true, + hidden: false, + description: 'ID of the shuttle system', + }), +}; + +module.exports = ShuttleSystemsIssueRegistrationTokenCommand; diff --git a/src/commands/shuttle/systems/update.js b/src/commands/shuttle/systems/update.js new file mode 100644 index 00000000..99490040 --- /dev/null +++ b/src/commands/shuttle/systems/update.js @@ -0,0 +1,49 @@ +'use strict'; + +const BoxCommand = require('../../../box-command'); +const { Flags, Args } = require('@oclif/core'); + +class ShuttleSystemsUpdateCommand extends BoxCommand { + async run() { + const { flags, args } = await this.parse(ShuttleSystemsUpdateCommand); + const requestBody = {}; + + if (flags.name) { + requestBody.name = flags.name; + } + if (Object.keys(requestBody).length === 0) { + this.error('Please provide at least one update flag.'); + } + + const system = await this.tsClient.shuttle.updateShuttleSystemByIdV2026R0( + args.id, + requestBody + ); + await this.output(system.rawData ?? system); + } +} + +ShuttleSystemsUpdateCommand.description = 'Update a shuttle system'; +ShuttleSystemsUpdateCommand.examples = [ + 'box shuttle:systems:update 6a315c0754833bfc5df067bf --name "NYC File Server (Primary)"', +]; +ShuttleSystemsUpdateCommand._endpoint = 'put_shuttle_systems_id'; + +ShuttleSystemsUpdateCommand.flags = { + ...BoxCommand.flags, + name: Flags.string({ + char: 'n', + description: 'Updated display name for the shuttle system', + }), +}; + +ShuttleSystemsUpdateCommand.args = { + id: Args.string({ + name: 'id', + required: true, + hidden: false, + description: 'ID of the shuttle system to update', + }), +}; + +module.exports = ShuttleSystemsUpdateCommand; diff --git a/src/modules/shuttle.js b/src/modules/shuttle.js new file mode 100644 index 00000000..6a337f38 --- /dev/null +++ b/src/modules/shuttle.js @@ -0,0 +1,455 @@ +'use strict'; + +/** + * Shared helpers for Shuttle CLI commands: oclif flag defs and sdk-gen request + * body builders (camelCase). Not a client wrapper — commands call + * `this.tsClient.shuttle.*` directly (same pattern as hubs). + */ + +const fs = require('node:fs'); +const { Flags } = require('@oclif/core'); + +function parseJsonValue(raw, flagName) { + if (raw === undefined || raw === null || raw === '') { + return undefined; + } + try { + return JSON.parse(raw); + } catch (error) { + throw new Error(`Invalid JSON for --${flagName}: ${error.message}`); + } +} + +/** + * Convert wire-format (snake_case) JSON keys to sdk-gen camelCase. + * Safe for already-camelCase input (keys without `_` are unchanged). + */ +function snakeToCamelKey(key) { + return key.replace(/_([a-z0-9])/g, (_, ch) => ch.toUpperCase()); +} + +function snakeToCamel(value) { + if (Array.isArray(value)) { + return value.map(snakeToCamel); + } + if ( + value && + typeof value === 'object' && + Object.getPrototypeOf(value) === Object.prototype + ) { + const out = {}; + for (const [key, child] of Object.entries(value)) { + out[snakeToCamelKey(key)] = snakeToCamel(child); + } + return out; + } + return value; +} + +function readBodyOverride(flags) { + if (flags.body !== undefined) { + const parsed = parseJsonValue(flags.body, 'body'); + if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) { + throw new Error('--body must be a JSON object'); + } + return snakeToCamel(parsed); + } + if (flags['body-file']) { + const content = fs.readFileSync(flags['body-file'], 'utf8'); + const parsed = parseJsonValue(content, 'body-file'); + if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) { + throw new Error('--body-file must contain a JSON object'); + } + return snakeToCamel(parsed); + } + return null; +} + +function resolveBody(flags, buildFromFlags) { + const override = readBodyOverride(flags); + if (override) { + return override; + } + return buildFromFlags(); +} + +const bodyFlags = Object.freeze({ + body: Flags.string({ + description: + 'Escape hatch: full JSON request body (snake_case or camelCase). When set, other body-building flags are ignored.', + }), + 'body-file': Flags.string({ + description: + 'Escape hatch: path to a JSON file used as the full request body.', + exclusive: ['body'], + }), +}); + +function hasAny(flags, names) { + return names.some((name) => flags[name] !== undefined); +} + +/** + * Flat flags for a path-mapping location (source or target). + * @param {'source'|'target'} prefix + * @param {{ allowIsDirectory?: boolean }} [options] + */ +function pathLocationFlags(prefix, options = {}) { + const { allowIsDirectory = false } = options; + const flags = { + [`${prefix}-type`]: Flags.string({ + description: `${prefix} location type`, + options: ['account', 'shared_space'], + }), + [`${prefix}-path`]: Flags.string({ + description: `${prefix} path`, + }), + [`${prefix}-path-id`]: Flags.string({ + description: `${prefix} path ID from browsing`, + }), + [`${prefix}-account-id`]: Flags.string({ + description: `${prefix} account id (account type)`, + }), + [`${prefix}-account-email`]: Flags.string({ + description: `${prefix} account email (account type)`, + }), + [`${prefix}-space`]: Flags.string({ + description: `${prefix} shared container name (shared_space type)`, + }), + }; + if (allowIsDirectory) { + flags[`${prefix}-is-directory`] = Flags.boolean({ + description: `Whether the ${prefix} path is a directory`, + allowNo: true, + }); + } + return flags; +} + +/** + * Build a path location object (sdk-gen camelCase), or undefined if none set. + * @param {Object} flags + * @param {'source'|'target'} prefix + * @param {{ required?: boolean, allowIsDirectory?: boolean, command?: { error: Function } }} [options] + */ +function buildPathLocation(flags, prefix, options = {}) { + const { required = false, allowIsDirectory = false, command } = options; + const typeKey = `${prefix}-type`; + const pathKey = `${prefix}-path`; + const pathIdKey = `${prefix}-path-id`; + const accountIdKey = `${prefix}-account-id`; + const accountEmailKey = `${prefix}-account-email`; + const spaceKey = `${prefix}-space`; + const isDirectoryKey = `${prefix}-is-directory`; + + const relevant = [ + typeKey, + pathKey, + pathIdKey, + accountIdKey, + accountEmailKey, + spaceKey, + ]; + if (allowIsDirectory) { + relevant.push(isDirectoryKey); + } + + if (!hasAny(flags, relevant)) { + if (required && command) { + command.error( + `--${typeKey} and --${pathKey} are required (or pass --body / --body-file).` + ); + } + return undefined; + } + + const type = flags[typeKey]; + const path = flags[pathKey]; + if (!type || !path) { + if (command) { + command.error( + `--${typeKey} and --${pathKey} are required when setting a ${prefix} location.` + ); + } + throw new Error(`Missing --${typeKey} or --${pathKey}`); + } + + const location = { type, path }; + if (flags[pathIdKey] !== undefined) { + location.pathId = flags[pathIdKey]; + } + if (allowIsDirectory && flags[isDirectoryKey] !== undefined) { + location.isDirectory = flags[isDirectoryKey]; + } + + if (type === 'account') { + const account = {}; + if (flags[accountIdKey] !== undefined) { + account.id = flags[accountIdKey]; + } + if (flags[accountEmailKey] !== undefined) { + account.email = flags[accountEmailKey]; + } + if (Object.keys(account).length === 0) { + if (command) { + command.error( + `--${accountIdKey} and/or --${accountEmailKey} are required when --${typeKey}=account.` + ); + } + throw new Error(`Missing account fields for ${prefix}`); + } + location.account = account; + } else if (type === 'shared_space') { + if (flags[spaceKey] !== undefined) { + location.space = flags[spaceKey]; + } + } + + return location; +} + +const subjectTargetFlags = Object.freeze({ + 'target-type': Flags.string({ + description: 'Box target subject type', + options: ['user', 'group'], + }), + 'target-id': Flags.string({ + description: 'Box target subject ID', + }), + 'clear-target': Flags.boolean({ + description: 'Set target to null (e.g. when skipping the subject)', + default: false, + }), +}); + +function buildSubjectTarget(flags, options = {}) { + const { command } = options; + if (flags['clear-target']) { + return null; + } + if (flags['target-type'] === undefined && flags['target-id'] === undefined) { + return undefined; + } + if (!flags['target-type'] || !flags['target-id']) { + if (command) { + command.error( + '--target-type and --target-id are required together (or use --clear-target).' + ); + } + throw new Error('Incomplete subject target flags'); + } + return { + type: flags['target-type'], + id: flags['target-id'], + }; +} + +const scheduleTriggerFlags = Object.freeze({ + 'trigger-type': Flags.string({ + description: 'Schedule trigger type', + options: ['by_time', 'by_job'], + }), + 'start-at': Flags.string({ + description: + 'Local start time for by_time trigger (YYYY-MM-DDTHH:MM:SS, no Z)', + }), + 'time-zone': Flags.string({ + description: 'IANA time zone for by_time trigger', + }), + repeat: Flags.string({ + description: 'Repeat cadence for by_time trigger', + options: ['none', 'daily', 'weekly', 'weekdays', 'weekends'], + }), + 'triggered-by-job-id': Flags.string({ + description: 'Triggering job ID for by_job trigger', + }), + 'last-run-condition': Flags.string({ + description: 'Condition on the triggering job last run', + options: ['always', 'on_success', 'on_finish'], + }), +}); + +function buildScheduleTrigger(flags, options = {}) { + const { required = false, command } = options; + const relevant = [ + 'trigger-type', + 'start-at', + 'time-zone', + 'repeat', + 'triggered-by-job-id', + 'last-run-condition', + ]; + if (!hasAny(flags, relevant)) { + if (required && command) { + command.error( + '--trigger-type and matching trigger fields are required (or pass --body / --body-file).' + ); + } + return undefined; + } + + const type = flags['trigger-type']; + if (!type) { + if (command) { + command.error('--trigger-type is required when setting a trigger.'); + } + throw new Error('Missing --trigger-type'); + } + + if (type === 'by_time') { + if (!flags['start-at'] || !flags['time-zone'] || !flags.repeat) { + if (command) { + command.error( + '--start-at, --time-zone, and --repeat are required for --trigger-type=by_time.' + ); + } + throw new Error('Incomplete by_time trigger'); + } + return { + type: 'by_time', + startAt: flags['start-at'], + timeZone: flags['time-zone'], + repeat: flags.repeat, + }; + } + + if (type === 'by_job') { + if (!flags['triggered-by-job-id'] || !flags['last-run-condition']) { + if (command) { + command.error( + '--triggered-by-job-id and --last-run-condition are required for --trigger-type=by_job.' + ); + } + throw new Error('Incomplete by_job trigger'); + } + return { + type: 'by_job', + triggeredByJobId: flags['triggered-by-job-id'], + lastRunCondition: flags['last-run-condition'], + }; + } + + if (command) { + command.error(`Unsupported --trigger-type: ${type}`); + } + throw new Error(`Unsupported trigger type: ${type}`); +} + +const jobAccountMappingFlags = Object.freeze({ + 'account-mapping-enabled': Flags.boolean({ + description: 'Enable account mapping for the job', + allowNo: true, + }), + 'permission-conflict-strategy': Flags.string({ + description: 'Permission conflict strategy', + options: [ + 'preserve_structure_restrict_sharing', + 'preserve_structure_expand_sharing', + 'preserve_sharing_modify_structure', + 'warn_permissions_conflicts', + ], + }), + 'are-external-shares-mapped': Flags.boolean({ + description: 'Map external shares', + allowNo: true, + }), + 'are-permissions-mapped': Flags.boolean({ + description: 'Map permissions', + allowNo: true, + }), +}); + +function buildAccountMapping(flags) { + const body = {}; + if (flags['account-mapping-enabled'] !== undefined) { + body.isEnabled = flags['account-mapping-enabled']; + } + if (flags['permission-conflict-strategy'] !== undefined) { + body.permissionConflictStrategy = + flags['permission-conflict-strategy']; + } + if (flags['are-external-shares-mapped'] !== undefined) { + body.areExternalSharesMapped = flags['are-external-shares-mapped']; + } + if (flags['are-permissions-mapped'] !== undefined) { + body.arePermissionsMapped = flags['are-permissions-mapped']; + } + return Object.keys(body).length > 0 ? body : undefined; +} + +const jobSettingsFlags = Object.freeze({ + 'bandwidth-limit-bytes-per-sec': Flags.integer({ + description: 'Max transfer bandwidth in bytes/sec', + }), + 'are-deletions-mirrored': Flags.boolean({ + description: 'Mirror source deletions in Box', + allowNo: true, + }), + 'are-file-versions-synced': Flags.boolean({ + description: 'Sync prior file versions', + allowNo: true, + }), + 'max-file-versions': Flags.integer({ + description: 'Max file versions to sync', + }), + 'is-overwrite-enabled': Flags.boolean({ + description: 'Overwrite existing files in Box', + allowNo: true, + }), + 'are-completed-mappings-skipped-on-rerun': Flags.boolean({ + description: 'Skip completed mappings on rerun', + allowNo: true, + }), + 'is-content-scanning-enabled': Flags.boolean({ + description: 'Enable content scanning when supported', + allowNo: true, + }), + 'include-shared-source-folders': Flags.boolean({ + description: 'Include shared folders on the source', + allowNo: true, + }), + 'include-shared-destination-folders': Flags.boolean({ + description: 'Include shared folders on the destination', + allowNo: true, + }), +}); + +function buildSettings(flags) { + const map = { + 'bandwidth-limit-bytes-per-sec': 'bandwidthLimitBytesPerSec', + 'are-deletions-mirrored': 'areDeletionsMirrored', + 'are-file-versions-synced': 'areFileVersionsSynced', + 'max-file-versions': 'maxFileVersions', + 'is-overwrite-enabled': 'isOverwriteEnabled', + 'are-completed-mappings-skipped-on-rerun': + 'areCompletedMappingsSkippedOnRerun', + 'is-content-scanning-enabled': 'isContentScanningEnabled', + 'include-shared-source-folders': 'includeSharedSourceFolders', + 'include-shared-destination-folders': 'includeSharedDestinationFolders', + }; + const body = {}; + for (const [flagName, apiName] of Object.entries(map)) { + if (flags[flagName] !== undefined) { + body[apiName] = flags[flagName]; + } + } + return Object.keys(body).length > 0 ? body : undefined; +} + +module.exports = { + parseJsonValue, + snakeToCamel, + readBodyOverride, + resolveBody, + bodyFlags, + pathLocationFlags, + buildPathLocation, + subjectTargetFlags, + buildSubjectTarget, + scheduleTriggerFlags, + buildScheduleTrigger, + jobAccountMappingFlags, + buildAccountMapping, + jobSettingsFlags, + buildSettings, +}; diff --git a/test/commands/shuttle.test.js b/test/commands/shuttle.test.js new file mode 100644 index 00000000..cf5daa30 --- /dev/null +++ b/test/commands/shuttle.test.js @@ -0,0 +1,1040 @@ +'use strict'; + +const { test } = require('@oclif/test'); +const { assert } = require('chai'); +const os = require('node:os'); +const { TEST_API_ROOT } = require('../helpers/test-helper'); + +describe('Shuttle', function () { + const jobId = '6a315c0754833bfc5df067bf'; + const systemId = '6a3ba68e810d9156194530a1'; + const scheduleId = '6a3ba68e810d9156194530b2'; + const pathMappingId = '6a3ba68e810d9156194530c3'; + const subjectMappingId = '6a3ba68e810d9156194530d4'; + const discoveredSubjectId = '6a3ba7c02ea17bc9461eeacf'; + const runId = `${jobId}:42`; + const reportId = '6a3ba68e810d9156194530e5'; + const fileId = '12345678901'; + + const system = { + id: systemId, + type: 'shuttle_system', + name: 'NYC File Server', + system_type: 'windows', + state: 'connected', + }; + + const job = { + id: jobId, + type: 'shuttle_job', + name: 'Q4 Migration', + job_type: 'transfer', + state: 'draft', + mapping_state: 'incomplete', + is_enabled: true, + source_system: { + type: 'shuttle_system', + id: systemId, + name: 'NYC File Server', + }, + }; + + const scheduleTrigger = { + type: 'by_time', + start_at: '2026-03-16T09:00:00', + time_zone: 'America/Los_Angeles', + repeat: 'daily', + }; + + const schedule = { + id: scheduleId, + type: 'shuttle_job_schedule', + job_id: jobId, + run_mode: 'transfer', + state: 'enabled', + trigger: scheduleTrigger, + }; + + const pathMapping = { + id: pathMappingId, + type: 'shuttle_path_mapping', + job_id: jobId, + is_path_skipped: false, + source: { + type: 'account', + path: '/Finance', + account: { + email: 'admin@company.com', + }, + }, + target: { + type: 'account', + path: '/HR Migration', + account: { + email: 'admin@company.box.com', + }, + }, + }; + + const subjectMapping = { + id: subjectMappingId, + type: 'shuttle_subject_mapping', + job_id: jobId, + discovered_subject_id: discoveredSubjectId, + is_subject_skipped: false, + source: { + type: 'user', + email: 'alice@company.com', + name: 'Alice', + }, + target: { + type: 'user', + id: '12345678', + }, + }; + + const run = { + id: runId, + type: 'shuttle_run', + job_id: jobId, + run_mode: 'transfer', + state: 'completed', + }; + + const report = { + id: reportId, + type: 'shuttle_report', + run_id: runId, + status: 'ready', + format: 'xlsx', + }; + + const mappingError = { + id: 'err1', + type: 'shuttle_mapping_error', + job_id: jobId, + revision: 1, + category: 'path_import', + path: '/Finance', + messages: ['Row 2: Invalid path'], + }; + + const discoveredSubject = { + id: discoveredSubjectId, + type: 'shuttle_discovered_subject', + job_id: jobId, + revision: 1, + source: { + type: 'user', + email: 'alice@company.com', + name: 'Alice', + }, + }; + + const pathEntries = { + entries: [ + { + id: 'pe1', + type: 'shuttle_path_entry', + name: 'Finance', + path: '/Finance', + is_directory: true, + can_map: true, + scope: { + type: 'account', + email: 'admin@company.com', + }, + }, + ], + limit: 100, + }; + + /* ------------------------------------------------------------------ */ + /* systems */ + /* ------------------------------------------------------------------ */ + + describe('shuttle:systems', function () { + test.nock(TEST_API_ROOT, (api) => + api + .get('/2.0/shuttle_systems') + .query({ limit: 10 }) + .matchHeader('box-version', '2026.0') + .reply(200, { entries: [system], limit: 10 }) + ) + .stdout() + .command(['shuttle:systems', '--max-items=10', '--json', '--token=test']) + .it('lists shuttle systems', (context) => { + assert.deepEqual(JSON.parse(context.stdout), [system]); + }); + }); + + describe('shuttle:systems:get', function () { + test.nock(TEST_API_ROOT, (api) => + api + .get(`/2.0/shuttle_systems/${systemId}`) + .matchHeader('box-version', '2026.0') + .reply(200, system) + ) + .stdout() + .command(['shuttle:systems:get', systemId, '--json', '--token=test']) + .it('gets a shuttle system by id', (context) => { + assert.deepEqual(JSON.parse(context.stdout), system); + }); + }); + + describe('shuttle:systems:create', function () { + test.nock(TEST_API_ROOT, (api) => + api + .post('/2.0/shuttle_systems', { + name: 'NYC File Server', + system_type: 'windows', + }) + .matchHeader('box-version', '2026.0') + .reply(201, system) + ) + .stdout() + .command([ + 'shuttle:systems:create', + 'NYC File Server', + '--system-type=windows', + '--json', + '--token=test', + ]) + .it('creates a windows shuttle system', (context) => { + assert.deepEqual(JSON.parse(context.stdout), system); + }); + }); + + describe('shuttle:systems:update', function () { + const updated = { ...system, name: 'NYC File Server (Primary)' }; + + test.nock(TEST_API_ROOT, (api) => + api + .put(`/2.0/shuttle_systems/${systemId}`, { + name: 'NYC File Server (Primary)', + }) + .matchHeader('box-version', '2026.0') + .reply(200, updated) + ) + .stdout() + .command([ + 'shuttle:systems:update', + systemId, + '--name=NYC File Server (Primary)', + '--json', + '--token=test', + ]) + .it('updates a shuttle system name', (context) => { + assert.deepEqual(JSON.parse(context.stdout), updated); + }); + }); + + describe('shuttle:systems:delete', function () { + test.nock(TEST_API_ROOT, (api) => + api + .delete(`/2.0/shuttle_systems/${systemId}`) + .matchHeader('box-version', '2026.0') + .reply(204) + ) + .stderr() + .command(['shuttle:systems:delete', systemId, '--token=test']) + .it('deletes a shuttle system', (context) => { + assert.equal( + context.stderr, + `Deleted shuttle system ${systemId}${os.EOL}` + ); + }); + }); + + describe('shuttle:systems:issue-registration-token', function () { + const credentials = { + type: 'shuttle_agent_credentials', + registration_token: 'reg-token-xyz', + expires_at: '2026-03-16T10:00:00Z', + }; + + test.nock(TEST_API_ROOT, (api) => + api + .post(`/2.0/shuttle_systems/${systemId}/issue_registration_token`) + .matchHeader('box-version', '2026.0') + .reply(201, credentials) + ) + .stdout() + .command([ + 'shuttle:systems:issue-registration-token', + systemId, + '--json', + '--token=test', + ]) + .it('issues a Windows agent registration token', (context) => { + assert.deepEqual(JSON.parse(context.stdout), credentials); + }); + }); + + /* ------------------------------------------------------------------ */ + /* jobs */ + /* ------------------------------------------------------------------ */ + + describe('shuttle:jobs', function () { + test.nock(TEST_API_ROOT, (api) => + api + .get('/2.0/shuttle_jobs') + .query({ limit: 10 }) + .matchHeader('box-version', '2026.0') + .reply(200, { entries: [job], limit: 10 }) + ) + .stdout() + .command(['shuttle:jobs', '--max-items=10', '--json', '--token=test']) + .it('lists shuttle jobs', (context) => { + assert.deepEqual(JSON.parse(context.stdout), [job]); + }); + }); + + describe('shuttle:jobs:get', function () { + test.nock(TEST_API_ROOT, (api) => + api + .get(`/2.0/shuttle_jobs/${jobId}`) + .matchHeader('box-version', '2026.0') + .reply(200, job) + ) + .stdout() + .command(['shuttle:jobs:get', jobId, '--json', '--token=test']) + .it('gets a shuttle job by id', (context) => { + assert.deepEqual(JSON.parse(context.stdout), job); + }); + }); + + describe('shuttle:jobs:create', function () { + test.nock(TEST_API_ROOT, (api) => + api + .post('/2.0/shuttle_jobs', { + name: 'Q4 Migration', + job_type: 'transfer', + source_system_id: systemId, + account_mapping: { + is_enabled: true, + are_permissions_mapped: true, + }, + settings: { + is_overwrite_enabled: true, + }, + }) + .matchHeader('box-version', '2026.0') + .reply(201, job) + ) + .stdout() + .command([ + 'shuttle:jobs:create', + 'Q4 Migration', + '--job-type=transfer', + `--source-system-id=${systemId}`, + '--account-mapping-enabled', + '--are-permissions-mapped', + '--is-overwrite-enabled', + '--json', + '--token=test', + ]) + .it('creates a shuttle job with camelCase→wire body', (context) => { + assert.deepEqual(JSON.parse(context.stdout), job); + }); + }); + + describe('shuttle:jobs:create --body', function () { + test.nock(TEST_API_ROOT, (api) => + api + .post('/2.0/shuttle_jobs', { + name: 'Body Job', + job_type: 'transfer', + source_system_id: systemId, + }) + .matchHeader('box-version', '2026.0') + .reply(201, { ...job, name: 'Body Job' }) + ) + .stdout() + .command([ + 'shuttle:jobs:create', + 'ignored-when-body-set', + `--body={"name":"Body Job","job_type":"transfer","source_system_id":"${systemId}"}`, + '--json', + '--token=test', + ]) + .it('accepts snake_case --body and sends the wire payload', (context) => { + assert.equal(JSON.parse(context.stdout).name, 'Body Job'); + }); + }); + + describe('shuttle:jobs:update', function () { + const updated = { ...job, name: 'Q4 Migration (revised)' }; + + test.nock(TEST_API_ROOT, (api) => + api + .put(`/2.0/shuttle_jobs/${jobId}`, { + name: 'Q4 Migration (revised)', + }) + .matchHeader('box-version', '2026.0') + .reply(200, updated) + ) + .stdout() + .command([ + 'shuttle:jobs:update', + jobId, + '--name=Q4 Migration (revised)', + '--json', + '--token=test', + ]) + .it('updates a shuttle job name', (context) => { + assert.deepEqual(JSON.parse(context.stdout), updated); + }); + }); + + describe('shuttle:jobs:delete', function () { + test.nock(TEST_API_ROOT, (api) => + api + .delete(`/2.0/shuttle_jobs/${jobId}`) + .matchHeader('box-version', '2026.0') + .reply(204) + ) + .stderr() + .command(['shuttle:jobs:delete', jobId, '--token=test']) + .it('deletes a shuttle job', (context) => { + assert.equal(context.stderr, `Deleted shuttle job ${jobId}${os.EOL}`); + }); + }); + + describe('shuttle:jobs:start', function () { + test.nock(TEST_API_ROOT, (api) => + api + .post(`/2.0/shuttle_jobs/${jobId}/start`, { + mode: 'transfer', + }) + .matchHeader('box-version', '2026.0') + .reply(204) + ) + .stdout() + .command([ + 'shuttle:jobs:start', + jobId, + '--mode=transfer', + '--token=test', + ]) + .it('starts a shuttle job run', (context) => { + assert.equal(context.stdout, ''); + }); + }); + + describe('shuttle:jobs:stop', function () { + test.nock(TEST_API_ROOT, (api) => + api + .post(`/2.0/shuttle_jobs/${jobId}/stop`) + .matchHeader('box-version', '2026.0') + .reply(204) + ) + .stdout() + .command(['shuttle:jobs:stop', jobId, '--token=test']) + .it('stops a shuttle job', (context) => { + assert.equal(context.stdout, ''); + }); + }); + + describe('shuttle:jobs:prepare-migration', function () { + test.nock(TEST_API_ROOT, (api) => + api + .post(`/2.0/shuttle_jobs/${jobId}/prepare_migration`) + .matchHeader('box-version', '2026.0') + .reply(204) + ) + .stdout() + .command(['shuttle:jobs:prepare-migration', jobId, '--token=test']) + .it('prepares a shuttle job for migration', (context) => { + assert.equal(context.stdout, ''); + }); + }); + + /* ------------------------------------------------------------------ */ + /* job schedules */ + /* ------------------------------------------------------------------ */ + + describe('shuttle:job-schedules', function () { + test.nock(TEST_API_ROOT, (api) => + api + .get('/2.0/shuttle_job_schedules') + .query({ job_id: jobId, limit: 10 }) + .matchHeader('box-version', '2026.0') + .reply(200, { entries: [schedule], limit: 10 }) + ) + .stdout() + .command([ + 'shuttle:job-schedules', + `--job-id=${jobId}`, + '--max-items=10', + '--json', + '--token=test', + ]) + .it('lists shuttle job schedules', (context) => { + assert.deepEqual(JSON.parse(context.stdout), [schedule]); + }); + }); + + describe('shuttle:job-schedules:get', function () { + test.nock(TEST_API_ROOT, (api) => + api + .get(`/2.0/shuttle_job_schedules/${scheduleId}`) + .matchHeader('box-version', '2026.0') + .reply(200, schedule) + ) + .stdout() + .command([ + 'shuttle:job-schedules:get', + scheduleId, + '--json', + '--token=test', + ]) + .it('gets a shuttle job schedule by id', (context) => { + assert.deepEqual(JSON.parse(context.stdout), schedule); + }); + }); + + describe('shuttle:job-schedules:create', function () { + test.nock(TEST_API_ROOT, (api) => + api + .post('/2.0/shuttle_job_schedules', { + job_id: jobId, + run_mode: 'transfer', + trigger: scheduleTrigger, + }) + .matchHeader('box-version', '2026.0') + .reply(201, schedule) + ) + .stdout() + .command([ + 'shuttle:job-schedules:create', + `--job-id=${jobId}`, + '--run-mode=transfer', + '--trigger-type=by_time', + '--start-at=2026-03-16T09:00:00', + '--time-zone=America/Los_Angeles', + '--repeat=daily', + '--json', + '--token=test', + ]) + .it('creates a by_time job schedule from flat flags', (context) => { + assert.deepEqual(JSON.parse(context.stdout), schedule); + }); + }); + + describe('shuttle:job-schedules:update', function () { + const updated = { ...schedule, state: 'paused' }; + + test.nock(TEST_API_ROOT, (api) => + api + .put(`/2.0/shuttle_job_schedules/${scheduleId}`, { + state: 'paused', + }) + .matchHeader('box-version', '2026.0') + .reply(200, updated) + ) + .stdout() + .command([ + 'shuttle:job-schedules:update', + scheduleId, + '--state=paused', + '--json', + '--token=test', + ]) + .it('updates a shuttle job schedule state', (context) => { + assert.deepEqual(JSON.parse(context.stdout), updated); + }); + }); + + describe('shuttle:job-schedules:delete', function () { + test.nock(TEST_API_ROOT, (api) => + api + .delete(`/2.0/shuttle_job_schedules/${scheduleId}`) + .matchHeader('box-version', '2026.0') + .reply(204) + ) + .stderr() + .command(['shuttle:job-schedules:delete', scheduleId, '--token=test']) + .it('deletes a shuttle job schedule', (context) => { + assert.equal( + context.stderr, + `Deleted shuttle job schedule ${scheduleId}${os.EOL}` + ); + }); + }); + + /* ------------------------------------------------------------------ */ + /* runs */ + /* ------------------------------------------------------------------ */ + + describe('shuttle:runs', function () { + test.nock(TEST_API_ROOT, (api) => + api + .get('/2.0/shuttle_runs') + .query({ job_id: jobId, limit: 10 }) + .matchHeader('box-version', '2026.0') + .reply(200, { entries: [run], limit: 10 }) + ) + .stdout() + .command([ + 'shuttle:runs', + `--job-id=${jobId}`, + '--max-items=10', + '--json', + '--token=test', + ]) + .it('lists shuttle runs', (context) => { + assert.deepEqual(JSON.parse(context.stdout), [run]); + }); + }); + + describe('shuttle:runs:get', function () { + test.nock(TEST_API_ROOT, (api) => + api + .get(`/2.0/shuttle_runs/${runId}`) + .matchHeader('box-version', '2026.0') + .reply(200, run) + ) + .stdout() + .command(['shuttle:runs:get', runId, '--json', '--token=test']) + .it('gets a shuttle run by id', (context) => { + assert.deepEqual(JSON.parse(context.stdout), run); + }); + }); + + /* ------------------------------------------------------------------ */ + /* reports */ + /* ------------------------------------------------------------------ */ + + describe('shuttle:reports', function () { + test.nock(TEST_API_ROOT, (api) => + api + .get('/2.0/shuttle_reports') + .query({ run_id: runId, limit: 10 }) + .matchHeader('box-version', '2026.0') + .reply(200, { entries: [report], limit: 10 }) + ) + .stdout() + .command([ + 'shuttle:reports', + `--run-id=${runId}`, + '--max-items=10', + '--json', + '--token=test', + ]) + .it('lists shuttle reports', (context) => { + assert.deepEqual(JSON.parse(context.stdout), [report]); + }); + }); + + describe('shuttle:reports:get', function () { + test.nock(TEST_API_ROOT, (api) => + api + .get(`/2.0/shuttle_reports/${reportId}`) + .matchHeader('box-version', '2026.0') + .reply(200, report) + ) + .stdout() + .command(['shuttle:reports:get', reportId, '--json', '--token=test']) + .it('gets a shuttle report by id', (context) => { + assert.deepEqual(JSON.parse(context.stdout), report); + }); + }); + + describe('shuttle:reports:create', function () { + test.nock(TEST_API_ROOT, (api) => + api + .post('/2.0/shuttle_reports', { + run_id: runId, + format: 'xlsx', + sections: ['event_log', 'transfer_overview'], + }) + .matchHeader('box-version', '2026.0') + .reply(201, report) + ) + .stdout() + .command([ + 'shuttle:reports:create', + `--run-id=${runId}`, + '--format=xlsx', + '--section=event_log', + '--section=transfer_overview', + '--json', + '--token=test', + ]) + .it('creates a shuttle report', (context) => { + assert.deepEqual(JSON.parse(context.stdout), report); + }); + }); + + /* ------------------------------------------------------------------ */ + /* path mappings */ + /* ------------------------------------------------------------------ */ + + describe('shuttle:path-mappings', function () { + test.nock(TEST_API_ROOT, (api) => + api + .get('/2.0/shuttle_path_mappings') + .query({ job_id: jobId, limit: 10 }) + .matchHeader('box-version', '2026.0') + .reply(200, { entries: [pathMapping], limit: 10 }) + ) + .stdout() + .command([ + 'shuttle:path-mappings', + `--job-id=${jobId}`, + '--max-items=10', + '--json', + '--token=test', + ]) + .it('lists shuttle path mappings', (context) => { + assert.deepEqual(JSON.parse(context.stdout), [pathMapping]); + }); + }); + + describe('shuttle:path-mappings:get', function () { + test.nock(TEST_API_ROOT, (api) => + api + .get(`/2.0/shuttle_path_mappings/${pathMappingId}`) + .matchHeader('box-version', '2026.0') + .reply(200, pathMapping) + ) + .stdout() + .command([ + 'shuttle:path-mappings:get', + pathMappingId, + '--json', + '--token=test', + ]) + .it('gets a shuttle path mapping by id', (context) => { + assert.deepEqual(JSON.parse(context.stdout), pathMapping); + }); + }); + + describe('shuttle:path-mappings:create', function () { + test.nock(TEST_API_ROOT, (api) => + api + .post('/2.0/shuttle_path_mappings', { + job_id: jobId, + source: { + type: 'account', + path: '/Finance', + account: { + email: 'admin@company.com', + }, + }, + target: { + type: 'account', + path: '/HR Migration', + account: { + email: 'admin@company.box.com', + }, + }, + }) + .matchHeader('box-version', '2026.0') + .reply(201, pathMapping) + ) + .stdout() + .command([ + 'shuttle:path-mappings:create', + `--job-id=${jobId}`, + '--source-type=account', + '--source-path=/Finance', + '--source-account-email=admin@company.com', + '--target-type=account', + '--target-path=/HR Migration', + '--target-account-email=admin@company.box.com', + '--json', + '--token=test', + ]) + .it('creates a path mapping from flat flags', (context) => { + assert.deepEqual(JSON.parse(context.stdout), pathMapping); + }); + }); + + describe('shuttle:path-mappings:update', function () { + const updated = { ...pathMapping, is_path_skipped: true, target: null }; + + test.nock(TEST_API_ROOT, (api) => + api + .put(`/2.0/shuttle_path_mappings/${pathMappingId}`, { + is_path_skipped: true, + target: null, + }) + .matchHeader('box-version', '2026.0') + .reply(200, updated) + ) + .stdout() + .command([ + 'shuttle:path-mappings:update', + pathMappingId, + '--is-path-skipped', + '--clear-target', + '--json', + '--token=test', + ]) + .it('updates a path mapping skip/clear-target', (context) => { + assert.deepEqual(JSON.parse(context.stdout), updated); + }); + }); + + describe('shuttle:path-mappings:delete', function () { + test.nock(TEST_API_ROOT, (api) => + api + .delete(`/2.0/shuttle_path_mappings/${pathMappingId}`) + .matchHeader('box-version', '2026.0') + .reply(204) + ) + .stderr() + .command(['shuttle:path-mappings:delete', pathMappingId, '--token=test']) + .it('deletes a shuttle path mapping', (context) => { + assert.equal( + context.stderr, + `Deleted shuttle path mapping ${pathMappingId}${os.EOL}` + ); + }); + }); + + describe('shuttle:path-mapping-imports:create', function () { + test.nock(TEST_API_ROOT, (api) => + api + .post('/2.0/shuttle_path_mapping_imports', { + job_id: jobId, + file_id: fileId, + }) + .matchHeader('box-version', '2026.0') + .reply(204) + ) + .stdout() + .command([ + 'shuttle:path-mapping-imports:create', + `--job-id=${jobId}`, + `--file-id=${fileId}`, + '--token=test', + ]) + .it('starts a path-mapping spreadsheet import (no content)', (context) => { + assert.equal(context.stdout, ''); + }); + }); + + /* ------------------------------------------------------------------ */ + /* subject mappings */ + /* ------------------------------------------------------------------ */ + + describe('shuttle:subject-mappings', function () { + test.nock(TEST_API_ROOT, (api) => + api + .get('/2.0/shuttle_subject_mappings') + .query({ job_id: jobId, limit: 10 }) + .matchHeader('box-version', '2026.0') + .reply(200, { entries: [subjectMapping], limit: 10 }) + ) + .stdout() + .command([ + 'shuttle:subject-mappings', + `--job-id=${jobId}`, + '--max-items=10', + '--json', + '--token=test', + ]) + .it('lists shuttle subject mappings', (context) => { + assert.deepEqual(JSON.parse(context.stdout), [subjectMapping]); + }); + }); + + describe('shuttle:subject-mappings:get', function () { + test.nock(TEST_API_ROOT, (api) => + api + .get(`/2.0/shuttle_subject_mappings/${subjectMappingId}`) + .matchHeader('box-version', '2026.0') + .reply(200, subjectMapping) + ) + .stdout() + .command([ + 'shuttle:subject-mappings:get', + subjectMappingId, + '--json', + '--token=test', + ]) + .it('gets a shuttle subject mapping by id', (context) => { + assert.deepEqual(JSON.parse(context.stdout), subjectMapping); + }); + }); + + describe('shuttle:subject-mappings:create', function () { + test.nock(TEST_API_ROOT, (api) => + api + .post('/2.0/shuttle_subject_mappings', { + job_id: jobId, + discovered_subject_id: discoveredSubjectId, + target: { + type: 'user', + id: '12345678', + }, + }) + .matchHeader('box-version', '2026.0') + .reply(201, subjectMapping) + ) + .stdout() + .command([ + 'shuttle:subject-mappings:create', + `--job-id=${jobId}`, + `--discovered-subject-id=${discoveredSubjectId}`, + '--target-type=user', + '--target-id=12345678', + '--json', + '--token=test', + ]) + .it('creates a subject mapping from flat flags', (context) => { + assert.deepEqual(JSON.parse(context.stdout), subjectMapping); + }); + }); + + describe('shuttle:subject-mappings:update', function () { + const updated = { + ...subjectMapping, + is_subject_skipped: true, + target: null, + }; + + test.nock(TEST_API_ROOT, (api) => + api + .put(`/2.0/shuttle_subject_mappings/${subjectMappingId}`, { + is_subject_skipped: true, + target: null, + }) + .matchHeader('box-version', '2026.0') + .reply(200, updated) + ) + .stdout() + .command([ + 'shuttle:subject-mappings:update', + subjectMappingId, + '--is-subject-skipped', + '--clear-target', + '--json', + '--token=test', + ]) + .it('updates a subject mapping skip/clear-target', (context) => { + assert.deepEqual(JSON.parse(context.stdout), updated); + }); + }); + + describe('shuttle:subject-mappings:delete', function () { + test.nock(TEST_API_ROOT, (api) => + api + .delete(`/2.0/shuttle_subject_mappings/${subjectMappingId}`) + .matchHeader('box-version', '2026.0') + .reply(204) + ) + .stderr() + .command([ + 'shuttle:subject-mappings:delete', + subjectMappingId, + '--token=test', + ]) + .it('deletes a shuttle subject mapping', (context) => { + assert.equal( + context.stderr, + `Deleted shuttle subject mapping ${subjectMappingId}${os.EOL}` + ); + }); + }); + + describe('shuttle:subject-mapping-imports:create', function () { + test.nock(TEST_API_ROOT, (api) => + api + .post('/2.0/shuttle_subject_mapping_imports', { + job_id: jobId, + file_id: fileId, + }) + .matchHeader('box-version', '2026.0') + .reply(204) + ) + .stdout() + .command([ + 'shuttle:subject-mapping-imports:create', + `--job-id=${jobId}`, + `--file-id=${fileId}`, + '--token=test', + ]) + .it( + 'starts a subject-mapping spreadsheet import (no content)', + (context) => { + assert.equal(context.stdout, ''); + } + ); + }); + + /* ------------------------------------------------------------------ */ + /* browse / errors / discovered */ + /* ------------------------------------------------------------------ */ + + describe('shuttle:path-entries', function () { + test.nock(TEST_API_ROOT, (api) => + api + .get('/2.0/shuttle_path_entries') + .query({ + job_id: jobId, + side: 'source', + limit: 100, + }) + .matchHeader('box-version', '2026.0') + .reply(200, pathEntries) + ) + .stdout() + .command([ + 'shuttle:path-entries', + `--job-id=${jobId}`, + '--side=source', + '--max-items=100', + '--json', + '--token=test', + ]) + .it('lists path entries with camelCase query params', (context) => { + assert.deepEqual(JSON.parse(context.stdout), pathEntries.entries); + }); + }); + + describe('shuttle:mapping-errors', function () { + test.nock(TEST_API_ROOT, (api) => + api + .get('/2.0/shuttle_mapping_errors') + .query({ job_id: jobId, limit: 10 }) + .matchHeader('box-version', '2026.0') + .reply(200, { entries: [mappingError], limit: 10 }) + ) + .stdout() + .command([ + 'shuttle:mapping-errors', + `--job-id=${jobId}`, + '--max-items=10', + '--json', + '--token=test', + ]) + .it('lists shuttle mapping errors', (context) => { + assert.deepEqual(JSON.parse(context.stdout), [mappingError]); + }); + }); + + describe('shuttle:discovered-subjects', function () { + test.nock(TEST_API_ROOT, (api) => + api + .get('/2.0/shuttle_discovered_subjects') + .query({ job_id: jobId, limit: 10 }) + .matchHeader('box-version', '2026.0') + .reply(200, { entries: [discoveredSubject], limit: 10 }) + ) + .stdout() + .command([ + 'shuttle:discovered-subjects', + `--job-id=${jobId}`, + '--max-items=10', + '--json', + '--token=test', + ]) + .it('lists discovered subjects', (context) => { + assert.deepEqual(JSON.parse(context.stdout), [discoveredSubject]); + }); + }); +}); diff --git a/test/modules/shuttle.test.js b/test/modules/shuttle.test.js new file mode 100644 index 00000000..607cc5c4 --- /dev/null +++ b/test/modules/shuttle.test.js @@ -0,0 +1,315 @@ +'use strict'; + +const assert = require('chai').assert; +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const shuttle = require('../../src/modules/shuttle'); + +describe('Shuttle module helpers', function () { + describe('snakeToCamel', function () { + it('converts nested snake_case keys to camelCase', function () { + assert.deepEqual( + shuttle.snakeToCamel({ + job_id: 'j1', + account_mapping: { + is_enabled: true, + permission_conflict_strategy: 'warn_permissions_conflicts', + }, + tags: ['a', 'b'], + }), + { + jobId: 'j1', + accountMapping: { + isEnabled: true, + permissionConflictStrategy: 'warn_permissions_conflicts', + }, + tags: ['a', 'b'], + } + ); + }); + + it('leaves camelCase keys unchanged', function () { + assert.deepEqual(shuttle.snakeToCamel({ jobId: 'j1' }), { + jobId: 'j1', + }); + }); + }); + + describe('resolveBody', function () { + it('prefers --body JSON and converts snake_case', function () { + const body = shuttle.resolveBody( + { body: '{"job_id":"abc","job_type":"transfer"}' }, + () => { + throw new Error('builder should not run'); + } + ); + assert.deepEqual(body, { jobId: 'abc', jobType: 'transfer' }); + }); + + it('reads --body-file and converts snake_case', function () { + const filePath = path.join(os.tmpdir(), `boxcli-shuttle-body-${Date.now()}.json`); + fs.writeFileSync( + filePath, + JSON.stringify({ source_system_id: 'sys1', name: 'Job' }) + ); + try { + const body = shuttle.resolveBody({ 'body-file': filePath }, () => { + throw new Error('builder should not run'); + }); + assert.deepEqual(body, { + sourceSystemId: 'sys1', + name: 'Job', + }); + } finally { + fs.unlinkSync(filePath); + } + }); + + it('uses flag builder when no body override is set', function () { + const body = shuttle.resolveBody({}, () => ({ jobType: 'analysis' })); + assert.deepEqual(body, { jobType: 'analysis' }); + }); + + it('rejects non-object --body', function () { + assert.throws( + () => shuttle.resolveBody({ body: '[1,2]' }, () => ({})), + /--body must be a JSON object/ + ); + }); + }); + + describe('buildPathLocation', function () { + it('builds an account location with camelCase fields', function () { + const location = shuttle.buildPathLocation( + { + 'source-type': 'account', + 'source-path': '/Finance', + 'source-path-id': 'pid', + 'source-account-email': 'a@b.com', + 'source-is-directory': true, + }, + 'source', + { allowIsDirectory: true } + ); + assert.deepEqual(location, { + type: 'account', + path: '/Finance', + pathId: 'pid', + isDirectory: true, + account: { email: 'a@b.com' }, + }); + }); + + it('builds a shared_space location', function () { + const location = shuttle.buildPathLocation( + { + 'target-type': 'shared_space', + 'target-path': '/Shared', + 'target-space': 'Team Drive', + }, + 'target' + ); + assert.deepEqual(location, { + type: 'shared_space', + path: '/Shared', + space: 'Team Drive', + }); + }); + + it('returns undefined when no location flags are set', function () { + assert.isUndefined(shuttle.buildPathLocation({}, 'source')); + }); + + it('errors when required location flags are incomplete', function () { + const errors = []; + assert.throws( + () => + shuttle.buildPathLocation( + { 'source-type': 'account' }, + 'source', + { + command: { + error(message) { + errors.push(message); + throw new Error(message); + }, + }, + } + ), + /--source-type and --source-path are required/ + ); + assert.lengthOf(errors, 1); + }); + }); + + describe('buildSubjectTarget', function () { + it('builds a target', function () { + assert.deepEqual( + shuttle.buildSubjectTarget({ + 'target-type': 'user', + 'target-id': '123', + }), + { type: 'user', id: '123' } + ); + }); + + it('returns null for --clear-target', function () { + assert.isNull( + shuttle.buildSubjectTarget({ 'clear-target': true }) + ); + }); + + it('returns undefined when target flags are omitted', function () { + assert.isUndefined(shuttle.buildSubjectTarget({})); + }); + }); + + describe('buildScheduleTrigger', function () { + it('builds a by_time trigger', function () { + assert.deepEqual( + shuttle.buildScheduleTrigger({ + 'trigger-type': 'by_time', + 'start-at': '2026-03-16T09:00:00', + 'time-zone': 'America/Los_Angeles', + repeat: 'daily', + }), + { + type: 'by_time', + startAt: '2026-03-16T09:00:00', + timeZone: 'America/Los_Angeles', + repeat: 'daily', + } + ); + }); + + it('builds a by_job trigger', function () { + assert.deepEqual( + shuttle.buildScheduleTrigger({ + 'trigger-type': 'by_job', + 'triggered-by-job-id': 'job2', + 'last-run-condition': 'on_success', + }), + { + type: 'by_job', + triggeredByJobId: 'job2', + lastRunCondition: 'on_success', + } + ); + }); + }); + + describe('buildAccountMapping / buildSettings', function () { + it('builds account mapping in camelCase', function () { + assert.deepEqual( + shuttle.buildAccountMapping({ + 'account-mapping-enabled': true, + 'permission-conflict-strategy': + 'warn_permissions_conflicts', + 'are-permissions-mapped': false, + }), + { + isEnabled: true, + permissionConflictStrategy: 'warn_permissions_conflicts', + arePermissionsMapped: false, + } + ); + }); + + it('builds settings in camelCase', function () { + assert.deepEqual( + shuttle.buildSettings({ + 'is-overwrite-enabled': true, + 'bandwidth-limit-bytes-per-sec': 1000, + }), + { + isOverwriteEnabled: true, + bandwidthLimitBytesPerSec: 1000, + } + ); + }); + + it('returns undefined when no related flags are set', function () { + assert.isUndefined(shuttle.buildAccountMapping({})); + assert.isUndefined(shuttle.buildSettings({})); + }); + }); + + describe('flag bundles', function () { + it('exports expected flag groups', function () { + assert.property(shuttle.bodyFlags, 'body'); + assert.property(shuttle.bodyFlags, 'body-file'); + assert.property(shuttle.jobAccountMappingFlags, 'account-mapping-enabled'); + assert.property(shuttle.jobSettingsFlags, 'is-overwrite-enabled'); + assert.property(shuttle.scheduleTriggerFlags, 'trigger-type'); + assert.property(shuttle.subjectTargetFlags, 'target-type'); + assert.property( + shuttle.pathLocationFlags('source', { allowIsDirectory: true }), + 'source-is-directory' + ); + }); + }); + + describe('official box-node-sdk ShuttleManager', function () { + it('exposes all V2026R0 methods used by CLI commands', function () { + const { + BoxClient, + BoxDeveloperTokenAuth, + } = require('box-node-sdk/sdk-gen'); + const client = new BoxClient({ + auth: new BoxDeveloperTokenAuth({ token: 'test' }), + }); + assert.ok(client.shuttle, 'BoxClient.shuttle should exist on 4.19+'); + + const expected = [ + 'createShuttleSystemV2026R0', + 'getShuttleSystemsV2026R0', + 'getShuttleSystemByIdV2026R0', + 'updateShuttleSystemByIdV2026R0', + 'deleteShuttleSystemByIdV2026R0', + 'createShuttleSystemIssueRegistrationTokenV2026R0', + 'createShuttleJobV2026R0', + 'getShuttleJobsV2026R0', + 'getShuttleJobByIdV2026R0', + 'updateShuttleJobByIdV2026R0', + 'deleteShuttleJobByIdV2026R0', + 'createShuttleJobStartV2026R0', + 'createShuttleJobStopV2026R0', + 'createShuttleJobPrepareMigrationV2026R0', + 'createShuttleJobScheduleV2026R0', + 'getShuttleJobSchedulesV2026R0', + 'getShuttleJobScheduleByIdV2026R0', + 'updateShuttleJobScheduleByIdV2026R0', + 'deleteShuttleJobScheduleByIdV2026R0', + 'getShuttleRunsV2026R0', + 'getShuttleRunByIdV2026R0', + 'createShuttleReportV2026R0', + 'getShuttleReportsV2026R0', + 'getShuttleReportByIdV2026R0', + 'createShuttlePathMappingV2026R0', + 'getShuttlePathMappingsV2026R0', + 'getShuttlePathMappingByIdV2026R0', + 'updateShuttlePathMappingByIdV2026R0', + 'deleteShuttlePathMappingByIdV2026R0', + 'createShuttlePathMappingImportV2026R0', + 'createShuttleSubjectMappingV2026R0', + 'getShuttleSubjectMappingsV2026R0', + 'getShuttleSubjectMappingByIdV2026R0', + 'updateShuttleSubjectMappingByIdV2026R0', + 'deleteShuttleSubjectMappingByIdV2026R0', + 'createShuttleSubjectMappingImportV2026R0', + 'getShuttlePathEntriesV2026R0', + 'getShuttleMappingErrorsV2026R0', + 'getShuttleDiscoveredSubjectsV2026R0', + ]; + + for (const method of expected) { + assert.isFunction( + client.shuttle[method], + `missing ShuttleManager.${method}` + ); + } + assert.lengthOf(expected, 39); + }); + }); +}); From d5d0cd116feb26b0bf14217b9694bacdca22def1 Mon Sep 17 00:00:00 2001 From: Mateusz Wasiak Date: Thu, 12 Nov 2026 16:16:49 +0100 Subject: [PATCH 2/2] feat: align Shuttle CLI with add-box-cli-command skill Add oclif topic docs/fixtures, permission help text, Object.hasOwn for allowNo flags, negative options tests, and regenerate docs/shuttle.md. --- README.md | 1 + docs/shuttle.md | 2288 +++++++++++++++++ package.json | 3 + .../shuttle/discovered-subjects/index.js | 3 +- src/commands/shuttle/job-schedules/create.js | 8 +- src/commands/shuttle/job-schedules/delete.js | 3 +- src/commands/shuttle/job-schedules/get.js | 3 +- src/commands/shuttle/job-schedules/index.js | 3 +- src/commands/shuttle/job-schedules/update.js | 14 +- src/commands/shuttle/jobs/create.js | 6 +- src/commands/shuttle/jobs/delete.js | 3 +- src/commands/shuttle/jobs/get.js | 3 +- src/commands/shuttle/jobs/index.js | 3 +- .../shuttle/jobs/prepare-migration.js | 3 +- src/commands/shuttle/jobs/start.js | 7 +- src/commands/shuttle/jobs/stop.js | 3 +- src/commands/shuttle/jobs/update.js | 7 +- src/commands/shuttle/mapping-errors/index.js | 3 +- src/commands/shuttle/path-entries/index.js | 6 +- .../shuttle/path-mapping-imports/create.js | 4 +- src/commands/shuttle/path-mappings/create.js | 5 +- src/commands/shuttle/path-mappings/delete.js | 3 +- src/commands/shuttle/path-mappings/get.js | 3 +- src/commands/shuttle/path-mappings/index.js | 3 +- src/commands/shuttle/path-mappings/update.js | 5 +- src/commands/shuttle/reports/create.js | 10 +- src/commands/shuttle/reports/get.js | 3 +- src/commands/shuttle/reports/index.js | 3 +- src/commands/shuttle/runs/get.js | 3 +- src/commands/shuttle/runs/index.js | 3 +- .../shuttle/subject-mapping-imports/create.js | 4 +- .../shuttle/subject-mappings/create.js | 5 +- .../shuttle/subject-mappings/delete.js | 3 +- src/commands/shuttle/subject-mappings/get.js | 3 +- .../shuttle/subject-mappings/index.js | 3 +- .../shuttle/subject-mappings/update.js | 5 +- src/commands/shuttle/systems/create.js | 3 +- src/commands/shuttle/systems/delete.js | 3 +- src/commands/shuttle/systems/get.js | 3 +- src/commands/shuttle/systems/index.js | 3 +- .../systems/issue-registration-token.js | 3 +- src/commands/shuttle/systems/update.js | 3 +- src/modules/shuttle.js | 55 +- test/commands/shuttle.test.js | 302 ++- .../get_shuttle_discovered_subjects.json | 16 + .../shuttle/get_shuttle_job_schedules_id.json | 13 + .../fixtures/shuttle/get_shuttle_jobs_id.json | 14 + .../shuttle/get_shuttle_mapping_errors.json | 16 + .../shuttle/get_shuttle_path_entries.json | 17 + .../shuttle/get_shuttle_path_mappings_id.json | 20 + .../shuttle/get_shuttle_reports_id.json | 7 + .../fixtures/shuttle/get_shuttle_runs_id.json | 7 + .../get_shuttle_subject_mappings_id.json | 16 + .../shuttle/get_shuttle_systems_id.json | 7 + .../shuttle/post_shuttle_job_schedules.json | 13 + test/fixtures/shuttle/post_shuttle_jobs.json | 14 + .../shuttle/post_shuttle_path_mappings.json | 20 + .../shuttle/post_shuttle_reports.json | 7 + .../post_shuttle_subject_mappings.json | 16 + .../shuttle/post_shuttle_systems.json | 7 + ...e_systems_id_issue_registration_token.json | 5 + 61 files changed, 2806 insertions(+), 222 deletions(-) create mode 100644 docs/shuttle.md create mode 100644 test/fixtures/shuttle/get_shuttle_discovered_subjects.json create mode 100644 test/fixtures/shuttle/get_shuttle_job_schedules_id.json create mode 100644 test/fixtures/shuttle/get_shuttle_jobs_id.json create mode 100644 test/fixtures/shuttle/get_shuttle_mapping_errors.json create mode 100644 test/fixtures/shuttle/get_shuttle_path_entries.json create mode 100644 test/fixtures/shuttle/get_shuttle_path_mappings_id.json create mode 100644 test/fixtures/shuttle/get_shuttle_reports_id.json create mode 100644 test/fixtures/shuttle/get_shuttle_runs_id.json create mode 100644 test/fixtures/shuttle/get_shuttle_subject_mappings_id.json create mode 100644 test/fixtures/shuttle/get_shuttle_systems_id.json create mode 100644 test/fixtures/shuttle/post_shuttle_job_schedules.json create mode 100644 test/fixtures/shuttle/post_shuttle_jobs.json create mode 100644 test/fixtures/shuttle/post_shuttle_path_mappings.json create mode 100644 test/fixtures/shuttle/post_shuttle_reports.json create mode 100644 test/fixtures/shuttle/post_shuttle_subject_mappings.json create mode 100644 test/fixtures/shuttle/post_shuttle_systems.json create mode 100644 test/fixtures/shuttle/post_shuttle_systems_id_issue_registration_token.json diff --git a/README.md b/README.md index 7f670b7d..86128776 100644 --- a/README.md +++ b/README.md @@ -228,6 +228,7 @@ Avatar URL: 'https://app.box.com/api/avatar/large/77777' * [`box retention-policies`](docs/retention-policies.md) - List all retention policies for your enterprise * [`box search`](docs/search.md) - Search for files and folders in your Enterprise * [`box shared-links`](docs/shared-links.md) - Manage shared links +* [`box shuttle`](docs/shuttle.md) - Manage Box Shuttle migrations (systems, jobs, mappings, runs, reports) * [`box sign-requests`](docs/sign-requests.md) - List sign requests * [`box sign-templates`](docs/sign-templates.md) - List sign templates * [`box storage-policies`](docs/storage-policies.md) - List storage policies diff --git a/docs/shuttle.md b/docs/shuttle.md new file mode 100644 index 00000000..9fc99969 --- /dev/null +++ b/docs/shuttle.md @@ -0,0 +1,2288 @@ +`box shuttle` +============= + +Manage Box Shuttle migrations (systems, jobs, mappings, runs, reports) + +* [`box shuttle:discovered-subjects`](#box-shuttlediscovered-subjects) +* [`box shuttle:discovered-subjects:list`](#box-shuttlediscovered-subjectslist) +* [`box shuttle:job-schedules`](#box-shuttlejob-schedules) +* [`box shuttle:job-schedules:create`](#box-shuttlejob-schedulescreate) +* [`box shuttle:job-schedules:delete ID`](#box-shuttlejob-schedulesdelete-id) +* [`box shuttle:job-schedules:get ID`](#box-shuttlejob-schedulesget-id) +* [`box shuttle:job-schedules:list`](#box-shuttlejob-scheduleslist) +* [`box shuttle:job-schedules:update ID`](#box-shuttlejob-schedulesupdate-id) +* [`box shuttle:jobs`](#box-shuttlejobs) +* [`box shuttle:jobs:create [NAME]`](#box-shuttlejobscreate-name) +* [`box shuttle:jobs:delete ID`](#box-shuttlejobsdelete-id) +* [`box shuttle:jobs:get ID`](#box-shuttlejobsget-id) +* [`box shuttle:jobs:list`](#box-shuttlejobslist) +* [`box shuttle:jobs:prepare-migration ID`](#box-shuttlejobsprepare-migration-id) +* [`box shuttle:jobs:start ID`](#box-shuttlejobsstart-id) +* [`box shuttle:jobs:stop ID`](#box-shuttlejobsstop-id) +* [`box shuttle:jobs:update ID`](#box-shuttlejobsupdate-id) +* [`box shuttle:mapping-errors`](#box-shuttlemapping-errors) +* [`box shuttle:mapping-errors:list`](#box-shuttlemapping-errorslist) +* [`box shuttle:path-entries`](#box-shuttlepath-entries) +* [`box shuttle:path-entries:list`](#box-shuttlepath-entrieslist) +* [`box shuttle:path-mapping-imports:create`](#box-shuttlepath-mapping-importscreate) +* [`box shuttle:path-mappings`](#box-shuttlepath-mappings) +* [`box shuttle:path-mappings:create`](#box-shuttlepath-mappingscreate) +* [`box shuttle:path-mappings:delete ID`](#box-shuttlepath-mappingsdelete-id) +* [`box shuttle:path-mappings:get ID`](#box-shuttlepath-mappingsget-id) +* [`box shuttle:path-mappings:list`](#box-shuttlepath-mappingslist) +* [`box shuttle:path-mappings:update ID`](#box-shuttlepath-mappingsupdate-id) +* [`box shuttle:reports`](#box-shuttlereports) +* [`box shuttle:reports:create`](#box-shuttlereportscreate) +* [`box shuttle:reports:get ID`](#box-shuttlereportsget-id) +* [`box shuttle:reports:list`](#box-shuttlereportslist) +* [`box shuttle:runs`](#box-shuttleruns) +* [`box shuttle:runs:get ID`](#box-shuttlerunsget-id) +* [`box shuttle:runs:list`](#box-shuttlerunslist) +* [`box shuttle:subject-mapping-imports:create`](#box-shuttlesubject-mapping-importscreate) +* [`box shuttle:subject-mappings`](#box-shuttlesubject-mappings) +* [`box shuttle:subject-mappings:create`](#box-shuttlesubject-mappingscreate) +* [`box shuttle:subject-mappings:delete ID`](#box-shuttlesubject-mappingsdelete-id) +* [`box shuttle:subject-mappings:get ID`](#box-shuttlesubject-mappingsget-id) +* [`box shuttle:subject-mappings:list`](#box-shuttlesubject-mappingslist) +* [`box shuttle:subject-mappings:update ID`](#box-shuttlesubject-mappingsupdate-id) +* [`box shuttle:systems`](#box-shuttlesystems) +* [`box shuttle:systems:create NAME`](#box-shuttlesystemscreate-name) +* [`box shuttle:systems:delete ID`](#box-shuttlesystemsdelete-id) +* [`box shuttle:systems:get ID`](#box-shuttlesystemsget-id) +* [`box shuttle:systems:issue-registration-token ID`](#box-shuttlesystemsissue-registration-token-id) +* [`box shuttle:systems:list`](#box-shuttlesystemslist) +* [`box shuttle:systems:update ID`](#box-shuttlesystemsupdate-id) + +## `box shuttle:discovered-subjects` + +List discovered subjects for shuttle jobs Requires a token with the manage_enterprise_properties scope (and Shuttle access for the enterprise). + +``` +USAGE + $ box shuttle:discovered-subjects --job-id [-t ] [--as-user ] [--no-color] [--json | --csv] [-s | + --save-to-file-path ] [--fields ] [--bulk-file-path ] [-h] [-v] [-y] [-q] [--max-items ] + +FLAGS + -h, --help Show CLI help + -q, --quiet Suppress any non-error output to stderr + -s, --save Save report to default reports folder on disk + -t, --token= Provide a token to perform this call + -v, --verbose Show verbose output, which can be helpful for debugging + -y, --yes Automatically respond yes to all confirmation prompts + --as-user= Provide an ID for a user + --bulk-file-path= File path to bulk .csv or .json objects + --csv Output formatted CSV + --fields= Comma separated list of fields to show + --job-id= (required) Shuttle job ID whose discovered subjects to list + --json Output formatted JSON + --max-items= A value that indicates the maximum number of results to return. This only specifies a + maximum boundary and will not guarantee the minimum number of results returned. When + the max-items (x) is greater than 1000, then the maximum ceil(x/1000) requests will + be made. + --no-color Turn off colors for logging + --save-to-file-path= Override default file path to save report + +DESCRIPTION + List discovered subjects for shuttle jobs Requires a token with the manage_enterprise_properties scope (and Shuttle + access for the enterprise). + +ALIASES + $ box shuttle:discovered-subjects:list + +EXAMPLES + $ box shuttle:discovered-subjects --job-id 6a315c0754833bfc5df067bf +``` + +_See code: [src/commands/shuttle/discovered-subjects/index.js](https://github.com/box/boxcli/blob/v4.10.1/src/commands/shuttle/discovered-subjects/index.js)_ + +## `box shuttle:discovered-subjects:list` + +List discovered subjects for shuttle jobs Requires a token with the manage_enterprise_properties scope (and Shuttle access for the enterprise). + +``` +USAGE + $ box shuttle:discovered-subjects:list --job-id [-t ] [--as-user ] [--no-color] [--json | --csv] [-s | + --save-to-file-path ] [--fields ] [--bulk-file-path ] [-h] [-v] [-y] [-q] [--max-items ] + +FLAGS + -h, --help Show CLI help + -q, --quiet Suppress any non-error output to stderr + -s, --save Save report to default reports folder on disk + -t, --token= Provide a token to perform this call + -v, --verbose Show verbose output, which can be helpful for debugging + -y, --yes Automatically respond yes to all confirmation prompts + --as-user= Provide an ID for a user + --bulk-file-path= File path to bulk .csv or .json objects + --csv Output formatted CSV + --fields= Comma separated list of fields to show + --job-id= (required) Shuttle job ID whose discovered subjects to list + --json Output formatted JSON + --max-items= A value that indicates the maximum number of results to return. This only specifies a + maximum boundary and will not guarantee the minimum number of results returned. When + the max-items (x) is greater than 1000, then the maximum ceil(x/1000) requests will + be made. + --no-color Turn off colors for logging + --save-to-file-path= Override default file path to save report + +DESCRIPTION + List discovered subjects for shuttle jobs Requires a token with the manage_enterprise_properties scope (and Shuttle + access for the enterprise). + +ALIASES + $ box shuttle:discovered-subjects:list + +EXAMPLES + $ box shuttle:discovered-subjects --job-id 6a315c0754833bfc5df067bf +``` + +## `box shuttle:job-schedules` + +List shuttle job schedules Requires a token with the manage_enterprise_properties scope (and Shuttle access for the enterprise). + +``` +USAGE + $ box shuttle:job-schedules --job-id [-t ] [--as-user ] [--no-color] [--json | --csv] [-s | + --save-to-file-path ] [--fields ] [--bulk-file-path ] [-h] [-v] [-y] [-q] [--max-items ] + +FLAGS + -h, --help Show CLI help + -q, --quiet Suppress any non-error output to stderr + -s, --save Save report to default reports folder on disk + -t, --token= Provide a token to perform this call + -v, --verbose Show verbose output, which can be helpful for debugging + -y, --yes Automatically respond yes to all confirmation prompts + --as-user= Provide an ID for a user + --bulk-file-path= File path to bulk .csv or .json objects + --csv Output formatted CSV + --fields= Comma separated list of fields to show + --job-id= (required) Shuttle job ID whose schedules to list + --json Output formatted JSON + --max-items= A value that indicates the maximum number of results to return. This only specifies a + maximum boundary and will not guarantee the minimum number of results returned. When + the max-items (x) is greater than 1000, then the maximum ceil(x/1000) requests will + be made. + --no-color Turn off colors for logging + --save-to-file-path= Override default file path to save report + +DESCRIPTION + List shuttle job schedules Requires a token with the manage_enterprise_properties scope (and Shuttle access for the + enterprise). + +ALIASES + $ box shuttle:job-schedules:list + +EXAMPLES + $ box shuttle:job-schedules --job-id 6a315c0754833bfc5df067bf +``` + +_See code: [src/commands/shuttle/job-schedules/index.js](https://github.com/box/boxcli/blob/v4.10.1/src/commands/shuttle/job-schedules/index.js)_ + +## `box shuttle:job-schedules:create` + +Create a shuttle job schedule Requires a token with the manage_enterprise_properties scope (and Shuttle access for the enterprise). + +``` +USAGE + $ box shuttle:job-schedules:create [-t ] [--as-user ] [--no-color] [--json | --csv] [-s | --save-to-file-path + ] [--fields ] [--bulk-file-path ] [-h] [-v] [-y] [-q] [--body-file | --body ] + [--job-id ] [--run-mode transfer|simulation|analysis] [--state enabled|paused] [--description ] + [--trigger-type by_time|by_job] [--start-at ] [--time-zone ] [--repeat + none|daily|weekly|weekdays|weekends] [--triggered-by-job-id ] [--last-run-condition + always|on_success|on_finish] + +FLAGS + -h, --help Show CLI help + -q, --quiet Suppress any non-error output to stderr + -s, --save Save report to default reports folder on disk + -t, --token= Provide a token to perform this call + -v, --verbose Show verbose output, which can be helpful for debugging + -y, --yes Automatically respond yes to all confirmation prompts + --as-user= Provide an ID for a user + --body= Escape hatch: full JSON request body (snake_case or camelCase). When set, other + body-building flags are ignored. + --body-file= Escape hatch: path to a JSON file used as the full request body. + --bulk-file-path= File path to bulk .csv or .json objects + --csv Output formatted CSV + --description= Optional schedule description + --fields= Comma separated list of fields to show + --job-id= Shuttle job ID (required unless --body / --body-file) + --json Output formatted JSON + --last-run-condition=