diff --git a/.talismanrc b/.talismanrc index 8b5afad01..aa7705a49 100644 --- a/.talismanrc +++ b/.talismanrc @@ -1,10 +1,100 @@ fileignoreconfig: +- filename: pnpm-lock.yaml + checksum: 2e1116588e19b058f0d6a5efa28f54c6a3af11b0f0d8c442ed9866a90b77b713 +- filename: packages/contentstack-clone/test/lib/helpers/command-helpers.test.ts + checksum: 32d385a677a76faa54cb3ddfe92fb1f1f53cfab878a78e2792c823a363956bd2 +- filename: packages/contentstack-clone/test/commands/stack-clone.test.ts + checksum: 4547ba010cc3c9954de49e2b2bef45288dd070c4ad000b6ae3e9a8c9bb1ad296 +- filename: packages/contentstack-asset-management/test/unit/import/workspaces.test.ts + checksum: 67c11d312ba6838659c0b2cefd59d706d4b3c1a9807ed551dcb6d94f4d1d60a3 +- filename: packages/contentstack-clone/test/lib/util/clone-handler.branch.test.ts + checksum: db8c090aaada21d3952c5bc382228a0a11cebb63926512a5e2581bdaf5f4c2f6 +- filename: packages/contentstack-asset-management/test/unit/import/spaces.test.ts + checksum: d2363470664a6cdeaf59c305fa6307704e48b73f92b7d471433c17528daa5450 +- filename: packages/contentstack-asset-management/test/unit/utils/detect-asset-management-export.test.ts + checksum: 1917c51ebf4ba7f94cc440c27341d840911b5c315e250a451732c2d37518db64 +- filename: packages/contentstack-bootstrap/test/bootstrap-integration.test.js + checksum: 67579b79fc5c1b4feead8f537e7bdfd14fac58aa93c127f590505e47d26e0eb0 +- filename: packages/contentstack-import/test/unit/import/modules/index.test.ts + checksum: 27dd2c5f60f9f4c70f2ebf4c08cf8bcbe2d5e4dc7fe52a53972af3fdb57e0e63 +- filename: packages/contentstack-bulk-operations/test/unit/base-bulk-command.test.ts + checksum: 522b91a2d4bb6a73ebe5ab92c0111e74d567c4360470beefd6a7ca8d30a6e311 +- filename: packages/contentstack-content-type/tests/core/command.test.ts + checksum: 81da32fc47083a3421f5f1d9c04149ab313cb238d6416d3182f8b6d64dc8827d +- filename: packages/contentstack-content-type/tests/core/contentstack/client.test.ts + checksum: da15b04aa0a7ab0e48f70b847e68c05372943279e51c22f8539b01bb48033183 +- filename: packages/contentstack-import/test/unit/import/modules/marketplace-apps.test.ts + checksum: 17b3e6e0f7ce12ddabd688996c72d04dba5245fd300a23b541cf7bdfbbf7ef1a +- filename: packages/contentstack-asset-management/test/unit/import/asset-types.test.ts + checksum: 76aad963f5977059f7632d69a6fb36004229f3455b1618c372579dfa3cbc2d62 +- filename: packages/contentstack-asset-management/test/unit/import/assets.test.ts + checksum: f5f0494c9263ebcf7eabff806ff41bd7beed0cb6a0f9a631dd70ffa01524a304 +- filename: packages/contentstack-import/test/unit/utils/common-helper.test.ts + checksum: 7a10c4625d666cf705cd9869b74b3bc2dcec94926cbb5f719e4f8dc4f053b2c7 +- filename: packages/contentstack-content-type/tests/utils/index.test.ts + checksum: 8510fedf5eb6078c27d6a0d65d8bff4aa2d41b545246aed220d1a6a643188ad8 +- filename: packages/contentstack-import/test/unit/utils/interactive.test.ts + checksum: dad20e229e02e847b5e939fdaf987b1a6c9ff4e1529b6d96a51130f7293285a9 +- filename: packages/contentstack-asset-management/test/unit/import/base.test.ts + checksum: 53168bda5251300d8dfd0effd470070a5733c37b77abacc846d91e7086c8c022 +- filename: packages/contentstack-asset-management/test/unit/import/fields.test.ts + checksum: 9e97001a6c92d213a9f55a1458bf39c0996952467e0b4aabd954ddd8ad1805b9 +- filename: packages/contentstack-export-to-csv/test/unit/commands/export-to-csv.test.ts + checksum: cc82f4b0c50783dd42d8d57ba2af1e5c9aa3857399c5abf39bc5dc3fac105658 +- filename: packages/contentstack-bulk-operations/test/unit/utils/bulk-operation-log-handler.test.ts + checksum: 8698f77bbb828df10ddc3a94726076a262fbf47d2468233c7e10e66cac42f97d +- filename: packages/contentstack-import/test/unit/utils/login-handler.test.ts + checksum: a60f46b4d79ccdbeecfed27ffffbc97a44d6753f36ee74b68b725c8b20bbb2eb +- filename: packages/contentstack-export-to-csv/test/unit/utils/error-handler.test.ts + checksum: cef8f515ab6c0441f67748693473bf1499c2d54945aab1b89baaad37be8af2d9 +- filename: packages/contentstack-export-to-csv/test/unit/utils/teams-export.functional.test.ts + checksum: bcf70dbb201a0a1755f0b8f59550aae95a3e1d03700959eee7e8d415f9647746 +- filename: packages/contentstack-export/test/unit/utils/marketplace-app-helper.test.ts + checksum: 5648d90b0f7dc33a5b020324db5218707861ebd7201802f7fef00f1c03c5ce82 +- filename: packages/contentstack-export/test/unit/export/module-exporter.test.ts + checksum: 485bbd175cc95aff6c1c3c6b2904f92c3d4ea1272a056158a901da3eff9692d6 +- filename: packages/contentstack-import-setup/test/unit/common-helper.test.ts + checksum: ccbd8209d342bf651c2c38326cba13bc3e5c2af4200d7110fdd7c3de4824d21d +- filename: packages/contentstack-export/test/unit/utils/common-helper.test.ts + checksum: a4bfba19846e1075821521a202d856203f02a6bc0e5ec4f51b5265801cb4ddd9 +- filename: packages/contentstack-import/test/unit/import/modules/assets.test.ts + checksum: 259d5c325ec7de08d0f45099df9d3eb7c97374691c1c4d281221e112b9097191 +- filename: packages/contentstack-export/test/unit/export/modules/marketplace-apps.test.ts + checksum: 07eef22a3c471e8da12b1e7e51d45b76410725c4c160a0f35432d9aac5916974 +- filename: packages/contentstack-export/test/unit/utils/interactive.test.ts + checksum: 0ea47db305f576c8c9da8cb34ae915f6b01992208ff1c4380cb7ffd93f538e5d +- filename: packages/contentstack-bulk-operations/test/unit/utils/revert-retry-handler.test.ts + checksum: c34a4bdea186f4ce48171d90b8ab0f1c94162e441acb23e3a0e00cb9f9f9255f +- filename: packages/contentstack-bulk-operations/test/unit/utils/config-builder.test.ts + checksum: e5891257999e9e940956ca77f5a7834be35d259edccd41c06f77736194ad7454 +- filename: packages/contentstack-migrate-rte/test/commands/json-migration.test.js + checksum: 0d18641a32f7cf33d013a5a7c447c1067ba77022513b42304a727f873c741e99 +- filename: packages/contentstack-seed/test/commands/cm/stacks/seed.test.ts + checksum: e469a60f09a9a5fd3e2ce80dd6df8bf4f2c82acad1afebde00f458afa26d36c9 +- filename: packages/contentstack-migrate-rte/test/utils/index.js + checksum: f67235e607a446e5b2dd624046966a1ac5af7bce128aa080ec3c420c48b8bddc +- filename: packages/contentstack-query-export/test/unit/common-helper.test.ts + checksum: a0aa399a2ec15760fe25f7e952d64792b55d3c38d2281858a48e64f0f9081344 +- filename: packages/contentstack-seed/test/seed/contentstack/client.test.ts + checksum: 63bd3325e2717e5653f12e4afa362938e58eeaad5fbe07c1d41e2bd15e3451bd +- filename: packages/contentstack-import/test/unit/commands/cm/stacks/import.test.ts + checksum: d241111877a997f00702eaf171c1fde53a075a9c8f7971a8e6b1577c1c83279d +- filename: packages/contentstack-import/test/unit/import/modules/base-class.test.ts + checksum: 40c504d508d40b6416ccedb5a323365dd97f313b1dcc885e4fec3639e807b383 +- filename: packages/contentstack-import/test/unit/import/modules/content-types.test.ts + checksum: 662cb42217c12af2b4dad5d1355858095c5df062e42c89e09b2680fdbee80699 +- filename: packages/contentstack-import/test/unit/import/modules/entries.test.ts + checksum: a7311e3077fee52bfd23c46d51ebe9ce58d202dd735514a44ec230615f4cd917 +- filename: packages/contentstack-import/test/unit/import/modules/global-fields.test.ts + checksum: 2b4e0d5d11f825d21d45569f681acd886c5f84717873b26998b4dab2e91bb439 +- filename: packages/contentstack-seed/test/seed/interactive.test.ts + checksum: 8364557eb69b9a297281f868420c13c5b61b26b79bc2ceb0a9b548cfb0365d6d +- filename: packages/contentstack-variants/test/unit/export/variant-entries.test.ts + checksum: dd7018cd5cc8b5fab73004b2968afed7ce33abec0b20e1b17f15f2be23b117db +- filename: packages/contentstack-import-setup/test/unit/login-handler.test.ts + checksum: 2df43be75a96e1a3a00dd256628e191909aa9d7f192b672b56cb3772e074958c - filename: packages/contentstack-export/test/unit/utils/export-config-handler.test.ts checksum: f612661a8b6784b20ea62794b35192b340a34d8424a54a1a2cffec1878ab7528 - filename: packages/contentstack-import/test/unit/utils/marketplace-app-helper.test.ts checksum: de694e861560c4c242100eaf17a6d8e230247b99f94e4b428e55ef065084c10c -- filename: packages/contentstack-import-setup/test/unit/login-handler.test.ts - checksum: 2df43be75a96e1a3a00dd256628e191909aa9d7f192b672b56cb3772e074958c -- filename: pnpm-lock.yaml - checksum: eae269c31146f807a80978acb68b83b761742b6d8e82116b6af48dcc0156f085 version: "" diff --git a/packages/contentstack-bulk-operations/package.json b/packages/contentstack-bulk-operations/package.json index 4f530293d..b4c78cb6f 100644 --- a/packages/contentstack-bulk-operations/package.json +++ b/packages/contentstack-bulk-operations/package.json @@ -112,7 +112,6 @@ }, "csdxConfig": { "shortCommandName": { - "cm:stacks:bulk-am-assets": "BOAM", "cm:stacks:bulk-assets": "BOA", "cm:stacks:bulk-entries": "BOE", "cm:stacks:bulk-taxonomies": "BOT" diff --git a/packages/contentstack-bulk-operations/src/base-am-command.ts b/packages/contentstack-bulk-operations/src/base-am-command.ts deleted file mode 100644 index 0d23b9c93..000000000 --- a/packages/contentstack-bulk-operations/src/base-am-command.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Command } from '@contentstack/cli-command'; -import { handleAndLogError } from '@contentstack/cli-utilities'; - -import { fillMissingCsAssetsFlags } from './utils'; -import type { CsAssetsFlags } from './interfaces'; - -/** - * Thin base command for CS Assets operations. - * Handles flag prompting in init() and exposes typed parsedFlags / loggerContext. - * Deliberately does NOT inherit BaseBulkCommand — CS Assets operations use a different API - * surface with no stack setup, queue managers, or rate limiters. - */ -export abstract class BaseCsAssetsCommand extends Command { - protected parsedFlags!: CsAssetsFlags; - protected loggerContext!: { module: string }; - - protected async init(): Promise { - await super.init(); - const { flags } = await this.parse(this.constructor as typeof BaseCsAssetsCommand); - this.loggerContext = { module: this.id ?? 'cm:stacks:bulk-am-assets' }; - this.parsedFlags = (await fillMissingCsAssetsFlags(flags)) as CsAssetsFlags; - } - - async catch(error: Error): Promise { - handleAndLogError(error); - } - - abstract run(): Promise; -} diff --git a/packages/contentstack-bulk-operations/src/base-bulk-command.ts b/packages/contentstack-bulk-operations/src/base-bulk-command.ts index 55436fc33..3db56adb3 100644 --- a/packages/contentstack-bulk-operations/src/base-bulk-command.ts +++ b/packages/contentstack-bulk-operations/src/base-bulk-command.ts @@ -135,12 +135,25 @@ export abstract class BaseBulkCommand extends Command { private batchResults: Map = new Map(); protected parsedFlags: any; + /** + * Hook for subclasses to bypass the bulk-publish pipeline (stack setup, queue, + * rate limiter) when an operation uses a different execution path entirely. + * The subclass is then responsible for its own initialization after super.init(). + */ + protected shouldSkipBulkPipeline(): boolean { + return false; + } + /** * Initialize common components */ protected async init(): Promise { await super.init(); + if (this.shouldSkipBulkPipeline()) { + return; + } + let { flags } = await this.parse(this.constructor as typeof BaseBulkCommand); this.parsedFlags = flags; diff --git a/packages/contentstack-bulk-operations/src/commands/cm/stacks/bulk-am-assets.ts b/packages/contentstack-bulk-operations/src/commands/cm/stacks/bulk-am-assets.ts deleted file mode 100644 index 850832eca..000000000 --- a/packages/contentstack-bulk-operations/src/commands/cm/stacks/bulk-am-assets.ts +++ /dev/null @@ -1,257 +0,0 @@ -import chalk from 'chalk'; -import { flags, log, createLogContext, cliux, handleAndLogError, FlagInput } from '@contentstack/cli-utilities'; - -import messages, { $t } from '../../../messages'; -import { BaseCsAssetsCommand } from '../../../base-am-command'; -import { CsAssetsService } from '../../../services'; -import { - loadAssetUidsFromFile, - loadBulkDeleteItemsFromFile, - LoadAssetUidsError, -} from '../../../utils/asset-uids-from-file'; -import { generateCsAssetsJobStatusUrl } from '../../../utils/bulk-publish-url-generator'; -import { CsAssetsBulkDeleteItem } from '../../../interfaces'; - -const COMMAND_ID = 'cm:stacks:bulk-am-assets'; - -type RegionWithOptionalCsAssetsUrl = { csAssetsUrl?: string }; - -/** - * CS Assets bulk delete (job) / bulk move; asset UIDs come from a JSON file `{ "uids": [...] }`. - */ -export default class BulkCsAssets extends BaseCsAssetsCommand { - static description = messages.BULK_CS_ASSETS_DESCRIPTION; - - static examples = [ - '<%= config.bin %> <%= command.id %> --operation delete --space-uid am123 --org-uid bltcOrg --locale en-us --asset-uids-file ./assets.json', - '<%= config.bin %> <%= command.id %> --operation move --space-uid am123 --org-uid bltcOrg --target-folder-uid amFolder --asset-uids-file ./assets.json', - '<%= config.bin %> <%= command.id %> --operation delete --space-uid am123 --org-uid bltcOrg --workspace main --locale en-us --asset-uids-file ./uids.json -y', - ]; - - static flags: FlagInput = { - operation: flags.string({ - description: messages.CS_ASSETS_OPERATION_FLAG, - options: ['delete', 'move'], - }), - 'space-uid': flags.string({ - description: messages.CS_ASSETS_SPACE_UID_FLAG, - }), - 'org-uid': flags.string({ - description: messages.CS_ASSETS_ORG_UID_FLAG, - }), - workspace: flags.string({ - default: 'main', - description: messages.CS_ASSETS_WORKSPACE_FLAG, - }), - 'asset-uids-file': flags.string({ - description: messages.CS_ASSETS_ASSET_UIDS_FILE_FLAG, - }), - locale: flags.string({ - description: messages.CS_ASSETS_LOCALE_FLAG, - }), - 'target-folder-uid': flags.string({ - description: messages.CS_ASSETS_TARGET_FOLDER_FLAG, - }), - yes: flags.boolean({ - char: 'y', - description: messages.YES, - default: false, - }), - }; - - private printCsAssetsSummary( - op: 'delete' | 'move', - opts: { jobId?: string; count?: number; folderUid?: string; notice?: string; error?: string; spaceUid?: string } - ): void { - if (opts.error) { - log.error($t(messages.CS_ASSETS_OPERATION_FAILED, { operation: op }), this.loggerContext); - log.error(opts.error, this.loggerContext); - } else if (op === 'delete') { - log.success($t(messages.CS_ASSETS_DELETE_SUCCESS), this.loggerContext); - if (opts.jobId) log.info($t(messages.CS_ASSETS_DELETE_JOB_ID, { jobId: opts.jobId }), this.loggerContext); - log.info($t(messages.CS_ASSETS_DELETE_ASYNC_NOTE), this.loggerContext); - const statusUrl = generateCsAssetsJobStatusUrl(opts.spaceUid); - if (statusUrl) log.info(statusUrl, this.loggerContext); - } else { - log.success($t(messages.CS_ASSETS_MOVE_SUCCESS), this.loggerContext); - if (opts.count !== undefined && opts.folderUid) { - log.info( - $t(messages.CS_ASSETS_MOVE_ASSETS_COUNT, { count: opts.count, folderUid: opts.folderUid }), - this.loggerContext - ); - } - const statusUrl = generateCsAssetsJobStatusUrl(opts.spaceUid); - if (statusUrl) log.info(statusUrl, this.loggerContext); - } - if (opts.notice) log.info(opts.notice, this.loggerContext); - } - - private handleAssetUidsFileError(e: LoadAssetUidsError): void { - const pathShown = e.filePath; - if (e.kind === 'READ') { - log.error( - $t(messages.CS_ASSETS_ASSET_UIDS_FILE_READ_FAILED, { path: pathShown, detail: e.message }), - this.loggerContext - ); - } else { - log.error( - $t(messages.CS_ASSETS_ASSET_UIDS_FILE_INVALID, { path: pathShown, detail: e.message }), - this.loggerContext - ); - } - process.exitCode = 1; - } - - async run(): Promise { - try { - const f = this.parsedFlags; - - const csAssetsBaseUrl = (this.region as RegionWithOptionalCsAssetsUrl).csAssetsUrl?.trim(); - if (!csAssetsBaseUrl) { - log.error($t(messages.CS_ASSETS_URL_NOT_CONFIGURED), this.loggerContext); - process.exitCode = 1; - return; - } - - const op = f.operation; - if (op !== 'delete' && op !== 'move') { - log.error($t(messages.CS_ASSETS_INVALID_OPERATION, { operation: String(op ?? '') }), this.loggerContext); - process.exitCode = 1; - return; - } - - const spaceUid = f['space-uid'].trim(); - const orgUid = f['org-uid'].trim(); - const assetUidsPath = f['asset-uids-file'].trim(); - - let deleteRows: CsAssetsBulkDeleteItem[]; - - if (op === 'delete') { - const locale = (f.locale ?? '').trim(); - if (!locale) { - log.error($t(messages.CS_ASSETS_LOCALE_REQUIRED), this.loggerContext); - process.exitCode = 1; - return; - } - try { - deleteRows = loadBulkDeleteItemsFromFile(assetUidsPath, locale); - } catch (e: unknown) { - if (e instanceof LoadAssetUidsError) { - this.handleAssetUidsFileError(e); - } else { - handleAndLogError(e); - process.exitCode = 1; - } - return; - } - - createLogContext(this.context?.info?.command || COMMAND_ID, spaceUid, 'OAuth/Token'); - const csAssetsService = new CsAssetsService(csAssetsBaseUrl, spaceUid, orgUid); - const workspace = f.workspace ?? 'main'; - - if (!f.yes) { - console.log(chalk.yellow(`\n${$t(messages.OPERATION_CONFIG_HEADER)}\n`)); - console.log(' Operation: CS Assets bulk delete'); - console.log(` Space UID: ${spaceUid}`); - console.log(` Organization UID: ${orgUid}`); - console.log(` Workspace: ${workspace}`); - console.log(` Locale: ${locale}`); - console.log(` Asset UIDs file: ${assetUidsPath}`); - console.log(` Total CS Assets delete entries: ${deleteRows.length}\n`); - - const confirmed: boolean = await cliux.inquire({ - type: 'confirm', - name: 'proceed', - message: chalk.grey($t(messages.CONTINUE_WITH_CONFIG)), - default: false, - }); - if (!confirmed) { - log.warn($t(messages.OPERATION_CANCELLED), this.loggerContext); - return; - } - } - - log.info($t(messages.CS_ASSETS_DELETING_ASSETS, { count: deleteRows.length, spaceUid }), this.loggerContext); - const result = await csAssetsService.bulkDelete(spaceUid, workspace, deleteRows); - if (!result.success) { - this.printCsAssetsSummary('delete', { error: result.error ?? 'CS Assets bulk delete failed', spaceUid }); - process.exitCode = 1; - return; - } - this.printCsAssetsSummary('delete', { jobId: result.jobId, notice: result.notice, spaceUid }); - return; - } - - if (f.locale) { - log.error($t(messages.CS_ASSETS_LOCALE_NOT_ALLOWED_FOR_MOVE), this.loggerContext); - process.exitCode = 1; - return; - } - - const moveFolderUid = (f['target-folder-uid'] ?? '').trim(); - if (!moveFolderUid) { - log.error($t(messages.TARGET_FOLDER_REQUIRED), this.loggerContext); - process.exitCode = 1; - return; - } - - let uids: string[]; - try { - uids = loadAssetUidsFromFile(assetUidsPath); - } catch (e: unknown) { - if (e instanceof LoadAssetUidsError) { - this.handleAssetUidsFileError(e); - } else { - handleAndLogError(e); - process.exitCode = 1; - } - return; - } - - createLogContext(this.context?.info?.command || COMMAND_ID, spaceUid, 'OAuth/Token'); - const csAssetsService = new CsAssetsService(csAssetsBaseUrl, spaceUid, orgUid); - const workspace = f.workspace ?? 'main'; - - if (!f.yes) { - console.log(chalk.yellow(`\n${$t(messages.OPERATION_CONFIG_HEADER)}\n`)); - console.log(' Operation: CS Assets bulk move'); - console.log(` Space UID: ${spaceUid}`); - console.log(` Organization UID: ${orgUid}`); - console.log(` Workspace: ${workspace}`); - console.log(` Target folder UID: ${moveFolderUid}`); - console.log(` Asset UIDs file: ${assetUidsPath}`); - console.log(` Assets: ${uids.length}\n`); - - const confirmed: boolean = await cliux.inquire({ - type: 'confirm', - name: 'proceed', - message: chalk.grey($t(messages.CONTINUE_WITH_CONFIG)), - default: false, - }); - if (!confirmed) { - log.warn($t(messages.OPERATION_CANCELLED), this.loggerContext); - return; - } - } - - log.info( - $t(messages.CS_ASSETS_MOVING_ASSETS, { count: uids.length, targetFolderUid: moveFolderUid }), - this.loggerContext - ); - const result = await csAssetsService.bulkMove(spaceUid, workspace, uids, moveFolderUid); - if (!result.success) { - this.printCsAssetsSummary('move', { error: result.error ?? 'CS Assets bulk move failed', spaceUid }); - process.exitCode = 1; - return; - } - this.printCsAssetsSummary('move', { - count: uids.length, - folderUid: moveFolderUid, - notice: result.notice, - spaceUid, - }); - } catch (error) { - handleAndLogError(error); - } - } -} diff --git a/packages/contentstack-bulk-operations/src/commands/cm/stacks/bulk-assets.ts b/packages/contentstack-bulk-operations/src/commands/cm/stacks/bulk-assets.ts index 71122a91d..1706fefba 100644 --- a/packages/contentstack-bulk-operations/src/commands/cm/stacks/bulk-assets.ts +++ b/packages/contentstack-bulk-operations/src/commands/cm/stacks/bulk-assets.ts @@ -1,12 +1,39 @@ import { flags, handleAndLogError, FlagInput } from '@contentstack/cli-utilities'; -import { ResourceType } from '../../../interfaces'; +import { ResourceType, OperationType, CsAssetsFlags } from '../../../interfaces'; import { BaseBulkCommand } from '../../../base-bulk-command'; -import { $t, messages, fetchAssets } from '../../../utils'; +import { + $t, + messages, + fetchAssets, + fillMissingCsAssetsFlags, + promptForOperation, + runCsAssetsOperation, + enforceOperationFlagMatrix, + getOperationFromArgv, + OperationFlagMatrixError, + RETRY_REVERT_CONTEXT, +} from '../../../utils'; + +type RegionWithOptionalCsAssetsUrl = { csAssetsUrl?: string }; + +const ALL_OPERATION_CHOICES = [ + { name: 'Publish', value: OperationType.PUBLISH }, + { name: 'Unpublish', value: OperationType.UNPUBLISH }, + { name: 'Delete (CS Assets bulk delete)', value: OperationType.DELETE }, + { name: 'Move (CS Assets bulk move)', value: OperationType.MOVE }, +]; /** * Bulk operations command for assets - * Supports publish, unpublish, and cross publish operations + * Supports publish, unpublish, and cross publish operations (CMS), plus + * delete and move operations (CS Assets). + * + * The two families use fully separate execution paths: + * - publish/unpublish run through the BaseBulkCommand pipeline (stack setup, + * queue, rate limiter, retry) authenticated via stack API key or alias. + * - delete/move run through the CS Assets runner (OAuth/Token against the + * region's csAssetsUrl) with no bulk-publish infrastructure at all. */ export default class BulkAssets extends BaseBulkCommand { static description = messages.BULK_ASSETS_DESCRIPTION; @@ -32,18 +59,118 @@ export default class BulkAssets extends BaseBulkCommand { // Revert (unpublish) previously published assets using success log '<%= config.bin %> <%= command.id %> --revert ./bulk-operation -a myAlias', + + // CS Assets bulk delete (asset UIDs from a JSON file `{ "uids": [...] }`) + '<%= config.bin %> <%= command.id %> --operation delete --space-uid am123 --org-uid bltOrg --locale en-us --asset-uids-file ./assets.json', + + // CS Assets bulk move to a target folder + '<%= config.bin %> <%= command.id %> --operation move --space-uid am123 --org-uid bltOrg --target-folder-uid amFolder --asset-uids-file ./assets.json', ]; static flags: FlagInput = { ...BaseBulkCommand.baseFlags, + operation: flags.string({ + description: messages.BULK_ASSETS_OPERATION, + options: [OperationType.PUBLISH, OperationType.UNPUBLISH, OperationType.DELETE, OperationType.MOVE], + required: false, // Not required if retry-failed or revert is used + }), 'folder-uid': flags.string({ description: messages.FOLDER_UID, }), + + // CS Assets delete/move flags + 'space-uid': flags.string({ + description: messages.CS_ASSETS_SPACE_UID_FLAG, + }), + 'org-uid': flags.string({ + description: messages.CS_ASSETS_ORG_UID_FLAG, + }), + workspace: flags.string({ + default: 'main', + description: messages.CS_ASSETS_WORKSPACE_FLAG, + }), + 'asset-uids-file': flags.string({ + description: messages.CS_ASSETS_ASSET_UIDS_FILE_FLAG, + }), + locale: flags.string({ + description: messages.CS_ASSETS_LOCALE_FLAG, + }), + 'target-folder-uid': flags.string({ + description: messages.CS_ASSETS_TARGET_FOLDER_FLAG, + }), }; protected resourceType: ResourceType = ResourceType.ASSET; + /** True when the resolved operation is a CS Assets one (delete/move). */ + private csAssetsMode = false; + private csAssetsFlags!: CsAssetsFlags; + + protected shouldSkipBulkPipeline(): boolean { + return this.csAssetsMode; + } + + protected async init(): Promise { + // Resolve the operation from raw argv BEFORE any pipeline work: the two + // operation families use disjoint flags and auth, so the pipeline choice + // (and flag validation) depends on it. Raw argv is also what lets us reject + // explicitly-passed CMS flags that carry defaults (--branch, --publish-mode). + let operation = getOperationFromArgv(this.argv); + const isRevertOrRetry = this.argv.some( + (token) => + token === '--retry-failed' || + token.startsWith('--retry-failed=') || + token === '--revert' || + token.startsWith('--revert=') + ); + + if (!operation && !isRevertOrRetry) { + if (!process.stdin.isTTY) { + throw new Error( + 'Missing required flag: --operation. Provide it when running in a non-interactive environment.' + ); + } + operation = await promptForOperation(ALL_OPERATION_CHOICES); + // Feed the answer back into argv so both pipelines parse it like any other flag + this.argv.push('--operation', operation); + } + + enforceOperationFlagMatrix(operation ?? RETRY_REVERT_CONTEXT, this.argv, { module: this.id }); + + this.csAssetsMode = operation === OperationType.DELETE || operation === OperationType.MOVE; + + // For delete/move, shouldSkipBulkPipeline() makes super.init() stop right after + // the framework-level Command init — no stack setup, queue, or rate limiter. + await super.init(); + + if (this.csAssetsMode) { + const { flags: parsed } = await this.parse(this.constructor as typeof BulkAssets); + this.loggerContext = { module: this.id ?? 'cm:stacks:bulk-assets' }; + this.csAssetsFlags = (await fillMissingCsAssetsFlags(parsed)) as CsAssetsFlags; + this.parsedFlags = this.csAssetsFlags; + } + } + + async catch(error: Error): Promise { + // Matrix violations are already logged (with exitCode set) by + // enforceOperationFlagMatrix — the throw only aborts init(). + if (error instanceof OperationFlagMatrixError) { + return; + } + return super.catch(error); + } + async run(): Promise { + if (this.csAssetsMode) { + await runCsAssetsOperation({ + flags: this.csAssetsFlags, + csAssetsBaseUrl: (this.region as RegionWithOptionalCsAssetsUrl).csAssetsUrl, + commandId: this.context?.info?.command || this.id || 'cm:stacks:bulk-assets', + loggerContext: this.loggerContext, + }); + return; + } + try { // Handle cross-publish separately if source-env is specified if (this.bulkOperationConfig.sourceEnv) { diff --git a/packages/contentstack-bulk-operations/src/interfaces/index.ts b/packages/contentstack-bulk-operations/src/interfaces/index.ts index 04b3ed901..f7a9c7596 100644 --- a/packages/contentstack-bulk-operations/src/interfaces/index.ts +++ b/packages/contentstack-bulk-operations/src/interfaces/index.ts @@ -271,7 +271,7 @@ export interface CsAssetsBulkOperationResult { error?: string; } -/** Typed flags for the bulk-am-assets command. */ +/** Typed flags for CS Assets delete/move operations (cm:stacks:bulk-assets). */ export interface CsAssetsFlags { operation: string; 'space-uid': string; diff --git a/packages/contentstack-bulk-operations/src/messages/index.ts b/packages/contentstack-bulk-operations/src/messages/index.ts index ccc90ebb5..9f82677a8 100644 --- a/packages/contentstack-bulk-operations/src/messages/index.ts +++ b/packages/contentstack-bulk-operations/src/messages/index.ts @@ -200,6 +200,8 @@ const assetServiceMsg = { * Bulk assets command messages */ const bulkAssetsMsg = { + BULK_ASSETS_OPERATION: + 'Operation to perform: `publish`/`unpublish` (CMS, requires stack API key or alias) or `delete`/`move` (CS Assets, requires OAuth/Token and --space-uid/--org-uid)', FETCHING: 'Fetching assets...', FOUND_ASSETS: 'Found {count} assets ({locale})', FETCH_FOR_LOCALES: 'Fetch assets for {count} locales', @@ -216,8 +218,6 @@ const bulkAssetsMsg = { * CS Assets bulk delete/move messages */ const csAssetsBulkMsg = { - BULK_CS_ASSETS_DESCRIPTION: - 'Bulk delete or move assets via CS Assets API. Loads asset UIDs from a JSON file `{ "uids": [...] }`; pass organization via `--org-uid`.', CS_ASSETS_URL_NOT_CONFIGURED: 'CS Assets operations require csAssetsUrl in your region settings. Ensure your region is configured correctly.', SPACE_UID_REQUIRED: '--space-uid is required for CS Assets operations', @@ -253,8 +253,13 @@ const csAssetsBulkMsg = { CS_ASSETS_MOVE_ASSETS_COUNT: '{count} asset(s) moved to folder: {folderUid}', CS_ASSETS_OPERATION_FAILED: 'CS Assets {operation} failed.', + // Merged-command flag matrix validation + FLAG_NOT_ALLOWED_FOR_OPERATION: '{flag} is not valid for operation "{operation}".{hint}', + FLAG_NOT_ALLOWED_WITH_RETRY_REVERT: '{flag} cannot be combined with --retry-failed/--revert.', + CS_ASSETS_AUTH_REQUIRED: + 'The {operation} operation requires OAuth login or an auth token. Run "csdx login" and try again.', + // Interactive prompts - CS_ASSETS_SELECT_OPERATION: 'Select CS Assets operation:', CS_ASSETS_ENTER_SPACE_UID: 'Enter CS Assets space UID:', CS_ASSETS_ENTER_ORG_UID: 'Enter organization UID:', CS_ASSETS_ENTER_ASSET_UIDS_FILE: 'Enter path to asset UIDs JSON file (e.g. ./assets.json):', @@ -419,10 +424,10 @@ const flagDescriptions = { */ const commandInfo = { BULK_ENTRIES_DESCRIPTION: 'Bulk operations for entries (publish/unpublish/cross-publish)', - BULK_ASSETS_DESCRIPTION: 'Bulk operations for assets (publish/unpublish/cross-publish)', + BULK_ASSETS_DESCRIPTION: + 'Bulk operations for assets: publish/unpublish/cross-publish (CMS) and delete/move (CS Assets). Delete/move load asset UIDs from a JSON file `{ "uids": [...] }`; pass organization via `--org-uid`.', BULK_TAXONOMIES_DESCRIPTION: 'Publish taxonomies to environments and locales (CMA POST /v3/taxonomies/publish; initiates a publish job)', - BULK_CS_ASSETS_DESCRIPTION: csAssetsBulkMsg.BULK_CS_ASSETS_DESCRIPTION, }; /** diff --git a/packages/contentstack-bulk-operations/src/utils/cs-assets-runner.ts b/packages/contentstack-bulk-operations/src/utils/cs-assets-runner.ts new file mode 100644 index 000000000..4428fa881 --- /dev/null +++ b/packages/contentstack-bulk-operations/src/utils/cs-assets-runner.ts @@ -0,0 +1,254 @@ +import chalk from 'chalk'; +import { log, createLogContext, cliux, handleAndLogError, authenticationHandler } from '@contentstack/cli-utilities'; + +import messages, { $t } from '../messages'; +import { CsAssetsService } from '../services'; +import { loadAssetUidsFromFile, loadBulkDeleteItemsFromFile, LoadAssetUidsError } from './asset-uids-from-file'; +import { generateCsAssetsJobStatusUrl } from './bulk-publish-url-generator'; +import { CsAssetsFlags, CsAssetsBulkOperationResult, OperationType } from '../interfaces'; + +/** + * Execution path for CS Assets bulk delete/move — deliberately independent of the + * BaseBulkCommand pipeline. CS Assets operations use a different API surface with + * no stack setup, queue managers, or rate limiters, and authenticate via OAuth/Token + * against the region's csAssetsUrl. + */ + +interface CsAssetsRunnerOptions { + flags: CsAssetsFlags; + csAssetsBaseUrl: string | undefined; + commandId: string; + loggerContext: { module: string }; +} + +/** Per-operation execution plan; the shared flow below drives it. */ +interface CsAssetsOperationPlan { + summaryLines: string[]; + logStart: () => void; + execute: (service: CsAssetsService) => Promise; + failureFallback: string; + successOpts: (result: CsAssetsBulkOperationResult) => Parameters[1]; +} + +function printCsAssetsSummary( + op: 'delete' | 'move', + opts: { jobId?: string; count?: number; folderUid?: string; notice?: string; error?: string; spaceUid?: string }, + loggerContext: { module: string } +): void { + if (opts.error) { + log.error($t(messages.CS_ASSETS_OPERATION_FAILED, { operation: op }), loggerContext); + log.error(opts.error, loggerContext); + } else if (op === 'delete') { + log.success($t(messages.CS_ASSETS_DELETE_SUCCESS), loggerContext); + if (opts.jobId) log.info($t(messages.CS_ASSETS_DELETE_JOB_ID, { jobId: opts.jobId }), loggerContext); + log.info($t(messages.CS_ASSETS_DELETE_ASYNC_NOTE), loggerContext); + const statusUrl = generateCsAssetsJobStatusUrl(opts.spaceUid); + if (statusUrl) log.info(statusUrl, loggerContext); + } else { + log.success($t(messages.CS_ASSETS_MOVE_SUCCESS), loggerContext); + if (opts.count !== undefined && opts.folderUid) { + log.info( + $t(messages.CS_ASSETS_MOVE_ASSETS_COUNT, { count: opts.count, folderUid: opts.folderUid }), + loggerContext + ); + } + const statusUrl = generateCsAssetsJobStatusUrl(opts.spaceUid); + if (statusUrl) log.info(statusUrl, loggerContext); + } + if (opts.notice) log.info(opts.notice, loggerContext); +} + +function handleAssetUidsFileError(e: LoadAssetUidsError, loggerContext: { module: string }): void { + const pathShown = e.filePath; + if (e.kind === 'READ') { + log.error( + $t(messages.CS_ASSETS_ASSET_UIDS_FILE_READ_FAILED, { path: pathShown, detail: e.message }), + loggerContext + ); + } else { + log.error($t(messages.CS_ASSETS_ASSET_UIDS_FILE_INVALID, { path: pathShown, detail: e.message }), loggerContext); + } + process.exitCode = 1; +} + +/** + * Loads rows from the asset UIDs file, reporting errors and setting the exit code. + * Returns undefined when loading failed. + */ +function tryLoadFromFile(loader: () => T, loggerContext: { module: string }): T | undefined { + try { + return loader(); + } catch (e: unknown) { + if (e instanceof LoadAssetUidsError) { + handleAssetUidsFileError(e, loggerContext); + } else { + handleAndLogError(e as Error); + process.exitCode = 1; + } + return undefined; + } +} + +/** + * Pre-flight auth check: CS Assets operations require an OAuth session or auth token. + * Fails before any confirmation prompt so users don't confirm a destructive operation + * that would only fail at the API call. + */ +async function ensureCsAssetsAuth(operation: string, loggerContext: { module: string }): Promise { + try { + await authenticationHandler.getAuthDetails(); + if (!authenticationHandler.accessToken) { + log.error($t(messages.CS_ASSETS_AUTH_REQUIRED, { operation }), loggerContext); + process.exitCode = 1; + return false; + } + return true; + } catch (error) { + log.error($t(messages.CS_ASSETS_AUTH_REQUIRED, { operation }), loggerContext); + handleAndLogError(error as Error); + process.exitCode = 1; + return false; + } +} + +/** Prints the operation summary and asks for confirmation (skipped with --yes). */ +async function confirmProceed( + summaryLines: string[], + yes: boolean, + loggerContext: { module: string } +): Promise { + if (yes) return true; + + console.log(chalk.yellow(`\n${$t(messages.OPERATION_CONFIG_HEADER)}\n`)); + for (const line of summaryLines) { + console.log(` ${line}`); + } + console.log(''); + + const confirmed: boolean = await cliux.inquire({ + type: 'confirm', + name: 'proceed', + message: chalk.grey($t(messages.CONTINUE_WITH_CONFIG)), + default: false, + }); + if (!confirmed) { + log.warn($t(messages.OPERATION_CANCELLED), loggerContext); + } + return confirmed; +} + +/** + * Runs a CS Assets bulk delete or move. Flags are expected to be pre-validated + * (operation flag matrix) and pre-filled (fillMissingCsAssetsFlags). + */ +export async function runCsAssetsOperation(options: CsAssetsRunnerOptions): Promise { + const { flags: f, csAssetsBaseUrl: rawBaseUrl, commandId, loggerContext } = options; + + try { + const csAssetsBaseUrl = rawBaseUrl?.trim(); + if (!csAssetsBaseUrl) { + log.error($t(messages.CS_ASSETS_URL_NOT_CONFIGURED), loggerContext); + process.exitCode = 1; + return; + } + + const op = f.operation; + if (op !== OperationType.DELETE && op !== OperationType.MOVE) { + log.error($t(messages.CS_ASSETS_INVALID_OPERATION, { operation: String(op ?? '') }), loggerContext); + process.exitCode = 1; + return; + } + + if (!(await ensureCsAssetsAuth(op, loggerContext))) { + return; + } + + const spaceUid = f['space-uid'].trim(); + const orgUid = f['org-uid'].trim(); + const assetUidsPath = f['asset-uids-file'].trim(); + const workspace = f.workspace ?? 'main'; + + const commonSummaryLines = [`Space UID: ${spaceUid}`, `Organization UID: ${orgUid}`, `Workspace: ${workspace}`]; + + let plan: CsAssetsOperationPlan; + + if (op === OperationType.DELETE) { + const locale = (f.locale ?? '').trim(); + if (!locale) { + log.error($t(messages.CS_ASSETS_LOCALE_REQUIRED), loggerContext); + process.exitCode = 1; + return; + } + + const deleteRows = tryLoadFromFile(() => loadBulkDeleteItemsFromFile(assetUidsPath, locale), loggerContext); + if (!deleteRows) return; + + plan = { + summaryLines: [ + 'Operation: CS Assets bulk delete', + ...commonSummaryLines, + `Locale: ${locale}`, + `Asset UIDs file: ${assetUidsPath}`, + `Total CS Assets delete entries: ${deleteRows.length}`, + ], + logStart: () => + log.info($t(messages.CS_ASSETS_DELETING_ASSETS, { count: deleteRows.length, spaceUid }), loggerContext), + execute: (service) => service.bulkDelete(spaceUid, workspace, deleteRows), + failureFallback: 'CS Assets bulk delete failed', + successOpts: (result) => ({ jobId: result.jobId, notice: result.notice, spaceUid }), + }; + } else { + if (f.locale) { + log.error($t(messages.CS_ASSETS_LOCALE_NOT_ALLOWED_FOR_MOVE), loggerContext); + process.exitCode = 1; + return; + } + + const moveFolderUid = (f['target-folder-uid'] ?? '').trim(); + if (!moveFolderUid) { + log.error($t(messages.TARGET_FOLDER_REQUIRED), loggerContext); + process.exitCode = 1; + return; + } + + const uids = tryLoadFromFile(() => loadAssetUidsFromFile(assetUidsPath), loggerContext); + if (!uids) return; + + plan = { + summaryLines: [ + 'Operation: CS Assets bulk move', + ...commonSummaryLines, + `Target folder UID: ${moveFolderUid}`, + `Asset UIDs file: ${assetUidsPath}`, + `Assets: ${uids.length}`, + ], + logStart: () => + log.info( + $t(messages.CS_ASSETS_MOVING_ASSETS, { count: uids.length, targetFolderUid: moveFolderUid }), + loggerContext + ), + execute: (service) => service.bulkMove(spaceUid, workspace, uids, moveFolderUid), + failureFallback: 'CS Assets bulk move failed', + successOpts: (result) => ({ count: uids.length, folderUid: moveFolderUid, notice: result.notice, spaceUid }), + }; + } + + createLogContext(commandId, spaceUid, 'OAuth/Token'); + const csAssetsService = new CsAssetsService(csAssetsBaseUrl, spaceUid, orgUid); + + if (!(await confirmProceed(plan.summaryLines, f.yes, loggerContext))) { + return; + } + + plan.logStart(); + const result = await plan.execute(csAssetsService); + if (!result.success) { + printCsAssetsSummary(op, { error: result.error ?? plan.failureFallback, spaceUid }, loggerContext); + process.exitCode = 1; + return; + } + printCsAssetsSummary(op, plan.successOpts(result), loggerContext); + } catch (error) { + handleAndLogError(error as Error); + } +} diff --git a/packages/contentstack-bulk-operations/src/utils/index.ts b/packages/contentstack-bulk-operations/src/utils/index.ts index aa1d8801f..651b8c755 100644 --- a/packages/contentstack-bulk-operations/src/utils/index.ts +++ b/packages/contentstack-bulk-operations/src/utils/index.ts @@ -35,7 +35,15 @@ import { buildBulkModeResult, handleOperationError, } from './command-helpers'; -import { fillMissingFlags, fillMissingCsAssetsFlags } from './interactive'; +import { fillMissingFlags, fillMissingCsAssetsFlags, promptForOperation } from './interactive'; +import { runCsAssetsOperation } from './cs-assets-runner'; +import { + validateOperationFlagMatrix, + enforceOperationFlagMatrix, + getOperationFromArgv, + OperationFlagMatrixError, + RETRY_REVERT_CONTEXT, +} from './operation-flag-matrix'; import { RATE_LIMITER_CONSTANTS, RETRY_STRATEGY_CONSTANTS, @@ -99,6 +107,13 @@ export { handleOperationError, fillMissingFlags, fillMissingCsAssetsFlags, + promptForOperation, + runCsAssetsOperation, + validateOperationFlagMatrix, + enforceOperationFlagMatrix, + getOperationFromArgv, + OperationFlagMatrixError, + RETRY_REVERT_CONTEXT, fetchTaxonomyList, RATE_LIMITER_CONSTANTS, RETRY_STRATEGY_CONSTANTS, diff --git a/packages/contentstack-bulk-operations/src/utils/interactive.ts b/packages/contentstack-bulk-operations/src/utils/interactive.ts index 448d1d5ca..2066c7809 100644 --- a/packages/contentstack-bulk-operations/src/utils/interactive.ts +++ b/packages/contentstack-bulk-operations/src/utils/interactive.ts @@ -2,15 +2,19 @@ import { cliux, configHandler, isAuthenticated } from '@contentstack/cli-utiliti import { OperationType, FilterType } from '../interfaces'; import { messages } from './index'; -async function promptForOperation(): Promise { +const DEFAULT_OPERATION_CHOICES = [ + { name: 'Publish', value: OperationType.PUBLISH }, + { name: 'Unpublish', value: OperationType.UNPUBLISH }, +]; + +export async function promptForOperation( + choices: Array<{ name: string; value: string }> = DEFAULT_OPERATION_CHOICES +): Promise { return cliux.inquire({ type: 'list', name: 'operation', message: messages.SELECT_OPERATION, - choices: [ - { name: 'Publish', value: OperationType.PUBLISH }, - { name: 'Unpublish', value: OperationType.UNPUBLISH }, - ], + choices, }); } @@ -235,9 +239,10 @@ async function runInteractivePrompts(prompts: Array<() => Promise>): Promi } /** - * Fills in missing flags for the bulk-am-assets command by prompting the user. + * Fills in missing flags for CS Assets delete/move operations by prompting the user. * Handles CS Assets-specific required flags including operation-conditional ones * (locale for delete, target-folder-uid for move). + * The operation itself is always resolved by the command's init() before this runs. * Throws in non-TTY environments when required flags are missing. */ export async function fillMissingCsAssetsFlags(flags: any): Promise { @@ -245,15 +250,13 @@ export async function fillMissingCsAssetsFlags(flags: any): Promise { const needsLocale = f.operation === 'delete' && !f.locale; const needsFolderUid = f.operation === 'move' && !f['target-folder-uid']; - const needsPrompt = - !f.operation || !f['space-uid'] || !f['org-uid'] || !f['asset-uids-file'] || needsLocale || needsFolderUid; + const needsPrompt = !f['space-uid'] || !f['org-uid'] || !f['asset-uids-file'] || needsLocale || needsFolderUid; if (!needsPrompt) return f; // Fail fast in non-interactive environments (CI/CD) rather than hanging on stdin if (!process.stdin.isTTY) { const missing = [ - !f.operation && '--operation', !f['space-uid'] && '--space-uid', !f['org-uid'] && '--org-uid', !f['asset-uids-file'] && '--asset-uids-file', @@ -266,19 +269,6 @@ export async function fillMissingCsAssetsFlags(flags: any): Promise { } await runInteractivePrompts([ - async () => { - if (!f.operation) { - f.operation = await cliux.inquire({ - type: 'list', - name: 'operation', - message: messages.CS_ASSETS_SELECT_OPERATION, - choices: [ - { name: 'Delete (CS Assets bulk delete)', value: 'delete' }, - { name: 'Move (CS Assets bulk move)', value: 'move' }, - ], - }); - } - }, async () => { if (!f['space-uid']) { f['space-uid'] = await cliux.inquire({ diff --git a/packages/contentstack-bulk-operations/src/utils/operation-flag-matrix.ts b/packages/contentstack-bulk-operations/src/utils/operation-flag-matrix.ts new file mode 100644 index 000000000..5d3ad02f1 --- /dev/null +++ b/packages/contentstack-bulk-operations/src/utils/operation-flag-matrix.ts @@ -0,0 +1,157 @@ +import { log } from '@contentstack/cli-utilities'; + +import messages, { $t } from '../messages'; +import { OperationType } from '../interfaces'; + +/** + * Per-operation flag matrix for the merged `cm:stacks:bulk-assets` command. + * + * publish/unpublish (CMS) and delete/move (CS Assets) share a single command but + * use disjoint flag sets and auth models. oclif cannot express operation-conditional + * flags statically, so this validates the RAW argv: several CMS flags carry defaults + * (--branch, --publish-mode, --bulk-operation-file), which makes parsed flags unable + * to distinguish "user passed it" from "default filled it". + */ + +interface FlagSpec { + /** Long flag name without leading dashes */ + name: string; + /** Optional short char without leading dash */ + char?: string; +} + +/** Flags only meaningful for CMS publish/unpublish operations. */ +const CMS_ONLY_FLAGS: FlagSpec[] = [ + { name: 'stack-api-key', char: 'k' }, + { name: 'alias', char: 'a' }, + { name: 'environments' }, + { name: 'locales' }, + { name: 'source-env' }, + { name: 'source-alias' }, + { name: 'publish-mode' }, + { name: 'branch' }, + { name: 'config', char: 'c' }, + { name: 'retry-failed' }, + { name: 'revert' }, + { name: 'bulk-operation-file' }, + { name: 'folder-uid' }, +]; + +/** Flags only meaningful for CS Assets delete/move operations. */ +const CS_ASSETS_ONLY_FLAGS: FlagSpec[] = [ + { name: 'space-uid' }, + { name: 'org-uid' }, + { name: 'asset-uids-file' }, + { name: 'locale' }, + { name: 'target-folder-uid' }, + { name: 'workspace' }, +]; + +/** Did-you-mean hints for the twin locale flags. */ +const FLAG_HINTS: Record> = { + locales: { + [OperationType.DELETE]: ' Did you mean --locale?', + }, + locale: { + [OperationType.PUBLISH]: ' Did you mean --locales?', + [OperationType.UNPUBLISH]: ' Did you mean --locales?', + }, +}; + +/** + * Returns true when the given flag was explicitly passed on the command line. + * Handles `--flag`, `--flag=value`, `--no-flag`, and short forms `-k` / `-k=value`. + */ +function isFlagInArgv(argv: string[], spec: FlagSpec): boolean { + return argv.some((token) => { + if (!token.startsWith('-')) return false; + // Strip `=value` if present + const bare = token.split('=')[0]; + if (bare === `--${spec.name}` || bare === `--no-${spec.name}`) return true; + if (spec.char && bare === `-${spec.char}`) return true; + return false; + }); +} + +function isCsAssetsOperation(operation: string): boolean { + return operation === OperationType.DELETE || operation === OperationType.MOVE; +} + +/** + * Sentinel operation context for the --retry-failed/--revert path, where no + * --operation flag is given but CS Assets flags must still be rejected. + */ +export const RETRY_REVERT_CONTEXT = 'retry/revert'; + +/** Thrown by enforceOperationFlagMatrix so init() aborts without process.exit(). */ +export class OperationFlagMatrixError extends Error { + readonly violations: string[]; + + constructor(violations: string[]) { + super(violations.join('\n')); + this.name = 'OperationFlagMatrixError'; + this.violations = violations; + } +} + +/** + * Validates that no cross-operation flags were explicitly passed for the resolved + * operation. Returns the list of violation messages (empty when valid). + */ +export function validateOperationFlagMatrix(operation: string, argv: string[]): string[] { + const violations: string[] = []; + + const rejected = isCsAssetsOperation(operation) ? CMS_ONLY_FLAGS : CS_ASSETS_ONLY_FLAGS; + + for (const spec of rejected) { + if (isFlagInArgv(argv, spec)) { + if (operation === RETRY_REVERT_CONTEXT) { + violations.push($t(messages.FLAG_NOT_ALLOWED_WITH_RETRY_REVERT, { flag: `--${spec.name}` })); + } else { + const hint = FLAG_HINTS[spec.name]?.[operation] ?? ''; + violations.push($t(messages.FLAG_NOT_ALLOWED_FOR_OPERATION, { flag: `--${spec.name}`, operation, hint })); + } + } + } + + // move additionally rejects --locale (delete-only within the CS Assets pair) + if (operation === OperationType.MOVE && isFlagInArgv(argv, { name: 'locale' })) { + violations.push(messages.CS_ASSETS_LOCALE_NOT_ALLOWED_FOR_MOVE); + } + + return violations; +} + +/** + * Runs the matrix validation. On violations: logs each one, sets a non-zero exit + * code, and throws OperationFlagMatrixError to abort init(). Throwing (instead of + * process.exit) lets oclif run its catch/finally hooks and keeps this testable. + */ +export function enforceOperationFlagMatrix(operation: string, argv: string[], loggerContext?: unknown): void { + const violations = validateOperationFlagMatrix(operation, argv); + if (violations.length > 0) { + for (const violation of violations) { + log.error(violation, loggerContext); + } + process.exitCode = 1; + throw new OperationFlagMatrixError(violations); + } +} + +/** + * Extracts the value of `--operation` from raw argv without a full oclif parse + * (parse cannot run before init, and init needs the operation to pick a pipeline). + * Returns undefined when the flag is absent. + */ +export function getOperationFromArgv(argv: string[]): string | undefined { + for (let i = 0; i < argv.length; i++) { + const token = argv[i]; + if (token === '--operation') { + return argv[i + 1]; + } + if (token.startsWith('--operation=')) { + return token.slice('--operation='.length); + } + } + return undefined; +} diff --git a/packages/contentstack-bulk-operations/test/unit/commands/bulk-am-assets.test.ts b/packages/contentstack-bulk-operations/test/unit/commands/bulk-assets-cs-assets.test.ts similarity index 53% rename from packages/contentstack-bulk-operations/test/unit/commands/bulk-am-assets.test.ts rename to packages/contentstack-bulk-operations/test/unit/commands/bulk-assets-cs-assets.test.ts index 4f31c7761..b004ba5de 100644 --- a/packages/contentstack-bulk-operations/test/unit/commands/bulk-am-assets.test.ts +++ b/packages/contentstack-bulk-operations/test/unit/commands/bulk-assets-cs-assets.test.ts @@ -2,11 +2,16 @@ import sinon from 'sinon'; import { expect } from 'chai'; import { describe, it, beforeEach, afterEach } from 'mocha'; -import BulkCsAssets from '../../../src/commands/cm/stacks/bulk-am-assets'; +import BulkAssets from '../../../src/commands/cm/stacks/bulk-assets'; -describe('BulkCsAssets command', () => { +/** + * CS Assets delete/move path of the merged cm:stacks:bulk-assets command + * (formerly cm:stacks:bulk-am-assets). + */ +describe('BulkAssets command — CS Assets delete/move path', () => { let sandbox: sinon.SinonSandbox; - let command: BulkCsAssets; + let command: BulkAssets; + let authHandler: any; const baseDeleteFlags = { operation: 'delete', @@ -32,12 +37,24 @@ describe('BulkCsAssets command', () => { Object.defineProperty(command, 'region', { value, configurable: true, writable: true }); } + function setCsAssetsFlags(flags: object): void { + (command as any).csAssetsMode = true; + (command as any).csAssetsFlags = { ...flags }; + (command as any).loggerContext = { module: 'cm:stacks:bulk-assets' }; + } + + function stubAuthSuccess(): void { + sandbox.stub(authHandler, 'getAuthDetails').resolves(); + sandbox.stub(Object.getPrototypeOf(authHandler), 'accessToken').get(() => 'test-token'); + } + beforeEach(() => { sandbox = sinon.createSandbox(); - command = new BulkCsAssets([], {} as any); - (command as any).parsedFlags = { ...baseDeleteFlags }; - (command as any).loggerContext = { module: 'cm:stacks:bulk-am-assets' }; + command = new BulkAssets([], {} as any); + setCsAssetsFlags(baseDeleteFlags); setRegion({}); + + authHandler = require('@contentstack/cli-utilities').authenticationHandler; }); afterEach(() => { @@ -47,7 +64,35 @@ describe('BulkCsAssets command', () => { describe('AM URL validation', () => { it('should set exitCode=1 when AM URL is not configured in region', async () => { - setRegion({}); // no csAssetsUrl + setRegion({}); // no csAssetsUrl + + await command.run(); + + expect(process.exitCode).to.equal(1); + }); + }); + + describe('auth pre-flight', () => { + it('should set exitCode=1 and skip the API when no auth token is available', async () => { + setRegion({ csAssetsUrl: 'https://assets.example.com' }); + sandbox.stub(authHandler, 'getAuthDetails').resolves(); + sandbox.stub(Object.getPrototypeOf(authHandler), 'accessToken').get(() => ''); + + const assetUidsModule = require('../../../src/utils/asset-uids-from-file'); + const loadStub = sandbox.stub(assetUidsModule, 'loadBulkDeleteItemsFromFile'); + const amServiceModule = require('../../../src/services/am-asset-service'); + const deleteStub = sandbox.stub(amServiceModule.CsAssetsService.prototype, 'bulkDelete'); + + await command.run(); + + expect(process.exitCode).to.equal(1); + expect(loadStub.called).to.be.false; // fails before reading any files + expect(deleteStub.called).to.be.false; + }); + + it('should set exitCode=1 when fetching auth details throws', async () => { + setRegion({ csAssetsUrl: 'https://assets.example.com' }); + sandbox.stub(authHandler, 'getAuthDetails').rejects(new Error('not logged in')); await command.run(); @@ -57,8 +102,9 @@ describe('BulkCsAssets command', () => { describe('locale not allowed for move', () => { it('should set exitCode=1 when --locale is passed with --operation move', async () => { - (command as any).parsedFlags = { ...baseMoveFlags, locale: 'en-us' }; + setCsAssetsFlags({ ...baseMoveFlags, locale: 'en-us' }); setRegion({ csAssetsUrl: 'https://assets.example.com' }); + stubAuthSuccess(); // Stub the file loader to confirm it is NOT reached const assetUidsModule = require('../../../src/utils/asset-uids-from-file'); @@ -67,12 +113,13 @@ describe('BulkCsAssets command', () => { await command.run(); expect(process.exitCode).to.equal(1); - expect(loadStub.called).to.be.false; // Should have exited before loading files + expect(loadStub.called).to.be.false; // Should have exited before loading files }); it('should NOT set exitCode when --locale is absent for move and API succeeds', async () => { - (command as any).parsedFlags = { ...baseMoveFlags }; + setCsAssetsFlags({ ...baseMoveFlags }); setRegion({ csAssetsUrl: 'https://assets.example.com' }); + stubAuthSuccess(); const assetUidsModule = require('../../../src/utils/asset-uids-from-file'); sandbox.stub(assetUidsModule, 'loadAssetUidsFromFile').returns(['uid1', 'uid2']); @@ -92,6 +139,7 @@ describe('BulkCsAssets command', () => { describe('delete operation', () => { beforeEach(() => { setRegion({ csAssetsUrl: 'https://assets.example.com' }); + stubAuthSuccess(); }); it('should NOT set exitCode on successful delete', async () => { @@ -125,14 +173,30 @@ describe('BulkCsAssets command', () => { }); }); - describe('BaseCsAssetsCommand isolation — no publish/unpublish infrastructure', () => { - it('should not have bulkOperationConfig, queueManager, or managementStack on the instance', () => { - // BulkCsAssets extends BaseCsAssetsCommand, NOT BaseBulkCommand. - // None of these publish/unpublish properties should exist. + describe('CS Assets path isolation — no publish/unpublish infrastructure', () => { + it('should not touch bulkOperationConfig, queueManager, or managementStack when running delete/move', async () => { + setRegion({ csAssetsUrl: 'https://assets.example.com' }); + stubAuthSuccess(); + + const assetUidsModule = require('../../../src/utils/asset-uids-from-file'); + sandbox.stub(assetUidsModule, 'loadBulkDeleteItemsFromFile').returns([{ uid: 'u1', locale: 'en-us' }]); + const amServiceModule = require('../../../src/services/am-asset-service'); + sandbox.stub(amServiceModule.CsAssetsService.prototype, 'bulkDelete').resolves({ success: true, jobId: 'j1' }); + + await command.run(); + + // The CS Assets path must never initialize the bulk-publish pipeline. expect((command as any).bulkOperationConfig).to.be.undefined; expect((command as any).queueManager).to.be.undefined; expect((command as any).managementStack).to.be.undefined; expect((command as any).rateLimiter).to.be.undefined; }); + + it('shouldSkipBulkPipeline() should reflect csAssetsMode', () => { + (command as any).csAssetsMode = true; + expect((command as any).shouldSkipBulkPipeline()).to.be.true; + (command as any).csAssetsMode = false; + expect((command as any).shouldSkipBulkPipeline()).to.be.false; + }); }); }); diff --git a/packages/contentstack-bulk-operations/test/unit/commands/bulk-assets-init.test.ts b/packages/contentstack-bulk-operations/test/unit/commands/bulk-assets-init.test.ts new file mode 100644 index 000000000..381347841 --- /dev/null +++ b/packages/contentstack-bulk-operations/test/unit/commands/bulk-assets-init.test.ts @@ -0,0 +1,244 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import sinon from 'sinon'; +import { expect } from 'chai'; +import { describe, it, beforeEach, afterEach } from 'mocha'; +import { Command } from '@contentstack/cli-command'; +import BulkAssets from '../../../src/commands/cm/stacks/bulk-assets'; +import { BaseBulkCommand } from '../../../src/base-bulk-command'; +import { OperationFlagMatrixError } from '../../../src/utils/operation-flag-matrix'; + +/** + * Integration tests for the merged command's init() dispatch: operation + * resolution from argv, flag-matrix enforcement, pipeline selection, and the + * interactive prompt fallback. + */ +describe('BulkAssets command — init() dispatch', () => { + let sandbox: sinon.SinonSandbox; + let command: BulkAssets; + // The utils barrel re-exports via getters, so stubs must target the defining modules + let interactiveModule: any; + let logHandlerModule: any; + let cliUtilitiesModule: any; + let logStub: any; + let originalIsTTY: PropertyDescriptor | undefined; + + function setStdinTTY(value: boolean): void { + Object.defineProperty(process.stdin, 'isTTY', { value, configurable: true }); + } + + const csDeleteArgv = [ + '--operation', + 'delete', + '--space-uid', + 'sp1', + '--org-uid', + 'org1', + '--asset-uids-file', + './assets.json', + '--locale', + 'en-us', + '-y', + ]; + + function makeCommand(argv: string[]): BulkAssets { + const cmd = new BulkAssets(argv, {} as any); + (cmd as any).config = { + runHook: sandbox.stub().resolves(), + bin: 'test-bin', + version: '1.0.0', + }; + return cmd; + } + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + cliUtilitiesModule = require('@contentstack/cli-utilities'); + logStub = { + info: sandbox.stub(), + warn: sandbox.stub(), + error: sandbox.stub(), + debug: sandbox.stub(), + success: sandbox.stub(), + }; + sandbox.stub(cliUtilitiesModule, 'log').value(logStub); + sandbox.stub(cliUtilitiesModule, 'handleAndLogError').callsFake(() => {}); + sandbox.stub(cliUtilitiesModule, 'createLogContext').callsFake(() => {}); + + interactiveModule = require('../../../src/utils/interactive'); + logHandlerModule = require('../../../src/utils/bulk-operation-log-handler'); + originalIsTTY = Object.getOwnPropertyDescriptor(process.stdin, 'isTTY'); + + // Stub the framework-level init so no real oclif/CLI setup runs + sandbox.stub(Command.prototype, 'init' as any).resolves(); + }); + + afterEach(() => { + sandbox.restore(); + process.exitCode = undefined; + if (originalIsTTY) { + Object.defineProperty(process.stdin, 'isTTY', originalIsTTY); + } else { + delete (process.stdin as any).isTTY; + } + }); + + describe('delete/move → CS Assets pipeline', () => { + it('skips the bulk pipeline entirely and fills CS Assets flags', async () => { + command = makeCommand([...csDeleteArgv]); + + const parsedFlags = { + operation: 'delete', + 'space-uid': 'sp1', + 'org-uid': 'org1', + 'asset-uids-file': './assets.json', + locale: 'en-us', + workspace: 'main', + yes: true, + }; + sandbox.stub(command as any, 'parse').resolves({ flags: parsedFlags }); + const fillStub = sandbox.stub(interactiveModule, 'fillMissingCsAssetsFlags').resolvesArg(0); + + const buildConfigSpy = sandbox.spy(command as any, 'buildConfiguration'); + const setupStackSpy = sandbox.spy(command as any, 'setupStack'); + + await (command as any).init(); + + expect((command as any).csAssetsMode).to.be.true; + expect((command as any).shouldSkipBulkPipeline()).to.be.true; + expect(fillStub.calledOnce).to.be.true; + expect((command as any).csAssetsFlags).to.deep.equal(parsedFlags); + // Bulk-publish pipeline must never run for CS Assets operations + expect(buildConfigSpy.called).to.be.false; + expect(setupStackSpy.called).to.be.false; + expect((command as any).queueManager).to.be.undefined; + expect((command as any).rateLimiter).to.be.undefined; + }); + }); + + describe('publish/unpublish → CMS pipeline', () => { + it('runs the full BaseBulkCommand pipeline', async () => { + command = makeCommand(['--operation', 'publish', '--environments', 'dev', '--locales', 'en-us', '-k', 'blt1']); + + sandbox.stub(command as any, 'parse').resolves({ + flags: { + operation: 'publish', + environments: ['dev'], + locales: ['en-us'], + 'stack-api-key': 'blt1', + }, + }); + sandbox.stub(interactiveModule, 'fillMissingFlags').resolvesArg(0); + sandbox.stub(logHandlerModule, 'clearLogs').returns(undefined); + const fillCsStub = sandbox.stub(interactiveModule, 'fillMissingCsAssetsFlags'); + + const buildConfigStub = sandbox.stub(command as any, 'buildConfiguration').callsFake(() => { + (command as any).bulkOperationConfig = { + operation: 'publish', + environments: ['dev'], + locales: ['en-us'], + bulkOperationFolder: '/mock/bulk-operation', + }; + return Promise.resolve(); + }); + const setupStackStub = sandbox.stub(command as any, 'setupStack').resolves(); + const initComponentsStub = sandbox.stub(command as any, 'initializeComponents').resolves(); + + await (command as any).init(); + + expect((command as any).csAssetsMode).to.be.false; + expect((command as any).shouldSkipBulkPipeline()).to.be.false; + expect(buildConfigStub.calledOnce).to.be.true; + expect(setupStackStub.calledOnce).to.be.true; + expect(initComponentsStub.calledOnce).to.be.true; + expect(fillCsStub.called).to.be.false; + }); + }); + + describe('flag-matrix enforcement', () => { + it('aborts init with OperationFlagMatrixError when a CMS flag is passed with delete', async () => { + command = makeCommand(['--operation', 'delete', '--environments', 'dev']); + const parseSpy = sandbox.spy(command as any, 'parse'); + + let thrown: unknown; + try { + await (command as any).init(); + } catch (e) { + thrown = e; + } + + expect(thrown).to.be.instanceOf(OperationFlagMatrixError); + expect(process.exitCode).to.equal(1); + expect(logStub.error.called).to.be.true; + expect(parseSpy.called).to.be.false; // aborted before any pipeline work + }); + + it('catch() swallows OperationFlagMatrixError without double-logging', async () => { + command = makeCommand([]); + const baseCatchStub = sandbox.stub(BaseBulkCommand.prototype, 'catch').resolves(); + + await (command as any).catch(new OperationFlagMatrixError(['--environments is not valid'])); + + expect(baseCatchStub.called).to.be.false; // violations already logged by enforce + }); + + it('catch() delegates other errors to the base handler', async () => { + command = makeCommand([]); + const baseCatchStub = sandbox.stub(BaseBulkCommand.prototype, 'catch').resolves(); + + await (command as any).catch(new Error('boom')); + + expect(baseCatchStub.calledOnce).to.be.true; + }); + }); + + describe('operation resolution', () => { + it('throws in non-TTY environments when --operation is missing', async () => { + command = makeCommand([]); + setStdinTTY(false); + + let thrown: unknown; + try { + await (command as any).init(); + } catch (e) { + thrown = e; + } + + expect(thrown).to.be.instanceOf(Error); + expect((thrown as Error).message).to.include('--operation'); + }); + + it('prompts with all four operations and feeds the answer back into argv', async () => { + command = makeCommand([]); + setStdinTTY(true); + + const promptStub = sandbox.stub(interactiveModule, 'promptForOperation').resolves('delete'); + sandbox.stub(command as any, 'parse').resolves({ flags: { operation: 'delete' } }); + sandbox.stub(interactiveModule, 'fillMissingCsAssetsFlags').resolvesArg(0); + + await (command as any).init(); + + expect(promptStub.calledOnce).to.be.true; + const choices = promptStub.firstCall.args[0]; + expect(choices.map((c: any) => c.value)).to.deep.equal(['publish', 'unpublish', 'delete', 'move']); + expect((command as any).argv).to.include.members(['--operation', 'delete']); + expect((command as any).csAssetsMode).to.be.true; + }); + + it('does not prompt on the retry/revert path but still rejects CS Assets flags', async () => { + command = makeCommand(['--retry-failed', './bulk-operation', '--space-uid', 'sp1']); + const promptStub = sandbox.stub(interactiveModule, 'promptForOperation'); + + let thrown: unknown; + try { + await (command as any).init(); + } catch (e) { + thrown = e; + } + + expect(promptStub.called).to.be.false; + expect(thrown).to.be.instanceOf(OperationFlagMatrixError); + expect(logStub.error.firstCall.args[0]).to.include('--retry-failed/--revert'); + }); + }); +}); diff --git a/packages/contentstack-bulk-operations/test/unit/utils/interactive.test.ts b/packages/contentstack-bulk-operations/test/unit/utils/interactive.test.ts index 371a23839..c75cfb1fa 100644 --- a/packages/contentstack-bulk-operations/test/unit/utils/interactive.test.ts +++ b/packages/contentstack-bulk-operations/test/unit/utils/interactive.test.ts @@ -472,13 +472,13 @@ describe('Interactive Prompts', () => { it('should throw in non-TTY when required base flags are missing', async () => { Object.defineProperty(process.stdin, 'isTTY', { value: false, configurable: true }); - const flags = { workspace: 'main', yes: false }; + const flags = { operation: 'delete', workspace: 'main', yes: false }; try { await fillMissingCsAssetsFlags(flags); expect.fail('Should have thrown'); } catch (error: any) { - expect(error.message).to.include('--operation'); + expect(error.message).to.not.include('--operation,'); expect(error.message).to.include('--space-uid'); expect(error.message).to.include('--org-uid'); expect(error.message).to.include('--asset-uids-file'); @@ -525,13 +525,13 @@ describe('Interactive Prompts', () => { it('should prompt for all missing base flags in TTY and show interactive header/footer', async () => { Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true }); - const flags = {}; + // operation is always resolved by the command's init() before this runs + const flags = { operation: 'delete' }; - inquireStub.onCall(0).resolves('delete'); // operation - inquireStub.onCall(1).resolves('sp123'); // space-uid - inquireStub.onCall(2).resolves('org456'); // org-uid - inquireStub.onCall(3).resolves('./assets.json'); // asset-uids-file - inquireStub.onCall(4).resolves('en-us'); // locale (delete-conditional) + inquireStub.onCall(0).resolves('sp123'); // space-uid + inquireStub.onCall(1).resolves('org456'); // org-uid + inquireStub.onCall(2).resolves('./assets.json'); // asset-uids-file + inquireStub.onCall(3).resolves('en-us'); // locale (delete-conditional) const result = await fillMissingCsAssetsFlags(flags); @@ -598,25 +598,22 @@ describe('Interactive Prompts', () => { expect(inquireStub.called).to.be.false; }); - it('should present delete/move choices for the operation prompt', async () => { + it('should never prompt for the operation (resolved by init() before this runs)', async () => { Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true }); const flags = { + operation: 'delete', 'space-uid': 'sp123', 'org-uid': 'org456', 'asset-uids-file': './assets.json', - 'target-folder-uid': 'folderABC', }; - inquireStub.onCall(0).resolves('move'); // operation + inquireStub.onCall(0).resolves('en-us'); // locale await fillMissingCsAssetsFlags(flags); - const operationCall = inquireStub.firstCall.args[0]; - expect(operationCall.type).to.equal('list'); - const values = operationCall.choices.map((c: any) => c.value); - expect(values).to.include('delete'); - expect(values).to.include('move'); + const promptNames = inquireStub.getCalls().map((c: any) => c.args[0].name); + expect(promptNames).to.not.include('operation'); }); it('should validate that space-uid is not blank', async () => { diff --git a/packages/contentstack-bulk-operations/test/unit/utils/operation-flag-matrix.test.ts b/packages/contentstack-bulk-operations/test/unit/utils/operation-flag-matrix.test.ts new file mode 100644 index 000000000..84a01cf8b --- /dev/null +++ b/packages/contentstack-bulk-operations/test/unit/utils/operation-flag-matrix.test.ts @@ -0,0 +1,176 @@ +import sinon from 'sinon'; +import { expect } from 'chai'; +import { describe, it, beforeEach, afterEach } from 'mocha'; +import { + validateOperationFlagMatrix, + enforceOperationFlagMatrix, + getOperationFromArgv, + OperationFlagMatrixError, + RETRY_REVERT_CONTEXT, +} from '../../../src/utils/operation-flag-matrix'; + +describe('operation-flag-matrix', () => { + describe('validateOperationFlagMatrix', () => { + describe('delete/move reject CMS-only flags', () => { + it('rejects --environments with delete', () => { + const violations = validateOperationFlagMatrix('delete', ['--operation', 'delete', '--environments', 'dev']); + expect(violations).to.have.lengthOf(1); + expect(violations[0]).to.include('--environments'); + expect(violations[0]).to.include('delete'); + }); + + it('rejects explicitly passed defaulted flags (--branch, --publish-mode) with delete', () => { + const violations = validateOperationFlagMatrix('delete', [ + '--operation', + 'delete', + '--branch', + 'dev', + '--publish-mode', + 'bulk', + ]); + expect(violations).to.have.lengthOf(2); + expect(violations.join(' ')).to.include('--branch'); + expect(violations.join(' ')).to.include('--publish-mode'); + }); + + it('does NOT trip on flag defaults when they are not in argv', () => { + // --branch defaults to "main" after parse, but was never passed + const violations = validateOperationFlagMatrix('delete', [ + '--operation', + 'delete', + '--space-uid', + 'sp1', + '--org-uid', + 'org1', + '--asset-uids-file', + './assets.json', + '--locale', + 'en-us', + '-y', + ]); + expect(violations).to.be.empty; + }); + + it('rejects short flags -k and -a with move', () => { + const violations = validateOperationFlagMatrix('move', ['-k', 'blt123', '-a', 'myAlias']); + expect(violations).to.have.lengthOf(2); + }); + + it('rejects --flag=value form', () => { + const violations = validateOperationFlagMatrix('delete', ['--environments=dev']); + expect(violations).to.have.lengthOf(1); + }); + + it('rejects --retry-failed and --revert with delete', () => { + const violations = validateOperationFlagMatrix('delete', ['--retry-failed', './log', '--revert', './log']); + expect(violations).to.have.lengthOf(2); + }); + + it('adds a did-you-mean hint for --locales with delete', () => { + const violations = validateOperationFlagMatrix('delete', ['--locales', 'en-us']); + expect(violations).to.have.lengthOf(1); + expect(violations[0]).to.include('--locale?'); + }); + + it('rejects --locale with move (delete-only flag within CS Assets pair)', () => { + const violations = validateOperationFlagMatrix('move', ['--locale', 'en-us']); + expect(violations).to.have.lengthOf(1); + expect(violations[0]).to.include('--locale'); + }); + }); + + describe('publish/unpublish reject CS Assets flags', () => { + it('rejects --space-uid with publish', () => { + const violations = validateOperationFlagMatrix('publish', ['--space-uid', 'sp1']); + expect(violations).to.have.lengthOf(1); + expect(violations[0]).to.include('--space-uid'); + }); + + it('adds a did-you-mean hint for --locale with publish', () => { + const violations = validateOperationFlagMatrix('publish', ['--locale', 'en-us']); + expect(violations).to.have.lengthOf(1); + expect(violations[0]).to.include('--locales?'); + }); + + it('accepts a normal publish invocation', () => { + const violations = validateOperationFlagMatrix('publish', [ + '--operation', + 'publish', + '--environments', + 'dev', + '--locales', + 'en-us', + '-k', + 'blt123', + ]); + expect(violations).to.be.empty; + }); + + it('rejects CS Assets flags on the retry/revert path (no operation given)', () => { + const violations = validateOperationFlagMatrix(RETRY_REVERT_CONTEXT, [ + '--retry-failed', + './log', + '--space-uid', + 'sp1', + ]); + expect(violations).to.have.lengthOf(1); + expect(violations[0]).to.include('--space-uid'); + // retry/revert is not an operation the user typed — message must not present it as one + expect(violations[0]).to.include('--retry-failed/--revert'); + expect(violations[0]).to.not.include('operation "retry/revert"'); + }); + }); + }); + + describe('enforceOperationFlagMatrix', () => { + let sandbox: sinon.SinonSandbox; + let logErrorStub: sinon.SinonStub; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + const cliUtilities = require('@contentstack/cli-utilities'); + logErrorStub = sandbox.stub(); + sandbox.stub(cliUtilities, 'log').value({ error: logErrorStub }); + }); + + afterEach(() => { + sandbox.restore(); + process.exitCode = undefined; + }); + + it('throws OperationFlagMatrixError and sets exitCode=1 on violations (no process.exit)', () => { + let thrown: unknown; + try { + enforceOperationFlagMatrix('delete', ['--environments', 'dev', '--branch', 'main']); + } catch (e) { + thrown = e; + } + + expect(thrown).to.be.instanceOf(OperationFlagMatrixError); + expect((thrown as OperationFlagMatrixError).violations).to.have.lengthOf(2); + expect(process.exitCode).to.equal(1); + expect(logErrorStub.callCount).to.equal(2); // each violation logged individually + }); + + it('does nothing on a valid flag set', () => { + enforceOperationFlagMatrix('delete', ['--operation', 'delete', '--space-uid', 'sp1']); + + expect(process.exitCode).to.not.equal(1); + expect(logErrorStub.called).to.be.false; + }); + }); + + describe('getOperationFromArgv', () => { + it('reads --operation value form', () => { + expect(getOperationFromArgv(['--operation', 'delete'])).to.equal('delete'); + }); + + it('reads --operation=value form', () => { + expect(getOperationFromArgv(['--operation=move'])).to.equal('move'); + }); + + it('returns undefined when absent', () => { + expect(getOperationFromArgv(['--retry-failed', './log'])).to.be.undefined; + }); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c8c990cac..b756676c8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -393,7 +393,7 @@ importers: version: 2.0.0-beta.10(@types/node@20.19.43) '@contentstack/delivery-sdk': specifier: ^5.2.0 - version: 5.2.2 + version: 5.3.0 '@contentstack/management': specifier: ^1.30.2 version: 1.30.4(debug@4.4.3) @@ -1663,76 +1663,76 @@ packages: '@asamuzakjp/dom-selector@2.0.2': resolution: {integrity: sha512-x1KXOatwofR6ZAYzXRBL5wrdV0vwNxlTCK9NCuLqAzQYARqGcvFwiJA6A1ERuh+dgeA4Dxm3JBYictIes+SqUQ==} - '@aws-sdk/checksums@3.1000.16': - resolution: {integrity: sha512-EKnvkXSmz3IpA99tCNuI+dLFXyZyClSm8zns9sB/elvkU+MTuomAs6toJMPMBf98/fICG/urXDkzGz0/c3yyAQ==} + '@aws-sdk/checksums@3.1000.18': + resolution: {integrity: sha512-IImkbEyXdV6/uaF5r6Wkk+8718mQw1ll83j0a4a30R3JM/rHVFdWAiT4jtJpFjJiIwM/oJ6SxIxr0z2TaQUGqw==} engines: {node: '>=20.0.0'} - '@aws-sdk/client-cloudfront@3.1086.0': - resolution: {integrity: sha512-MbqWAu9sNRgnCvuTvVSMgsUD9g4DCvhmiXc5G77/Lq3ty6joCsk/VdQ8naqw/IqMZc3HSVZMTqVugI8yHtq4Rw==} + '@aws-sdk/client-cloudfront@3.1088.0': + resolution: {integrity: sha512-ljqqpLZZrkeWR5U4A42YstyQyRhbcZHfGnh28ovDALlZ24rq+yP2o+nV84OlObjxRoRYYSZUxNga0Qi8aXNJtg==} engines: {node: '>=20.0.0'} - '@aws-sdk/client-s3@3.1086.0': - resolution: {integrity: sha512-6+7mVMPKetZmmF2L1yRJ+rN9b1OwVgc5sju2mj8ixdxuGjtVZ0ekFlcWGBFMQT9gpFk55PPLBng/XRYO3qah5A==} + '@aws-sdk/client-s3@3.1088.0': + resolution: {integrity: sha512-9ryKKxnjtJRKLDI24P+2gQZki7k28BeV6gz1AySvFDWayhjAZuh4QZjCyx55tqR8Oz0IJxWutgJRO43dDOcXoQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/core@3.975.1': - resolution: {integrity: sha512-8qh/6EYb7hl/ZwVfQufhbMEZs1gQIc7GbdrIf4eprQJ7cv042+74nE6l3YDfyWNzb9iPXb8fRyYSHkNIk5eE6Q==} + '@aws-sdk/core@3.975.3': + resolution: {integrity: sha512-7ur3kCKuvPLqlsZ2XlvnNBVQ7KkpSu6Y6dOTwSPHLrFpTEfZM8isLBJc4cgv96WB7GifeVM436mpycwxBd2vEA==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-env@3.972.57': - resolution: {integrity: sha512-1RfJaF7SW1TOnvNGU7kaYjwUf5H3sfm+synGH1bHhRlqcnxCt3szebH3dmKEyY4tuGcbQ6ffzUT89cRitBV8OQ==} + '@aws-sdk/credential-provider-env@3.972.59': + resolution: {integrity: sha512-Ny5e4Mfh3QPmiAc0AiUe+cbTXDlxkU3Rc+EpWOfyWeWEy6yp7Fa1KmfNeCc+1a8by9zQ9gtohmiQUkMPScF3ng==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-http@3.972.59': - resolution: {integrity: sha512-sRCkpTiFnCdQvuaRVjQ6SVoHu6i7RUpurVo1c4F81HWhPvUJ7Wdp5MNtSdX1O29CNXc8em3O5m52hCjVtAD9SA==} + '@aws-sdk/credential-provider-http@3.972.61': + resolution: {integrity: sha512-8jAjgStl5Ytq4+HF3X/9f+EmRinaRbGRRtQGktlPfBRVx73H+R1y48vIeXerQtYGFaUqkEp3fT6jP854rVO2yQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-ini@3.973.1': - resolution: {integrity: sha512-6d8H6ZAh3ZPKZ6fe1nG2OWeZEZPtt9ravoD1dezPdPtsSkJRoxGAnFSHwKT3E/Te6fHE30zRzjV6TD12rvF6yQ==} + '@aws-sdk/credential-provider-ini@3.973.3': + resolution: {integrity: sha512-WpuqYX4gGkx++fCTSWE8+41JzkZVcrI50SH48Ml4CsG1pyuHKyMmpw/FixBHDrmjoQ553PmeCLa/fZIcst+WyA==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-login@3.972.63': - resolution: {integrity: sha512-GREWRrMj0XnNKMaVa/Mauoaui26qBEHu71WWqXbwZOu/jFQOnPZjTf7u0KtGKC8VGa6VUs9kDWGgocrKNLS9vw==} + '@aws-sdk/credential-provider-login@3.972.65': + resolution: {integrity: sha512-xr9rgjYEdmC2Tpg2lwt9o+nOEaK9Qpd+dBjzrVCuWWyQfvhO91Ezu0Hh9ts2VUxOZxmS/k5T9msa34e4R1bnrQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-node@3.972.67': - resolution: {integrity: sha512-oYlzWst56rlhhjbYnexwv5hVLYe1cW4liLObhDfxDLI4RAQzleMVHQgQgx7XsC4HKj4e3kjT8v9DId+Pi/dndw==} + '@aws-sdk/credential-provider-node@3.972.69': + resolution: {integrity: sha512-wbJGGesd0Tl18bmUcbj1xJ+e7CpuRJ6PIpMywLFuUttGy615lua87cJ0EA8pFpY/QgPuUXbnupWBtSPJ9tyZhg==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-process@3.972.57': - resolution: {integrity: sha512-TiVQhuU0pbhIZAUZacbPHMyzrIdiH+lnx+PMY/Pu/b93dJrq3wdZwzUJ0TPpvNxaqbHsxJvQZW3/h/beLiKq7Q==} + '@aws-sdk/credential-provider-process@3.972.59': + resolution: {integrity: sha512-DlZF2/MhLlatDdlrIy3CUCpfdbLrKx+3SMjVo+WyHnPpwzkc/M3vwAHw4OVJf7DMvO+4vfRqSCMc/E9I1auN0g==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-sso@3.973.1': - resolution: {integrity: sha512-3foTZUJ4821Ij60X7K3NJroygiZLnbBmarN+T//O2cjkISan90zElN3NBmgSlDrTQ7Gs6z/yO8V7h60QNcDZHQ==} + '@aws-sdk/credential-provider-sso@3.973.3': + resolution: {integrity: sha512-hmdDHoy2G5Es2e8IgelNMYUuSQI6uCIAKZMJ2u2PdKDhxvbk1uWD/g4+R7R5c/tJfKEB1+KjjWiaoCr/S+ZTiQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-web-identity@3.972.63': - resolution: {integrity: sha512-8qZLFhM69eKcS37m459ctPR05Qimycm/74OPVioe6wNZabMT54GYhwBju0+J656RkMasNSawWQu+c8CmBe3TUQ==} + '@aws-sdk/credential-provider-web-identity@3.972.65': + resolution: {integrity: sha512-gHQb/Kt0chjk/JQDa/GJDqmAvEuVn8n7z10wK2h0LFM9TUDRkohgOO4aEF+s2sBLM0br7Cl5W6P7phgjrrJvLQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-sdk-s3@3.972.62': - resolution: {integrity: sha512-k8JJwYXVYlOOjWnPZDThQS1xDFJgi5Dokt73qFlDtrZAbdcint5aIdjB9XgJAAQVP5OoqcefQmh1FYXiPpvsvw==} + '@aws-sdk/middleware-sdk-s3@3.972.64': + resolution: {integrity: sha512-RBi43anhDBUv+HCfxCOXwGOE7GmT4n7ChV04Mwr22RhXTNcamW/iWnJlOotDPCZSrJ4dEvhZSiWWQMwLX+ZhFA==} engines: {node: '>=20.0.0'} - '@aws-sdk/nested-clients@3.997.31': - resolution: {integrity: sha512-BDHTpwcsZHEBNEJzOg/B1BkFYJxAXY50dau/NyVWs3d51F0WgIUGSWZot/Os+N3KpDhXeaXnz37mWffAvduREw==} + '@aws-sdk/nested-clients@3.997.33': + resolution: {integrity: sha512-dVZOroI/r3/ENvqNGgjMPul+jjlz9GddfVusgTXlVjfZj5isibOxecLkGQbRPp8XOuX+RAfjXLFgPkD1JS5xrw==} engines: {node: '>=20.0.0'} - '@aws-sdk/signature-v4-multi-region@3.996.39': - resolution: {integrity: sha512-8+srXqYIF8KYMLC4FxMLEM5Ek7kUNibJu1R4m8/fUhhNYIZZz26oGtKkCr8I/HiG2fFQxBvaGgQZT4/mqRCSnA==} + '@aws-sdk/signature-v4-multi-region@3.996.41': + resolution: {integrity: sha512-QMUytg+FQMGouc8gHS00KoYih3+N6cqmVI/pQGOIo7Nr7OpQaiXjSYOuL+vsPZ1tymY4LAQ8MYcHJmws5LRxng==} engines: {node: '>=20.0.0'} - '@aws-sdk/token-providers@3.1083.0': - resolution: {integrity: sha512-s0woKnxuHrExLc5L2ArIH5BMkbonHPtt+5hSBM8oknp9M6QTuUmmAmJ2E0EdzCGONrO+8+ADPqvv6UX0nNcc7A==} + '@aws-sdk/token-providers@3.1088.0': + resolution: {integrity: sha512-4ObatWt2qpJg5FBk4LOOKrTQYzaqeewAtdO3r9ZO8lH9YqLtpTzLyIdy0mJ+nVdfYOnqISkKNfmzP22bNDhwyw==} engines: {node: '>=20.0.0'} - '@aws-sdk/types@3.974.0': - resolution: {integrity: sha512-QIBrw90CDm4O0UaIIzkU6DrFdeJzEb2Va5EPEVpyldj6sHJxB6cshhStJuhZxk3wR3PmjJlYsjPmY1kNb+KGBg==} + '@aws-sdk/types@3.974.2': + resolution: {integrity: sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA==} engines: {node: '>=20.0.0'} - '@aws-sdk/xml-builder@3.972.34': - resolution: {integrity: sha512-wHhWL1y7sN3enBA8POrPpQM5jCcmu2ozyhbRei4c8OjVcEaEs6yLucLa/pla457ggS/ysuy7bosagz3HaJkZXA==} + '@aws-sdk/xml-builder@3.972.36': + resolution: {integrity: sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA==} engines: {node: '>=20.0.0'} '@aws/lambda-invoke-store@0.3.0': @@ -2368,8 +2368,8 @@ packages: '@contentstack/core@1.4.1': resolution: {integrity: sha512-QfLa8WUwquWSwvF8EltLyzQTkeNE2I9b9PBkPe21w0d5PnHOagxFzDNCYN4VO/zuJ52sNtKLFIFUcvLsOPk9ww==} - '@contentstack/delivery-sdk@5.2.2': - resolution: {integrity: sha512-hSRYbeWAIq46nkg94LH10jFTmUUJOQF18fw6BTvhfFbK0q98m6QYTLY8BswpQYuhqcH6HNu3wGam+SKzGwSGkg==} + '@contentstack/delivery-sdk@5.3.0': + resolution: {integrity: sha512-jtEV/SOX4iqzQYM4zaWtGhttyl7sio09XMf28FiiJ/TkaneRQNBWK5u2B0CWKHxtxp/nEWYhH2sgUB/HdR56tw==} engines: {node: '>=18'} '@contentstack/json-rte-serializer@2.1.0': @@ -3373,24 +3373,24 @@ packages: Deprecated: no longer maintained and no longer used by Sinon packages. See https://github.com/sinonjs/nise/issues/243 for replacement details. - '@smithy/core@3.29.3': - resolution: {integrity: sha512-L+Ys6ecjk5vwPMAKHBpPKlJ3DkqwNcnfEISXBZIsVvWG/XKXfsAP8mwIYlTeLcd2ElHdesPI8OuOmJSFAPhm6A==} + '@smithy/core@3.29.4': + resolution: {integrity: sha512-G1GRglAabzEhqghJMBAd54FkRS7SAFGHEwbhcI9r+O+LIMuFsLyXkLZkCoFSgAglRu8s/URVXJB0hglq3ZipIg==} engines: {node: '>=18.0.0'} - '@smithy/credential-provider-imds@4.4.8': - resolution: {integrity: sha512-q9J7JTiXrAhB8sDp4px97uEPT7CwKH61Co78grdNQvU8QZAdiuaSRhP0tUVf2ogy36RZTrlMU1rBmDEH+cnkiA==} + '@smithy/credential-provider-imds@4.4.9': + resolution: {integrity: sha512-2nfV4qRKiYeXU4zD2vvSCfg5dfp/BuhrM73vt7q9gzBhxs4rbPxXY21wo+kyI3bRmXcEGRnCLTaW8O437jzHIg==} engines: {node: '>=18.0.0'} - '@smithy/fetch-http-handler@5.6.5': - resolution: {integrity: sha512-SuqeisTyPoiIPtIYru/sGxGyXzmZ+8nnFOhC+qRPglt06Ebd1yH//CDltZB2J/3WBNVhwfUaZ0EtHB3cm2X32g==} + '@smithy/fetch-http-handler@5.6.6': + resolution: {integrity: sha512-NHLgAlORUFZjn5ZfhYuyyKMlXA1WLYOdGxEhyNxrPpbJzoacGbl0chn1lN2KiZ8mpNVk0tV5607CSYlYs/OFgw==} engines: {node: '>=18.0.0'} - '@smithy/node-http-handler@4.9.5': - resolution: {integrity: sha512-bNqdxTQTxmLbomSmlkZFz8L6B/feQ2HHzw4L2zY7Ecp2XffYAZq2uzdWDdxJHJFbEvqd+SRuluJso0P8+xPdbw==} + '@smithy/node-http-handler@4.9.6': + resolution: {integrity: sha512-odd+HYx3OLcXRSEz0ZeF3JQdSYdK8QnRgA2N87cPW7coWIbKfRk7a9VQjfeWQLqnzrDLk23KMEn46p8N7M/JFg==} engines: {node: '>=18.0.0'} - '@smithy/signature-v4@5.6.4': - resolution: {integrity: sha512-B89bpf2t/y/wia6LZ+4JfHXYQT9PnVftsH05rgJKKIStS7r/4XSs9HOjtPoLtgcA6HCW9jVqX5DBbq7E0PAkiQ==} + '@smithy/signature-v4@5.6.5': + resolution: {integrity: sha512-MO5VEhwVl0BN7xVoVeNrZfiUFoQtqxUbgl6/RwOTlMMxCSjblG8twSrVTwz3J4w9WZxd2rBfBAUXjH77agspBg==} engines: {node: '>=18.0.0'} '@smithy/types@4.16.1': @@ -4483,8 +4483,8 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001805: - resolution: {integrity: sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA==} + caniuse-lite@1.0.30001806: + resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==} capital-case@1.0.4: resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} @@ -5117,8 +5117,8 @@ packages: engines: {node: '>=0.10.0'} hasBin: true - electron-to-chromium@1.5.389: - resolution: {integrity: sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==} + electron-to-chromium@1.5.392: + resolution: {integrity: sha512-1yQq3VQCZRwsnYc67Oc+1fge6Lwtn0hzi6zmEVkB61Zx21kTbwJAW4dFLadl5Rc1tKhG/kSpYXnfiAhu0f0a1g==} elegant-spinner@1.0.1: resolution: {integrity: sha512-B+ZM+RXvRqQaAmkMlO/oSe5nMUOaUnyfGYCEHoR8wrXsZR2mA0XVibsxV1bvTwxdRWah1PkQqso2EzhILGHtEQ==} @@ -9152,8 +9152,8 @@ packages: resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - ws@8.21.0: - resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + ws@8.21.1: + resolution: {integrity: sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -9282,176 +9282,176 @@ snapshots: css-tree: 2.3.1 is-potential-custom-element-name: 1.0.1 - '@aws-sdk/checksums@3.1000.16': + '@aws-sdk/checksums@3.1000.18': dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.3 + '@aws-sdk/core': 3.975.3 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.4 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/client-cloudfront@3.1086.0': + '@aws-sdk/client-cloudfront@3.1088.0': dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/credential-provider-node': 3.972.67 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.3 - '@smithy/fetch-http-handler': 5.6.5 - '@smithy/node-http-handler': 4.9.5 + '@aws-sdk/core': 3.975.3 + '@aws-sdk/credential-provider-node': 3.972.69 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.4 + '@smithy/fetch-http-handler': 5.6.6 + '@smithy/node-http-handler': 4.9.6 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/client-s3@3.1086.0': - dependencies: - '@aws-sdk/checksums': 3.1000.16 - '@aws-sdk/core': 3.975.1 - '@aws-sdk/credential-provider-node': 3.972.67 - '@aws-sdk/middleware-sdk-s3': 3.972.62 - '@aws-sdk/signature-v4-multi-region': 3.996.39 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.3 - '@smithy/fetch-http-handler': 5.6.5 - '@smithy/node-http-handler': 4.9.5 + '@aws-sdk/client-s3@3.1088.0': + dependencies: + '@aws-sdk/checksums': 3.1000.18 + '@aws-sdk/core': 3.975.3 + '@aws-sdk/credential-provider-node': 3.972.69 + '@aws-sdk/middleware-sdk-s3': 3.972.64 + '@aws-sdk/signature-v4-multi-region': 3.996.41 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.4 + '@smithy/fetch-http-handler': 5.6.6 + '@smithy/node-http-handler': 4.9.6 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/core@3.975.1': + '@aws-sdk/core@3.975.3': dependencies: - '@aws-sdk/types': 3.974.0 - '@aws-sdk/xml-builder': 3.972.34 + '@aws-sdk/types': 3.974.2 + '@aws-sdk/xml-builder': 3.972.36 '@aws/lambda-invoke-store': 0.3.0 - '@smithy/core': 3.29.3 - '@smithy/signature-v4': 5.6.4 + '@smithy/core': 3.29.4 + '@smithy/signature-v4': 5.6.5 '@smithy/types': 4.16.1 bowser: 2.14.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-env@3.972.57': + '@aws-sdk/credential-provider-env@3.972.59': dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.3 + '@aws-sdk/core': 3.975.3 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.4 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-http@3.972.59': + '@aws-sdk/credential-provider-http@3.972.61': dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.3 - '@smithy/fetch-http-handler': 5.6.5 - '@smithy/node-http-handler': 4.9.5 + '@aws-sdk/core': 3.975.3 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.4 + '@smithy/fetch-http-handler': 5.6.6 + '@smithy/node-http-handler': 4.9.6 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-ini@3.973.1': - dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/credential-provider-env': 3.972.57 - '@aws-sdk/credential-provider-http': 3.972.59 - '@aws-sdk/credential-provider-login': 3.972.63 - '@aws-sdk/credential-provider-process': 3.972.57 - '@aws-sdk/credential-provider-sso': 3.973.1 - '@aws-sdk/credential-provider-web-identity': 3.972.63 - '@aws-sdk/nested-clients': 3.997.31 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.3 - '@smithy/credential-provider-imds': 4.4.8 + '@aws-sdk/credential-provider-ini@3.973.3': + dependencies: + '@aws-sdk/core': 3.975.3 + '@aws-sdk/credential-provider-env': 3.972.59 + '@aws-sdk/credential-provider-http': 3.972.61 + '@aws-sdk/credential-provider-login': 3.972.65 + '@aws-sdk/credential-provider-process': 3.972.59 + '@aws-sdk/credential-provider-sso': 3.973.3 + '@aws-sdk/credential-provider-web-identity': 3.972.65 + '@aws-sdk/nested-clients': 3.997.33 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.4 + '@smithy/credential-provider-imds': 4.4.9 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-login@3.972.63': + '@aws-sdk/credential-provider-login@3.972.65': dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/nested-clients': 3.997.31 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.3 + '@aws-sdk/core': 3.975.3 + '@aws-sdk/nested-clients': 3.997.33 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.4 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-node@3.972.67': - dependencies: - '@aws-sdk/credential-provider-env': 3.972.57 - '@aws-sdk/credential-provider-http': 3.972.59 - '@aws-sdk/credential-provider-ini': 3.973.1 - '@aws-sdk/credential-provider-process': 3.972.57 - '@aws-sdk/credential-provider-sso': 3.973.1 - '@aws-sdk/credential-provider-web-identity': 3.972.63 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.3 - '@smithy/credential-provider-imds': 4.4.8 + '@aws-sdk/credential-provider-node@3.972.69': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.59 + '@aws-sdk/credential-provider-http': 3.972.61 + '@aws-sdk/credential-provider-ini': 3.973.3 + '@aws-sdk/credential-provider-process': 3.972.59 + '@aws-sdk/credential-provider-sso': 3.973.3 + '@aws-sdk/credential-provider-web-identity': 3.972.65 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.4 + '@smithy/credential-provider-imds': 4.4.9 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-process@3.972.57': + '@aws-sdk/credential-provider-process@3.972.59': dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.3 + '@aws-sdk/core': 3.975.3 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.4 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-sso@3.973.1': + '@aws-sdk/credential-provider-sso@3.973.3': dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/nested-clients': 3.997.31 - '@aws-sdk/token-providers': 3.1083.0 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.3 + '@aws-sdk/core': 3.975.3 + '@aws-sdk/nested-clients': 3.997.33 + '@aws-sdk/token-providers': 3.1088.0 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.4 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-web-identity@3.972.63': + '@aws-sdk/credential-provider-web-identity@3.972.65': dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/nested-clients': 3.997.31 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.3 + '@aws-sdk/core': 3.975.3 + '@aws-sdk/nested-clients': 3.997.33 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.4 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/middleware-sdk-s3@3.972.62': + '@aws-sdk/middleware-sdk-s3@3.972.64': dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/signature-v4-multi-region': 3.996.39 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.3 + '@aws-sdk/core': 3.975.3 + '@aws-sdk/signature-v4-multi-region': 3.996.41 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.4 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/nested-clients@3.997.31': + '@aws-sdk/nested-clients@3.997.33': dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/signature-v4-multi-region': 3.996.39 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.3 - '@smithy/fetch-http-handler': 5.6.5 - '@smithy/node-http-handler': 4.9.5 + '@aws-sdk/core': 3.975.3 + '@aws-sdk/signature-v4-multi-region': 3.996.41 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.4 + '@smithy/fetch-http-handler': 5.6.6 + '@smithy/node-http-handler': 4.9.6 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/signature-v4-multi-region@3.996.39': + '@aws-sdk/signature-v4-multi-region@3.996.41': dependencies: - '@aws-sdk/types': 3.974.0 - '@smithy/signature-v4': 5.6.4 + '@aws-sdk/types': 3.974.2 + '@smithy/signature-v4': 5.6.5 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/token-providers@3.1083.0': + '@aws-sdk/token-providers@3.1088.0': dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/nested-clients': 3.997.31 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.3 + '@aws-sdk/core': 3.975.3 + '@aws-sdk/nested-clients': 3.997.33 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.4 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/types@3.974.0': + '@aws-sdk/types@3.974.2': dependencies: '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/xml-builder@3.972.34': + '@aws-sdk/xml-builder@3.972.36': dependencies: '@smithy/types': 4.16.1 tslib: 2.8.1 @@ -10587,7 +10587,7 @@ snapshots: - debug - supports-color - '@contentstack/delivery-sdk@5.2.2': + '@contentstack/delivery-sdk@5.3.0': dependencies: '@contentstack/core': 1.4.1 '@contentstack/utils': 1.9.1 @@ -10638,7 +10638,7 @@ snapshots: '@contentstack/types-generator@3.10.2(graphql@16.14.2)': dependencies: - '@contentstack/delivery-sdk': 5.2.2 + '@contentstack/delivery-sdk': 5.3.0 '@gql2ts/from-schema': 2.0.0-4(graphql@16.14.2) async: 3.2.6 axios: 1.18.0 @@ -12281,32 +12281,32 @@ snapshots: '@sinonjs/text-encoding@0.7.3': {} - '@smithy/core@3.29.3': + '@smithy/core@3.29.4': dependencies: '@smithy/types': 4.16.1 tslib: 2.8.1 - '@smithy/credential-provider-imds@4.4.8': + '@smithy/credential-provider-imds@4.4.9': dependencies: - '@smithy/core': 3.29.3 + '@smithy/core': 3.29.4 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@smithy/fetch-http-handler@5.6.5': + '@smithy/fetch-http-handler@5.6.6': dependencies: - '@smithy/core': 3.29.3 + '@smithy/core': 3.29.4 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@smithy/node-http-handler@4.9.5': + '@smithy/node-http-handler@4.9.6': dependencies: - '@smithy/core': 3.29.3 + '@smithy/core': 3.29.4 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@smithy/signature-v4@5.6.4': + '@smithy/signature-v4@5.6.5': dependencies: - '@smithy/core': 3.29.3 + '@smithy/core': 3.29.4 '@smithy/types': 4.16.1 tslib: 2.8.1 @@ -13825,8 +13825,8 @@ snapshots: browserslist@4.28.6: dependencies: baseline-browser-mapping: 2.10.43 - caniuse-lite: 1.0.30001805 - electron-to-chromium: 1.5.389 + caniuse-lite: 1.0.30001806 + electron-to-chromium: 1.5.392 node-releases: 2.0.51 update-browserslist-db: 1.2.3(browserslist@4.28.6) @@ -13911,7 +13911,7 @@ snapshots: camelcase@6.3.0: {} - caniuse-lite@1.0.30001805: {} + caniuse-lite@1.0.30001806: {} capital-case@1.0.4: dependencies: @@ -14584,7 +14584,7 @@ snapshots: dependencies: jake: 10.9.4 - electron-to-chromium@1.5.389: {} + electron-to-chromium@1.5.392: {} elegant-spinner@1.0.1: {} @@ -17318,7 +17318,7 @@ snapshots: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 - ws: 8.21.0 + ws: 8.21.1 xml-name-validator: 5.0.0 transitivePeerDependencies: - bufferutil @@ -18017,8 +18017,8 @@ snapshots: oclif@4.23.27(@types/node@14.18.63): dependencies: - '@aws-sdk/client-cloudfront': 3.1086.0 - '@aws-sdk/client-s3': 3.1086.0 + '@aws-sdk/client-cloudfront': 3.1088.0 + '@aws-sdk/client-s3': 3.1088.0 '@inquirer/confirm': 3.2.0 '@inquirer/input': 2.3.0 '@inquirer/select': 2.5.0 @@ -18046,8 +18046,8 @@ snapshots: oclif@4.23.27(@types/node@18.19.130): dependencies: - '@aws-sdk/client-cloudfront': 3.1086.0 - '@aws-sdk/client-s3': 3.1086.0 + '@aws-sdk/client-cloudfront': 3.1088.0 + '@aws-sdk/client-s3': 3.1088.0 '@inquirer/confirm': 3.2.0 '@inquirer/input': 2.3.0 '@inquirer/select': 2.5.0 @@ -18075,8 +18075,8 @@ snapshots: oclif@4.23.27(@types/node@20.19.43): dependencies: - '@aws-sdk/client-cloudfront': 3.1086.0 - '@aws-sdk/client-s3': 3.1086.0 + '@aws-sdk/client-cloudfront': 3.1088.0 + '@aws-sdk/client-s3': 3.1088.0 '@inquirer/confirm': 3.2.0 '@inquirer/input': 2.3.0 '@inquirer/select': 2.5.0 @@ -18104,8 +18104,8 @@ snapshots: oclif@4.23.27(@types/node@22.20.1): dependencies: - '@aws-sdk/client-cloudfront': 3.1086.0 - '@aws-sdk/client-s3': 3.1086.0 + '@aws-sdk/client-cloudfront': 3.1088.0 + '@aws-sdk/client-s3': 3.1088.0 '@inquirer/confirm': 3.2.0 '@inquirer/input': 2.3.0 '@inquirer/select': 2.5.0 @@ -20209,7 +20209,7 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 4.1.0 - ws@8.21.0: {} + ws@8.21.1: {} xdg-basedir@4.0.0: {}