From 8827f5735f457edbaaa22b2f3048559b3f1be74f Mon Sep 17 00:00:00 2001 From: naman-contentstack Date: Sat, 30 May 2026 15:00:29 +0530 Subject: [PATCH 1/5] fix: minor fixes in bulk delete and move ops --- .../src/base-bulk-command.ts | 26 ++++-- .../src/commands/cm/stacks/bulk-am-assets.ts | 55 +++++++------ .../src/interfaces/index.ts | 1 + .../src/messages/index.ts | 19 ++++- .../src/utils/index.ts | 3 +- .../src/utils/interactive.ts | 79 +++++++++++++++++++ 6 files changed, 150 insertions(+), 33 deletions(-) diff --git a/packages/contentstack-bulk-operations/src/base-bulk-command.ts b/packages/contentstack-bulk-operations/src/base-bulk-command.ts index e5f0bc62c..db3e8debb 100644 --- a/packages/contentstack-bulk-operations/src/base-bulk-command.ts +++ b/packages/contentstack-bulk-operations/src/base-bulk-command.ts @@ -28,6 +28,7 @@ import { buildBulkModeResult, handleOperationError, fillMissingFlags, + fillMissingAmFlags, getLogPaths, clearLogs, generateBulkPublishStatusUrl, @@ -51,7 +52,7 @@ import { * Provides common functionality for bulk-entries and bulk-assets */ export abstract class BaseBulkCommand extends Command { - protected abstract resourceType: ResourceType; + protected resourceType?: ResourceType; // Common flags for all bulk operations static baseFlags: FlagInput = { @@ -145,6 +146,15 @@ export abstract class BaseBulkCommand extends Command { this.parsedFlags = flags; + // AM assets uses a different API surface — prompt for AM-specific flags and skip + // the publish/unpublish stack setup, queue init, and config build. + if (this.resourceType === ResourceType.AM_ASSET) { + this.logger = log; + this.loggerContext = { module: this.id }; + this.parsedFlags = await fillMissingAmFlags(flags); + return; + } + const commandName = `cm:stacks:bulk-${this.resourceType === ResourceType.ENTRY ? 'entries' : 'assets'}`; createLogContext( this.context?.info?.command || commandName, @@ -177,7 +187,7 @@ export abstract class BaseBulkCommand extends Command { await this.setupStack(); await this.initializeComponents(); - this.logger.debug($t(messages.INITIALIZING, { resourceType: this.resourceType }), this.loggerContext); + this.logger.debug($t(messages.INITIALIZING, { resourceType: this.resourceType! }), this.loggerContext); } /** @@ -189,7 +199,7 @@ export abstract class BaseBulkCommand extends Command { const isRetry = !!flags['retry-failed']; // Load config from log file - const logFileConfig = loadConfigFromLogFile(logPath, isRetry, this.resourceType); + const logFileConfig = loadConfigFromLogFile(logPath, isRetry, this.resourceType!); if (!logFileConfig) { throw new Error($t(messages.NO_CONFIG_IN_LOG)); @@ -334,7 +344,7 @@ export abstract class BaseBulkCommand extends Command { batchResults: this.batchResults, logger: this.logger, retryStrategy: this.retryStrategy, - resourceType: this.resourceType, + resourceType: this.resourceType!, logFolderPath: this.bulkOperationConfig.bulkOperationFolder, apiKey: this.bulkOperationConfig.apiKey || this.bulkOperationConfig.stackApiKey, branch: this.bulkOperationConfig.branch, @@ -352,7 +362,7 @@ export abstract class BaseBulkCommand extends Command { const flags = this.parsedFlags || (await this.parse(this.constructor as typeof BaseBulkCommand)).flags; const itemCount = items?.length || 0; - return await confirmOperationUtil(this.bulkOperationConfig, itemCount, this.resourceType, flags.yes); + return await confirmOperationUtil(this.bulkOperationConfig, itemCount, this.resourceType!, flags.yes); } /** @@ -493,7 +503,7 @@ export abstract class BaseBulkCommand extends Command { const result = await handleRevertOrRetry( logPath, isRetry, - this.resourceType, + this.resourceType!, this.bulkOperationConfig, flags.yes, this.executeBulkOperation.bind(this), @@ -518,14 +528,14 @@ export abstract class BaseBulkCommand extends Command { targetEnvs: flags.environments as string[], locales: flags.locales as string[], contentTypes: flags['content-types'] as string[] | undefined, - resourceType: this.resourceType, + resourceType: this.resourceType!, deliveryStack: this.deliveryStack!, // Required: initialized via source-alias delivery token }, this.logger ); if (itemsToPublish.length === 0) { - this.logger.warn($t(messages.NO_ITEMS_FOUND, { resourceType: this.resourceType }), this.loggerContext); + this.logger.warn($t(messages.NO_ITEMS_FOUND, { resourceType: this.resourceType! }), this.loggerContext); return; } 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 index f1b0a7bf2..bde0f65f8 100644 --- 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 @@ -1,15 +1,15 @@ import chalk from 'chalk'; -import { Command } from '@contentstack/cli-command'; -import { flags, log, createLogContext, handleAndLogError, cliux, FlagInput } from '@contentstack/cli-utilities'; +import { flags, log, createLogContext, cliux, handleAndLogError, FlagInput } from '@contentstack/cli-utilities'; import messages, { $t } from '../../../messages'; +import { BaseBulkCommand } from '../../../base-bulk-command'; import { AmAssetService } from '../../../services'; import { loadAssetUidsFromFile, loadBulkDeleteItemsFromFile, LoadAssetUidsError, } from '../../../utils/asset-uids-from-file'; -import { AmBulkDeleteItem } from '../../../interfaces'; +import { AmBulkDeleteItem, ResourceType } from '../../../interfaces'; const COMMAND_ID = 'cm:stacks:bulk-am-assets'; @@ -18,7 +18,7 @@ type RegionWithOptionalAmUrl = { csAssetsUrl?: string }; /** * AM bulk delete (job) / bulk move — CS Assets API only; asset UIDs come from a JSON file `{ "uids": [...] }`. */ -export default class BulkAmAssets extends Command { +export default class BulkAmAssets extends BaseBulkCommand { static description = messages.BULK_AM_ASSETS_DESCRIPTION; static examples = [ @@ -31,15 +31,12 @@ export default class BulkAmAssets extends Command { operation: flags.string({ description: messages.AM_OPERATION_FLAG, options: ['delete', 'move'], - required: true, }), 'space-uid': flags.string({ description: messages.AM_SPACE_UID_FLAG, - required: true, }), 'org-uid': flags.string({ description: messages.AM_ORG_UID_FLAG, - required: true, }), workspace: flags.string({ default: 'main', @@ -47,7 +44,6 @@ export default class BulkAmAssets extends Command { }), 'asset-uids-file': flags.string({ description: messages.AM_ASSET_UIDS_FILE_FLAG, - required: true, }), locale: flags.string({ description: messages.AM_LOCALE_FLAG, @@ -62,7 +58,24 @@ export default class BulkAmAssets extends Command { }), }; - private readonly loggerContext = { module: COMMAND_ID }; + protected resourceType = ResourceType.AM_ASSET; + + private printAmSummary(op: 'delete' | 'move', opts: { jobId?: string; count?: number; folderUid?: string; notice?: string; error?: string }): void { + if (opts.error) { + log.error($t(messages.AM_OPERATION_FAILED, { operation: op }), this.loggerContext); + log.error(opts.error, this.loggerContext); + } else if (op === 'delete') { + log.success($t(messages.AM_DELETE_SUCCESS), this.loggerContext); + if (opts.jobId) log.info($t(messages.AM_DELETE_JOB_ID, { jobId: opts.jobId }), this.loggerContext); + log.info($t(messages.AM_DELETE_ASYNC_NOTE), this.loggerContext); + } else { + log.success($t(messages.AM_MOVE_SUCCESS), this.loggerContext); + if (opts.count !== undefined && opts.folderUid) { + log.info($t(messages.AM_MOVE_ASSETS_COUNT, { count: opts.count, folderUid: opts.folderUid }), this.loggerContext); + } + } + if (opts.notice) log.info(opts.notice, this.loggerContext); + } private handleAssetUidsFileError(e: LoadAssetUidsError): void { const pathShown = e.filePath; @@ -79,7 +92,7 @@ export default class BulkAmAssets extends Command { async run(): Promise { try { - const { flags: f } = await this.parse(BulkAmAssets); + const f = this.parsedFlags; const amBaseUrl = (this.region as RegionWithOptionalAmUrl).csAssetsUrl?.trim(); if (!amBaseUrl) { @@ -166,16 +179,17 @@ export default class BulkAmAssets extends Command { log.info($t(messages.AM_DELETING_ASSETS, { count: deleteRows.length, spaceUid }), this.loggerContext); const result = await amService.bulkDelete(spaceUid, workspace, deleteRows); if (!result.success) { - log.error(result.error ?? 'AM bulk delete failed', this.loggerContext); + this.printAmSummary('delete', { error: result.error ?? 'AM bulk delete failed' }); process.exitCode = 1; return; } - if (result.notice) { - log.info($t(messages.AM_OPERATION_NOTICE, { notice: result.notice }), this.loggerContext); - } - if (result.jobId) { - log.info($t(messages.AM_DELETE_SUBMITTED, { jobId: result.jobId }), this.loggerContext); - } + this.printAmSummary('delete', { jobId: result.jobId, notice: result.notice }); + return; + } + + if (f.locale) { + log.error($t(messages.AM_LOCALE_NOT_ALLOWED_FOR_MOVE), this.loggerContext); + process.exitCode = 1; return; } @@ -231,14 +245,11 @@ export default class BulkAmAssets extends Command { ); const result = await amService.bulkMove(spaceUid, workspace, uids, moveFolderUid); if (!result.success) { - log.error(result.error ?? 'AM bulk move failed', this.loggerContext); + this.printAmSummary('move', { error: result.error ?? 'AM bulk move failed' }); process.exitCode = 1; return; } - if (result.notice) { - log.info($t(messages.AM_OPERATION_NOTICE, { notice: result.notice }), this.loggerContext); - } - log.info($t(messages.AM_MOVE_SUBMITTED), this.loggerContext); + this.printAmSummary('move', { count: uids.length, folderUid: moveFolderUid, notice: result.notice }); } catch (error) { handleAndLogError(error); } diff --git a/packages/contentstack-bulk-operations/src/interfaces/index.ts b/packages/contentstack-bulk-operations/src/interfaces/index.ts index b5a18fe71..e311f8dcf 100644 --- a/packages/contentstack-bulk-operations/src/interfaces/index.ts +++ b/packages/contentstack-bulk-operations/src/interfaces/index.ts @@ -20,6 +20,7 @@ export enum ResourceType { ENTRY = 'entry', ASSET = 'asset', TAXONOMY = 'taxonomy', + AM_ASSET = 'am-asset', } export enum FilterType { diff --git a/packages/contentstack-bulk-operations/src/messages/index.ts b/packages/contentstack-bulk-operations/src/messages/index.ts index a285f734e..388247738 100644 --- a/packages/contentstack-bulk-operations/src/messages/index.ts +++ b/packages/contentstack-bulk-operations/src/messages/index.ts @@ -238,10 +238,25 @@ const amBulkAssetsMsg = { AM_WORKSPACE_FLAG: 'AM workspace query parameter (default: main)', AM_ASSET_UIDS_FILE_FLAG: 'Path to UTF-8 JSON file: exactly `{ "uids": ["uid1", "uid2"] }` (non-empty string array, no trimming; large lists: see docs for NODE_OPTIONS)', - AM_LOCALE_FLAG: 'Locale code for bulk delete (single locale per run)', - AM_TARGET_FOLDER_FLAG: 'Destination AM folder UID (required for move)', + AM_LOCALE_FLAG: 'Locale code for bulk delete only (single locale per run). Not applicable for move — move always relocates all locale variants of an asset.', + AM_LOCALE_NOT_ALLOWED_FOR_MOVE: '--locale is not applicable for the move operation. Move always relocates all locale variants of an asset. Remove --locale and try again.', + AM_TARGET_FOLDER_FLAG: 'Destination AM folder UID for bulk move. Use "root" to move assets to the root folder.', AM_INVALID_OPERATION: 'Invalid operation: {operation}. Must be delete or move', AM_CONFIRM_SUMMARY: 'Proceed with AM {operation} on {count} item(s)?', + AM_DELETE_SUCCESS: 'AM bulk delete job submitted successfully!', + AM_DELETE_JOB_ID: 'Job ID: {jobId}', + AM_DELETE_ASYNC_NOTE: 'The job runs asynchronously — check the Asset Management console for status.', + AM_MOVE_SUCCESS: 'AM bulk move completed successfully!', + AM_MOVE_ASSETS_COUNT: '{count} asset(s) moved to folder: {folderUid}', + AM_OPERATION_FAILED: 'AM {operation} failed.', + + // Interactive prompts + AM_SELECT_OPERATION: 'Select AM operation:', + AM_ENTER_SPACE_UID: 'Enter AM space UID:', + AM_ENTER_ORG_UID: 'Enter organization UID:', + AM_ENTER_ASSET_UIDS_FILE: 'Enter path to asset UIDs JSON file (e.g. ./assets.json):', + AM_ENTER_LOCALE: 'Enter locale code for bulk delete (e.g. en-us):', + AM_ENTER_TARGET_FOLDER: 'Enter target folder UID for bulk move (use "root" to move to the root folder):', }; /** diff --git a/packages/contentstack-bulk-operations/src/utils/index.ts b/packages/contentstack-bulk-operations/src/utils/index.ts index 02c334e3e..9f2b6e310 100644 --- a/packages/contentstack-bulk-operations/src/utils/index.ts +++ b/packages/contentstack-bulk-operations/src/utils/index.ts @@ -35,7 +35,7 @@ import { buildBulkModeResult, handleOperationError, } from './command-helpers'; -import { fillMissingFlags } from './interactive'; +import { fillMissingFlags, fillMissingAmFlags } from './interactive'; import { RATE_LIMITER_CONSTANTS, RETRY_STRATEGY_CONSTANTS, @@ -98,6 +98,7 @@ export { buildBulkModeResult, handleOperationError, fillMissingFlags, + fillMissingAmFlags, 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 8794efae6..61bb486c5 100644 --- a/packages/contentstack-bulk-operations/src/utils/interactive.ts +++ b/packages/contentstack-bulk-operations/src/utils/interactive.ts @@ -223,3 +223,82 @@ export async function fillMissingFlags(flags: any): Promise { return updatedFlags; } + +/** + * Fills in missing flags for the bulk-am-assets command by prompting the user. + * Handles AM-specific required flags including operation-conditional ones + * (locale for delete, target-folder-uid for move). + */ +export async function fillMissingAmFlags(flags: any): Promise { + const f = { ...flags }; + + 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; + + if (!needsPrompt) return f; + + cliux.print(messages.INTERACTIVE_MODE_START, { color: 'cyan' }); + + if (!f.operation) { + f.operation = await cliux.inquire({ + type: 'list', + name: 'operation', + message: messages.AM_SELECT_OPERATION, + choices: [ + { name: 'Delete (AM bulk delete)', value: 'delete' }, + { name: 'Move (AM bulk move)', value: 'move' }, + ], + }); + } + + if (!f['space-uid']) { + f['space-uid'] = await cliux.inquire({ + type: 'input', + name: 'spaceUid', + message: messages.AM_ENTER_SPACE_UID, + validate: (v: string) => (!v?.trim() ? messages.SPACE_UID_REQUIRED : true), + }); + } + + if (!f['org-uid']) { + f['org-uid'] = await cliux.inquire({ + type: 'input', + name: 'orgUid', + message: messages.AM_ENTER_ORG_UID, + validate: (v: string) => (!v?.trim() ? messages.ORG_UID_REQUIRED : true), + }); + } + + if (!f['asset-uids-file']) { + f['asset-uids-file'] = await cliux.inquire({ + type: 'input', + name: 'assetUidsFile', + message: messages.AM_ENTER_ASSET_UIDS_FILE, + validate: (v: string) => (!v?.trim() ? messages.AM_ASSET_UIDS_FILE_REQUIRED : true), + }); + } + + if (f.operation === 'delete' && !f.locale) { + f.locale = await cliux.inquire({ + type: 'input', + name: 'locale', + message: messages.AM_ENTER_LOCALE, + validate: (v: string) => (!v?.trim() ? messages.AM_LOCALE_REQUIRED : true), + }); + } + + if (f.operation === 'move' && !f['target-folder-uid']) { + f['target-folder-uid'] = await cliux.inquire({ + type: 'input', + name: 'targetFolderUid', + message: messages.AM_ENTER_TARGET_FOLDER, + validate: (v: string) => (!v?.trim() ? messages.TARGET_FOLDER_REQUIRED : true), + }); + } + + cliux.print(messages.INTERACTIVE_MODE_COMPLETE, { color: 'green' }); + + return f; +} From 2108c79f5a8ee18483360b8571c56d785a695f0a Mon Sep 17 00:00:00 2001 From: naman-contentstack Date: Sat, 30 May 2026 15:32:47 +0530 Subject: [PATCH 2/5] feat: implement BaseAmCommand and related functionality for asset management operations --- .talismanrc | 8 +- .../src/base-am-command.ts | 29 +++ .../src/base-bulk-command.ts | 31 +-- .../src/commands/cm/stacks/bulk-am-assets.ts | 31 +-- .../src/interfaces/index.ts | 12 + .../src/utils/interactive.ts | 148 +++++++----- .../test/unit/commands/bulk-am-assets.test.ts | 138 +++++++++++ .../test/unit/utils/interactive.test.ts | 222 ++++++++++++++++++ 8 files changed, 509 insertions(+), 110 deletions(-) create mode 100644 packages/contentstack-bulk-operations/src/base-am-command.ts create mode 100644 packages/contentstack-bulk-operations/test/unit/commands/bulk-am-assets.test.ts diff --git a/.talismanrc b/.talismanrc index 3db7ed059..ebd1224b7 100644 --- a/.talismanrc +++ b/.talismanrc @@ -1,8 +1,4 @@ fileignoreconfig: -- filename: packages/contentstack-bulk-operations/src/services/am-asset-service.ts - checksum: 5f6c0ecba74e27399a7079ca15e65e77ef692697093c9fb1d57213728c4fe985 -- filename: packages/contentstack-bulk-operations/src/utils/asset-uids-from-file.ts - checksum: 580932f192dd3fdd8bb2c55b7a7a78f1694f646ef5c5041f86c75668778f7ecb -- filename: packages/contentstack-bulk-operations/test/unit/utils/asset-uids-from-file.test.ts - checksum: 8123f7a675a0275795b59b15d0f2d5f8f1e57ccbecf3f97249a0dc5a037b9203 +- filename: packages/contentstack-bulk-operations/test/unit/commands/bulk-am-assets.test.ts + checksum: a26defc1f356308c3f46607e68714bfa2de174e58237cb6b949bd43f2494a818 version: '1.0' diff --git a/packages/contentstack-bulk-operations/src/base-am-command.ts b/packages/contentstack-bulk-operations/src/base-am-command.ts new file mode 100644 index 000000000..67ce99cb9 --- /dev/null +++ b/packages/contentstack-bulk-operations/src/base-am-command.ts @@ -0,0 +1,29 @@ +import { Command } from '@contentstack/cli-command'; +import { handleAndLogError } from '@contentstack/cli-utilities'; + +import { fillMissingAmFlags } from './utils'; +import type { AmAssetFlags } from './interfaces'; + +/** + * Thin base command for Asset Management operations. + * Handles flag prompting in init() and exposes typed parsedFlags / loggerContext. + * Deliberately does NOT inherit BaseBulkCommand — AM operations use a different API + * surface with no stack setup, queue managers, or rate limiters. + */ +export abstract class BaseAmCommand extends Command { + protected parsedFlags!: AmAssetFlags; + protected loggerContext!: { module: string }; + + protected async init(): Promise { + await super.init(); + const { flags } = await this.parse(this.constructor as typeof BaseAmCommand); + this.loggerContext = { module: this.id ?? 'cm:stacks:bulk-am-assets' }; + this.parsedFlags = (await fillMissingAmFlags(flags)) as AmAssetFlags; + } + + 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 db3e8debb..370b44a94 100644 --- a/packages/contentstack-bulk-operations/src/base-bulk-command.ts +++ b/packages/contentstack-bulk-operations/src/base-bulk-command.ts @@ -28,7 +28,6 @@ import { buildBulkModeResult, handleOperationError, fillMissingFlags, - fillMissingAmFlags, getLogPaths, clearLogs, generateBulkPublishStatusUrl, @@ -52,7 +51,7 @@ import { * Provides common functionality for bulk-entries and bulk-assets */ export abstract class BaseBulkCommand extends Command { - protected resourceType?: ResourceType; + protected abstract resourceType: ResourceType; // Common flags for all bulk operations static baseFlags: FlagInput = { @@ -146,24 +145,14 @@ export abstract class BaseBulkCommand extends Command { this.parsedFlags = flags; - // AM assets uses a different API surface — prompt for AM-specific flags and skip - // the publish/unpublish stack setup, queue init, and config build. - if (this.resourceType === ResourceType.AM_ASSET) { - this.logger = log; - this.loggerContext = { module: this.id }; - this.parsedFlags = await fillMissingAmFlags(flags); - return; - } - - const commandName = `cm:stacks:bulk-${this.resourceType === ResourceType.ENTRY ? 'entries' : 'assets'}`; createLogContext( - this.context?.info?.command || commandName, + this.context?.info?.command || this.id, flags['stack-api-key'] || '', flags.alias ? 'Management Token' : 'Basic Auth' ); this.logger = log; - this.loggerContext = { module: commandName }; + this.loggerContext = { module: this.id }; // Check for revert/retry EARLY - all config comes from log file const isRevertOrRetry = flags.revert || flags['retry-failed']; @@ -187,7 +176,7 @@ export abstract class BaseBulkCommand extends Command { await this.setupStack(); await this.initializeComponents(); - this.logger.debug($t(messages.INITIALIZING, { resourceType: this.resourceType! }), this.loggerContext); + this.logger.debug($t(messages.INITIALIZING, { resourceType: this.resourceType }), this.loggerContext); } /** @@ -199,7 +188,7 @@ export abstract class BaseBulkCommand extends Command { const isRetry = !!flags['retry-failed']; // Load config from log file - const logFileConfig = loadConfigFromLogFile(logPath, isRetry, this.resourceType!); + const logFileConfig = loadConfigFromLogFile(logPath, isRetry, this.resourceType); if (!logFileConfig) { throw new Error($t(messages.NO_CONFIG_IN_LOG)); @@ -344,7 +333,7 @@ export abstract class BaseBulkCommand extends Command { batchResults: this.batchResults, logger: this.logger, retryStrategy: this.retryStrategy, - resourceType: this.resourceType!, + resourceType: this.resourceType, logFolderPath: this.bulkOperationConfig.bulkOperationFolder, apiKey: this.bulkOperationConfig.apiKey || this.bulkOperationConfig.stackApiKey, branch: this.bulkOperationConfig.branch, @@ -362,7 +351,7 @@ export abstract class BaseBulkCommand extends Command { const flags = this.parsedFlags || (await this.parse(this.constructor as typeof BaseBulkCommand)).flags; const itemCount = items?.length || 0; - return await confirmOperationUtil(this.bulkOperationConfig, itemCount, this.resourceType!, flags.yes); + return await confirmOperationUtil(this.bulkOperationConfig, itemCount, this.resourceType, flags.yes); } /** @@ -503,7 +492,7 @@ export abstract class BaseBulkCommand extends Command { const result = await handleRevertOrRetry( logPath, isRetry, - this.resourceType!, + this.resourceType, this.bulkOperationConfig, flags.yes, this.executeBulkOperation.bind(this), @@ -528,14 +517,14 @@ export abstract class BaseBulkCommand extends Command { targetEnvs: flags.environments as string[], locales: flags.locales as string[], contentTypes: flags['content-types'] as string[] | undefined, - resourceType: this.resourceType!, + resourceType: this.resourceType, deliveryStack: this.deliveryStack!, // Required: initialized via source-alias delivery token }, this.logger ); if (itemsToPublish.length === 0) { - this.logger.warn($t(messages.NO_ITEMS_FOUND, { resourceType: this.resourceType! }), this.loggerContext); + this.logger.warn($t(messages.NO_ITEMS_FOUND, { resourceType: this.resourceType }), this.loggerContext); return; } 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 index bde0f65f8..7d1a3118c 100644 --- 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 @@ -2,14 +2,14 @@ import chalk from 'chalk'; import { flags, log, createLogContext, cliux, handleAndLogError, FlagInput } from '@contentstack/cli-utilities'; import messages, { $t } from '../../../messages'; -import { BaseBulkCommand } from '../../../base-bulk-command'; +import { BaseAmCommand } from '../../../base-am-command'; import { AmAssetService } from '../../../services'; import { loadAssetUidsFromFile, loadBulkDeleteItemsFromFile, LoadAssetUidsError, } from '../../../utils/asset-uids-from-file'; -import { AmBulkDeleteItem, ResourceType } from '../../../interfaces'; +import { AmBulkDeleteItem } from '../../../interfaces'; const COMMAND_ID = 'cm:stacks:bulk-am-assets'; @@ -18,7 +18,7 @@ type RegionWithOptionalAmUrl = { csAssetsUrl?: string }; /** * AM bulk delete (job) / bulk move — CS Assets API only; asset UIDs come from a JSON file `{ "uids": [...] }`. */ -export default class BulkAmAssets extends BaseBulkCommand { +export default class BulkAmAssets extends BaseAmCommand { static description = messages.BULK_AM_ASSETS_DESCRIPTION; static examples = [ @@ -58,8 +58,6 @@ export default class BulkAmAssets extends BaseBulkCommand { }), }; - protected resourceType = ResourceType.AM_ASSET; - private printAmSummary(op: 'delete' | 'move', opts: { jobId?: string; count?: number; folderUid?: string; notice?: string; error?: string }): void { if (opts.error) { log.error($t(messages.AM_OPERATION_FAILED, { operation: op }), this.loggerContext); @@ -108,26 +106,9 @@ export default class BulkAmAssets extends BaseBulkCommand { return; } - const spaceUid = (f['space-uid'] ?? '').trim(); - if (!spaceUid) { - log.error($t(messages.SPACE_UID_REQUIRED), this.loggerContext); - process.exitCode = 1; - return; - } - - const orgUid = (f['org-uid'] ?? '').trim(); - if (!orgUid) { - log.error($t(messages.ORG_UID_REQUIRED), this.loggerContext); - process.exitCode = 1; - return; - } - - const assetUidsPath = (f['asset-uids-file'] ?? '').trim(); - if (!assetUidsPath) { - log.error($t(messages.AM_ASSET_UIDS_FILE_REQUIRED), 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: AmBulkDeleteItem[]; diff --git a/packages/contentstack-bulk-operations/src/interfaces/index.ts b/packages/contentstack-bulk-operations/src/interfaces/index.ts index e311f8dcf..c13dbc3c4 100644 --- a/packages/contentstack-bulk-operations/src/interfaces/index.ts +++ b/packages/contentstack-bulk-operations/src/interfaces/index.ts @@ -271,6 +271,18 @@ export interface AmBulkOperationResult { error?: string; } +/** Typed flags for the bulk-am-assets command. */ +export interface AmAssetFlags { + operation: string; + 'space-uid': string; + 'org-uid': string; + workspace: string; + 'asset-uids-file': string; + locale?: string; + 'target-folder-uid'?: string; + yes: boolean; +} + export interface BulkJobResult { success: number; failed: number; diff --git a/packages/contentstack-bulk-operations/src/utils/interactive.ts b/packages/contentstack-bulk-operations/src/utils/interactive.ts index 61bb486c5..4bccb0a91 100644 --- a/packages/contentstack-bulk-operations/src/utils/interactive.ts +++ b/packages/contentstack-bulk-operations/src/utils/interactive.ts @@ -224,10 +224,21 @@ export async function fillMissingFlags(flags: any): Promise { return updatedFlags; } +/** + * Runs a sequence of prompt functions wrapped in the standard interactive mode header/footer. + * Each prompt is a no-op if its condition is already satisfied (value already in flags). + */ +async function runInteractivePrompts(prompts: Array<() => Promise>): Promise { + cliux.print(messages.INTERACTIVE_MODE_START, { color: 'cyan' }); + for (const prompt of prompts) await prompt(); + cliux.print(messages.INTERACTIVE_MODE_COMPLETE, { color: 'green' }); +} + /** * Fills in missing flags for the bulk-am-assets command by prompting the user. * Handles AM-specific required flags including operation-conditional ones * (locale for delete, target-folder-uid for move). + * Throws in non-TTY environments when required flags are missing. */ export async function fillMissingAmFlags(flags: any): Promise { const f = { ...flags }; @@ -239,66 +250,87 @@ export async function fillMissingAmFlags(flags: any): Promise { if (!needsPrompt) return f; - cliux.print(messages.INTERACTIVE_MODE_START, { color: 'cyan' }); - - if (!f.operation) { - f.operation = await cliux.inquire({ - type: 'list', - name: 'operation', - message: messages.AM_SELECT_OPERATION, - choices: [ - { name: 'Delete (AM bulk delete)', value: 'delete' }, - { name: 'Move (AM bulk move)', value: 'move' }, - ], - }); - } - - if (!f['space-uid']) { - f['space-uid'] = await cliux.inquire({ - type: 'input', - name: 'spaceUid', - message: messages.AM_ENTER_SPACE_UID, - validate: (v: string) => (!v?.trim() ? messages.SPACE_UID_REQUIRED : true), - }); - } - - if (!f['org-uid']) { - f['org-uid'] = await cliux.inquire({ - type: 'input', - name: 'orgUid', - message: messages.AM_ENTER_ORG_UID, - validate: (v: string) => (!v?.trim() ? messages.ORG_UID_REQUIRED : true), - }); - } - - if (!f['asset-uids-file']) { - f['asset-uids-file'] = await cliux.inquire({ - type: 'input', - name: 'assetUidsFile', - message: messages.AM_ENTER_ASSET_UIDS_FILE, - validate: (v: string) => (!v?.trim() ? messages.AM_ASSET_UIDS_FILE_REQUIRED : true), - }); + // 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', + (f.operation === 'delete' && !f.locale) && '--locale', + (f.operation === 'move' && !f['target-folder-uid']) && '--target-folder-uid', + ].filter(Boolean); + throw new Error( + `Missing required flag(s): ${missing.join(', ')}. Provide all required flags when running in a non-interactive environment.` + ); } - if (f.operation === 'delete' && !f.locale) { - f.locale = await cliux.inquire({ - type: 'input', - name: 'locale', - message: messages.AM_ENTER_LOCALE, - validate: (v: string) => (!v?.trim() ? messages.AM_LOCALE_REQUIRED : true), - }); - } - - if (f.operation === 'move' && !f['target-folder-uid']) { - f['target-folder-uid'] = await cliux.inquire({ - type: 'input', - name: 'targetFolderUid', - message: messages.AM_ENTER_TARGET_FOLDER, - validate: (v: string) => (!v?.trim() ? messages.TARGET_FOLDER_REQUIRED : true), - }); - } - - cliux.print(messages.INTERACTIVE_MODE_COMPLETE, { color: 'green' }); + await runInteractivePrompts([ + async () => { + if (!f.operation) { + f.operation = await cliux.inquire({ + type: 'list', + name: 'operation', + message: messages.AM_SELECT_OPERATION, + choices: [ + { name: 'Delete (AM bulk delete)', value: 'delete' }, + { name: 'Move (AM bulk move)', value: 'move' }, + ], + }); + } + }, + async () => { + if (!f['space-uid']) { + f['space-uid'] = await cliux.inquire({ + type: 'input', + name: 'spaceUid', + message: messages.AM_ENTER_SPACE_UID, + validate: (v: string) => (!v?.trim() ? messages.SPACE_UID_REQUIRED : true), + }); + } + }, + async () => { + if (!f['org-uid']) { + f['org-uid'] = await cliux.inquire({ + type: 'input', + name: 'orgUid', + message: messages.AM_ENTER_ORG_UID, + validate: (v: string) => (!v?.trim() ? messages.ORG_UID_REQUIRED : true), + }); + } + }, + async () => { + if (!f['asset-uids-file']) { + f['asset-uids-file'] = await cliux.inquire({ + type: 'input', + name: 'assetUidsFile', + message: messages.AM_ENTER_ASSET_UIDS_FILE, + validate: (v: string) => (!v?.trim() ? messages.AM_ASSET_UIDS_FILE_REQUIRED : true), + }); + } + }, + // Conditional prompts run after operation is resolved (captured by closure) + async () => { + if (f.operation === 'delete' && !f.locale) { + f.locale = await cliux.inquire({ + type: 'input', + name: 'locale', + message: messages.AM_ENTER_LOCALE, + validate: (v: string) => (!v?.trim() ? messages.AM_LOCALE_REQUIRED : true), + }); + } + }, + async () => { + if (f.operation === 'move' && !f['target-folder-uid']) { + f['target-folder-uid'] = await cliux.inquire({ + type: 'input', + name: 'targetFolderUid', + message: messages.AM_ENTER_TARGET_FOLDER, + validate: (v: string) => (!v?.trim() ? messages.TARGET_FOLDER_REQUIRED : true), + }); + } + }, + ]); return f; } diff --git a/packages/contentstack-bulk-operations/test/unit/commands/bulk-am-assets.test.ts b/packages/contentstack-bulk-operations/test/unit/commands/bulk-am-assets.test.ts new file mode 100644 index 000000000..0787bfeee --- /dev/null +++ b/packages/contentstack-bulk-operations/test/unit/commands/bulk-am-assets.test.ts @@ -0,0 +1,138 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import sinon from 'sinon'; +import { expect } from 'chai'; +import { describe, it, beforeEach, afterEach } from 'mocha'; +import BulkAmAssets from '../../../src/commands/cm/stacks/bulk-am-assets'; + +describe('BulkAmAssets command', () => { + let sandbox: sinon.SinonSandbox; + let command: BulkAmAssets; + + const baseDeleteFlags = { + operation: 'delete', + 'space-uid': 'sp123', + 'org-uid': 'org456', + 'asset-uids-file': './assets.json', + locale: 'en-us', + workspace: 'main', + yes: true, + }; + + const baseMoveFlags = { + operation: 'move', + 'space-uid': 'sp123', + 'org-uid': 'org456', + 'asset-uids-file': './assets.json', + 'target-folder-uid': 'folderABC', + workspace: 'main', + yes: true, + }; + + function setRegion(value: object): void { + Object.defineProperty(command, 'region', { value, configurable: true, writable: true }); + } + + beforeEach(() => { + sandbox = sinon.createSandbox(); + command = new BulkAmAssets([], {} as any); + (command as any).parsedFlags = { ...baseDeleteFlags }; + (command as any).loggerContext = { module: 'cm:stacks:bulk-am-assets' }; + setRegion({}); + }); + + afterEach(() => { + sandbox.restore(); + process.exitCode = undefined; + }); + + describe('AM URL validation', () => { + it('should set exitCode=1 when AM URL is not configured in region', async () => { + setRegion({}); // no csAssetsUrl + + await command.run(); + + expect(process.exitCode).to.equal(1); + }); + }); + + 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' }; + setRegion({ csAssetsUrl: 'https://assets.example.com' }); + + // Stub the file loader to confirm it is NOT reached + const assetUidsModule = require('../../../src/utils/asset-uids-from-file'); + const loadStub = sandbox.stub(assetUidsModule, 'loadAssetUidsFromFile'); + + await command.run(); + + expect(process.exitCode).to.equal(1); + 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 }; + setRegion({ csAssetsUrl: 'https://assets.example.com' }); + + const assetUidsModule = require('../../../src/utils/asset-uids-from-file'); + sandbox.stub(assetUidsModule, 'loadAssetUidsFromFile').returns(['uid1', 'uid2']); + + const amServiceModule = require('../../../src/services/am-asset-service'); + sandbox.stub(amServiceModule.AmAssetService.prototype, 'bulkMove').resolves({ + success: true, + notice: undefined, + }); + + await command.run(); + + expect(process.exitCode).to.not.equal(1); + }); + }); + + describe('delete operation', () => { + beforeEach(() => { + setRegion({ csAssetsUrl: 'https://assets.example.com' }); + }); + + it('should NOT set exitCode on successful delete', async () => { + 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.AmAssetService.prototype, 'bulkDelete').resolves({ + success: true, + jobId: 'job-abc-123', + }); + + await command.run(); + + expect(process.exitCode).to.not.equal(1); + }); + + it('should set exitCode=1 on failed delete', async () => { + 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.AmAssetService.prototype, 'bulkDelete').resolves({ + success: false, + error: 'API rate limit exceeded', + }); + + await command.run(); + + expect(process.exitCode).to.equal(1); + }); + }); + + describe('BaseAmCommand isolation — no publish/unpublish infrastructure', () => { + it('should not have bulkOperationConfig, queueManager, or managementStack on the instance', () => { + // BulkAmAssets extends BaseAmCommand, NOT BaseBulkCommand. + // None of these publish/unpublish properties should exist. + 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; + }); + }); +}); 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 36afee32f..20a5ea9fc 100644 --- a/packages/contentstack-bulk-operations/test/unit/utils/interactive.test.ts +++ b/packages/contentstack-bulk-operations/test/unit/utils/interactive.test.ts @@ -416,4 +416,226 @@ describe('Interactive Prompts', () => { expect(validValidation).to.be.true; }); }); + + describe('fillMissingAmFlags', () => { + // We need to import fillMissingAmFlags separately + let fillMissingAmFlags: typeof import('../../../src/utils/interactive').fillMissingAmFlags; + let originalIsTTY: boolean | undefined; + + before(async () => { + ({ fillMissingAmFlags } = await import('../../../src/utils/interactive')); + }); + + beforeEach(() => { + originalIsTTY = process.stdin.isTTY; + }); + + afterEach(() => { + Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, configurable: true }); + }); + + it('should return flags unchanged when all required flags are provided (delete)', async () => { + const flags = { + operation: 'delete', + 'space-uid': 'sp123', + 'org-uid': 'org456', + 'asset-uids-file': './assets.json', + locale: 'en-us', + workspace: 'main', + yes: false, + }; + + const result = await fillMissingAmFlags(flags); + + expect(result).to.deep.equal(flags); + expect(inquireStub.called).to.be.false; + expect(printStub.called).to.be.false; + }); + + it('should return flags unchanged when all required flags are provided (move)', async () => { + const flags = { + operation: 'move', + 'space-uid': 'sp123', + 'org-uid': 'org456', + 'asset-uids-file': './assets.json', + 'target-folder-uid': 'folderABC', + workspace: 'main', + yes: false, + }; + + const result = await fillMissingAmFlags(flags); + + expect(result).to.deep.equal(flags); + expect(inquireStub.called).to.be.false; + }); + + 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 }; + + try { + await fillMissingAmFlags(flags); + expect.fail('Should have thrown'); + } catch (error: any) { + expect(error.message).to.include('--operation'); + expect(error.message).to.include('--space-uid'); + expect(error.message).to.include('--org-uid'); + expect(error.message).to.include('--asset-uids-file'); + expect(error.message).to.include('non-interactive'); + } + }); + + it('should throw in non-TTY and include --locale when operation=delete and locale missing', async () => { + Object.defineProperty(process.stdin, 'isTTY', { value: false, configurable: true }); + + const flags = { + operation: 'delete', + 'space-uid': 'sp123', + 'org-uid': 'org456', + 'asset-uids-file': './assets.json', + }; + + try { + await fillMissingAmFlags(flags); + expect.fail('Should have thrown'); + } catch (error: any) { + expect(error.message).to.include('--locale'); + } + }); + + it('should throw in non-TTY and include --target-folder-uid when operation=move and folder missing', async () => { + Object.defineProperty(process.stdin, 'isTTY', { value: false, configurable: true }); + + const flags = { + operation: 'move', + 'space-uid': 'sp123', + 'org-uid': 'org456', + 'asset-uids-file': './assets.json', + }; + + try { + await fillMissingAmFlags(flags); + expect.fail('Should have thrown'); + } catch (error: any) { + expect(error.message).to.include('--target-folder-uid'); + } + }); + + 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 = {}; + + 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) + + const result = await fillMissingAmFlags(flags); + + expect(result.operation).to.equal('delete'); + expect(result['space-uid']).to.equal('sp123'); + expect(result['org-uid']).to.equal('org456'); + expect(result['asset-uids-file']).to.equal('./assets.json'); + expect(result.locale).to.equal('en-us'); + expect(printStub.calledTwice).to.be.true; + }); + + it('should prompt for locale only when operation=delete and locale is missing', 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', + }; + + inquireStub.onCall(0).resolves('en-us'); // locale + + const result = await fillMissingAmFlags(flags); + + expect(result.locale).to.equal('en-us'); + expect(inquireStub.calledOnce).to.be.true; + expect(inquireStub.firstCall.args[0].name).to.equal('locale'); + }); + + it('should prompt for target-folder-uid only when operation=move and folder is missing', async () => { + Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true }); + + const flags = { + operation: 'move', + 'space-uid': 'sp123', + 'org-uid': 'org456', + 'asset-uids-file': './assets.json', + }; + + inquireStub.onCall(0).resolves('folderABC'); // target-folder-uid + + const result = await fillMissingAmFlags(flags); + + expect(result['target-folder-uid']).to.equal('folderABC'); + expect(inquireStub.calledOnce).to.be.true; + expect(inquireStub.firstCall.args[0].name).to.equal('targetFolderUid'); + }); + + it('should NOT prompt for locale when operation=move', async () => { + Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true }); + + const flags = { + operation: 'move', + 'space-uid': 'sp123', + 'org-uid': 'org456', + 'asset-uids-file': './assets.json', + 'target-folder-uid': 'folderABC', + }; + + const result = await fillMissingAmFlags(flags); + + expect(result.locale).to.be.undefined; + expect(inquireStub.called).to.be.false; + }); + + it('should present delete/move choices for the operation prompt', async () => { + Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true }); + + const flags = { + 'space-uid': 'sp123', + 'org-uid': 'org456', + 'asset-uids-file': './assets.json', + 'target-folder-uid': 'folderABC', + }; + + inquireStub.onCall(0).resolves('move'); // operation + + await fillMissingAmFlags(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'); + }); + + it('should validate that space-uid is not blank', async () => { + Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true }); + + const flags = { + operation: 'delete', + 'org-uid': 'org456', + 'asset-uids-file': './assets.json', + locale: 'en-us', + }; + + inquireStub.onCall(0).resolves('sp123'); + + await fillMissingAmFlags(flags); + + const spaceUidCall = inquireStub.firstCall.args[0]; + expect(spaceUidCall.validate('')).to.not.equal(true); + expect(spaceUidCall.validate('sp123')).to.equal(true); + }); + }); }); From c16270e546b6e117b744e7ac0c14c301acc2a250 Mon Sep 17 00:00:00 2001 From: naman-contentstack Date: Tue, 2 Jun 2026 14:49:16 +0530 Subject: [PATCH 3/5] fix: add success url in bulk delete and move --- .../src/commands/cm/stacks/bulk-am-assets.ts | 15 ++++++++++----- .../src/messages/index.ts | 2 +- .../src/utils/bulk-publish-url-generator.ts | 15 ++++++++++++++- 3 files changed, 25 insertions(+), 7 deletions(-) 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 index 7d1a3118c..0ed63de11 100644 --- 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 @@ -9,6 +9,7 @@ import { loadBulkDeleteItemsFromFile, LoadAssetUidsError, } from '../../../utils/asset-uids-from-file'; +import { generateAmJobStatusUrl } from '../../../utils/bulk-publish-url-generator'; import { AmBulkDeleteItem } from '../../../interfaces'; const COMMAND_ID = 'cm:stacks:bulk-am-assets'; @@ -58,7 +59,7 @@ export default class BulkAmAssets extends BaseAmCommand { }), }; - private printAmSummary(op: 'delete' | 'move', opts: { jobId?: string; count?: number; folderUid?: string; notice?: string; error?: string }): void { + private printAmSummary(op: 'delete' | 'move', opts: { jobId?: string; count?: number; folderUid?: string; notice?: string; error?: string; spaceUid?: string }): void { if (opts.error) { log.error($t(messages.AM_OPERATION_FAILED, { operation: op }), this.loggerContext); log.error(opts.error, this.loggerContext); @@ -66,11 +67,15 @@ export default class BulkAmAssets extends BaseAmCommand { log.success($t(messages.AM_DELETE_SUCCESS), this.loggerContext); if (opts.jobId) log.info($t(messages.AM_DELETE_JOB_ID, { jobId: opts.jobId }), this.loggerContext); log.info($t(messages.AM_DELETE_ASYNC_NOTE), this.loggerContext); + const statusUrl = generateAmJobStatusUrl(opts.spaceUid); + if (statusUrl) log.info(statusUrl, this.loggerContext); } else { log.success($t(messages.AM_MOVE_SUCCESS), this.loggerContext); if (opts.count !== undefined && opts.folderUid) { log.info($t(messages.AM_MOVE_ASSETS_COUNT, { count: opts.count, folderUid: opts.folderUid }), this.loggerContext); } + const statusUrl = generateAmJobStatusUrl(opts.spaceUid); + if (statusUrl) log.info(statusUrl, this.loggerContext); } if (opts.notice) log.info(opts.notice, this.loggerContext); } @@ -160,11 +165,11 @@ export default class BulkAmAssets extends BaseAmCommand { log.info($t(messages.AM_DELETING_ASSETS, { count: deleteRows.length, spaceUid }), this.loggerContext); const result = await amService.bulkDelete(spaceUid, workspace, deleteRows); if (!result.success) { - this.printAmSummary('delete', { error: result.error ?? 'AM bulk delete failed' }); + this.printAmSummary('delete', { error: result.error ?? 'AM bulk delete failed', spaceUid }); process.exitCode = 1; return; } - this.printAmSummary('delete', { jobId: result.jobId, notice: result.notice }); + this.printAmSummary('delete', { jobId: result.jobId, notice: result.notice, spaceUid }); return; } @@ -226,11 +231,11 @@ export default class BulkAmAssets extends BaseAmCommand { ); const result = await amService.bulkMove(spaceUid, workspace, uids, moveFolderUid); if (!result.success) { - this.printAmSummary('move', { error: result.error ?? 'AM bulk move failed' }); + this.printAmSummary('move', { error: result.error ?? 'AM bulk move failed', spaceUid }); process.exitCode = 1; return; } - this.printAmSummary('move', { count: uids.length, folderUid: moveFolderUid, notice: result.notice }); + this.printAmSummary('move', { count: uids.length, folderUid: moveFolderUid, notice: result.notice, spaceUid }); } catch (error) { handleAndLogError(error); } diff --git a/packages/contentstack-bulk-operations/src/messages/index.ts b/packages/contentstack-bulk-operations/src/messages/index.ts index 388247738..d9fb94f93 100644 --- a/packages/contentstack-bulk-operations/src/messages/index.ts +++ b/packages/contentstack-bulk-operations/src/messages/index.ts @@ -245,7 +245,7 @@ const amBulkAssetsMsg = { AM_CONFIRM_SUMMARY: 'Proceed with AM {operation} on {count} item(s)?', AM_DELETE_SUCCESS: 'AM bulk delete job submitted successfully!', AM_DELETE_JOB_ID: 'Job ID: {jobId}', - AM_DELETE_ASYNC_NOTE: 'The job runs asynchronously — check the Asset Management console for status.', + AM_DELETE_ASYNC_NOTE: 'The job runs asynchronously — check the bulk task queue for status:', AM_MOVE_SUCCESS: 'AM bulk move completed successfully!', AM_MOVE_ASSETS_COUNT: '{count} asset(s) moved to folder: {folderUid}', AM_OPERATION_FAILED: 'AM {operation} failed.', diff --git a/packages/contentstack-bulk-operations/src/utils/bulk-publish-url-generator.ts b/packages/contentstack-bulk-operations/src/utils/bulk-publish-url-generator.ts index 5ccd5b906..a59448020 100644 --- a/packages/contentstack-bulk-operations/src/utils/bulk-publish-url-generator.ts +++ b/packages/contentstack-bulk-operations/src/utils/bulk-publish-url-generator.ts @@ -20,7 +20,6 @@ function getAppUrlFromHost(): string { * Generate the bulk publish status URL based on stack configuration * @param apiKey - Stack API key * @param branch - Branch name (optional) - * @param host - Host URL (optional) * @returns The status URL or null if apiKey is not available */ export function generateBulkPublishStatusUrl(apiKey?: string, branch?: string): string | null { @@ -34,3 +33,17 @@ export function generateBulkPublishStatusUrl(apiKey?: string, branch?: string): const branchParam = branch && branch !== 'main' ? `?branch=${branch}` : ''; return `${appUrl}/#!/stack/${apiKey}/publish-queue${branchParam}`; } + +/** + * Generate the AM bulk task queue URL for checking job status + * @param spaceUid - AM space UID + * @returns The AM job status URL or null if spaceUid is not available + */ +export function generateAmJobStatusUrl(spaceUid?: string): string | null { + if (!spaceUid) { + return null; + } + + const appUrl = getAppUrlFromHost(); + return `${appUrl}/#!/asset-management/spaces/${spaceUid}/space-settings/bulk-task-queue`; +} From b589b1be113490b11bfe914b2e501d5354f8a20e Mon Sep 17 00:00:00 2001 From: naman-contentstack Date: Tue, 2 Jun 2026 14:51:43 +0530 Subject: [PATCH 4/5] chore: fix lint issues --- .../src/commands/cm/stacks/bulk-am-assets.ts | 10 ++++++++-- .../contentstack-bulk-operations/src/messages/index.ts | 6 ++++-- .../src/utils/interactive.ts | 4 ++-- 3 files changed, 14 insertions(+), 6 deletions(-) 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 index 0ed63de11..c64bf16b1 100644 --- 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 @@ -59,7 +59,10 @@ export default class BulkAmAssets extends BaseAmCommand { }), }; - private printAmSummary(op: 'delete' | 'move', opts: { jobId?: string; count?: number; folderUid?: string; notice?: string; error?: string; spaceUid?: string }): void { + private printAmSummary( + op: 'delete' | 'move', + opts: { jobId?: string; count?: number; folderUid?: string; notice?: string; error?: string; spaceUid?: string } + ): void { if (opts.error) { log.error($t(messages.AM_OPERATION_FAILED, { operation: op }), this.loggerContext); log.error(opts.error, this.loggerContext); @@ -72,7 +75,10 @@ export default class BulkAmAssets extends BaseAmCommand { } else { log.success($t(messages.AM_MOVE_SUCCESS), this.loggerContext); if (opts.count !== undefined && opts.folderUid) { - log.info($t(messages.AM_MOVE_ASSETS_COUNT, { count: opts.count, folderUid: opts.folderUid }), this.loggerContext); + log.info( + $t(messages.AM_MOVE_ASSETS_COUNT, { count: opts.count, folderUid: opts.folderUid }), + this.loggerContext + ); } const statusUrl = generateAmJobStatusUrl(opts.spaceUid); if (statusUrl) log.info(statusUrl, this.loggerContext); diff --git a/packages/contentstack-bulk-operations/src/messages/index.ts b/packages/contentstack-bulk-operations/src/messages/index.ts index d9fb94f93..49cbbde84 100644 --- a/packages/contentstack-bulk-operations/src/messages/index.ts +++ b/packages/contentstack-bulk-operations/src/messages/index.ts @@ -238,8 +238,10 @@ const amBulkAssetsMsg = { AM_WORKSPACE_FLAG: 'AM workspace query parameter (default: main)', AM_ASSET_UIDS_FILE_FLAG: 'Path to UTF-8 JSON file: exactly `{ "uids": ["uid1", "uid2"] }` (non-empty string array, no trimming; large lists: see docs for NODE_OPTIONS)', - AM_LOCALE_FLAG: 'Locale code for bulk delete only (single locale per run). Not applicable for move — move always relocates all locale variants of an asset.', - AM_LOCALE_NOT_ALLOWED_FOR_MOVE: '--locale is not applicable for the move operation. Move always relocates all locale variants of an asset. Remove --locale and try again.', + AM_LOCALE_FLAG: + 'Locale code for bulk delete only (single locale per run). Not applicable for move — move always relocates all locale variants of an asset.', + AM_LOCALE_NOT_ALLOWED_FOR_MOVE: + '--locale is not applicable for the move operation. Move always relocates all locale variants of an asset. Remove --locale and try again.', AM_TARGET_FOLDER_FLAG: 'Destination AM folder UID for bulk move. Use "root" to move assets to the root folder.', AM_INVALID_OPERATION: 'Invalid operation: {operation}. Must be delete or move', AM_CONFIRM_SUMMARY: 'Proceed with AM {operation} on {count} item(s)?', diff --git a/packages/contentstack-bulk-operations/src/utils/interactive.ts b/packages/contentstack-bulk-operations/src/utils/interactive.ts index 4bccb0a91..586a1d928 100644 --- a/packages/contentstack-bulk-operations/src/utils/interactive.ts +++ b/packages/contentstack-bulk-operations/src/utils/interactive.ts @@ -257,8 +257,8 @@ export async function fillMissingAmFlags(flags: any): Promise { !f['space-uid'] && '--space-uid', !f['org-uid'] && '--org-uid', !f['asset-uids-file'] && '--asset-uids-file', - (f.operation === 'delete' && !f.locale) && '--locale', - (f.operation === 'move' && !f['target-folder-uid']) && '--target-folder-uid', + f.operation === 'delete' && !f.locale && '--locale', + f.operation === 'move' && !f['target-folder-uid'] && '--target-folder-uid', ].filter(Boolean); throw new Error( `Missing required flag(s): ${missing.join(', ')}. Provide all required flags when running in a non-interactive environment.` From 760c8ce38bff66d4366af46c8854ba367f079ee4 Mon Sep 17 00:00:00 2001 From: naman-contentstack Date: Wed, 3 Jun 2026 16:39:30 +0530 Subject: [PATCH 5/5] chore: update func/var names according to convention --- .../src/base-am-command.ts | 16 ++-- .../src/commands/cm/stacks/bulk-am-assets.ts | 92 +++++++++---------- .../src/interfaces/index.ts | 14 +-- .../src/messages/index.ts | 84 ++++++++--------- .../src/services/am-asset-service.ts | 16 ++-- .../src/services/index.ts | 2 +- .../src/utils/asset-uids-from-file.ts | 12 +-- .../src/utils/bulk-publish-url-generator.ts | 8 +- .../src/utils/index.ts | 4 +- .../src/utils/interactive.ts | 24 ++--- 10 files changed, 136 insertions(+), 136 deletions(-) diff --git a/packages/contentstack-bulk-operations/src/base-am-command.ts b/packages/contentstack-bulk-operations/src/base-am-command.ts index 67ce99cb9..0d23b9c93 100644 --- a/packages/contentstack-bulk-operations/src/base-am-command.ts +++ b/packages/contentstack-bulk-operations/src/base-am-command.ts @@ -1,24 +1,24 @@ import { Command } from '@contentstack/cli-command'; import { handleAndLogError } from '@contentstack/cli-utilities'; -import { fillMissingAmFlags } from './utils'; -import type { AmAssetFlags } from './interfaces'; +import { fillMissingCsAssetsFlags } from './utils'; +import type { CsAssetsFlags } from './interfaces'; /** - * Thin base command for Asset Management operations. + * Thin base command for CS Assets operations. * Handles flag prompting in init() and exposes typed parsedFlags / loggerContext. - * Deliberately does NOT inherit BaseBulkCommand — AM operations use a different API + * 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 BaseAmCommand extends Command { - protected parsedFlags!: AmAssetFlags; +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 BaseAmCommand); + const { flags } = await this.parse(this.constructor as typeof BaseCsAssetsCommand); this.loggerContext = { module: this.id ?? 'cm:stacks:bulk-am-assets' }; - this.parsedFlags = (await fillMissingAmFlags(flags)) as AmAssetFlags; + this.parsedFlags = (await fillMissingCsAssetsFlags(flags)) as CsAssetsFlags; } async catch(error: Error): Promise { 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 index c64bf16b1..2dd7961b9 100644 --- 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 @@ -2,25 +2,25 @@ import chalk from 'chalk'; import { flags, log, createLogContext, cliux, handleAndLogError, FlagInput } from '@contentstack/cli-utilities'; import messages, { $t } from '../../../messages'; -import { BaseAmCommand } from '../../../base-am-command'; -import { AmAssetService } from '../../../services'; +import { BaseCsAssetsCommand } from '../../../base-am-command'; +import { CsAssetsService } from '../../../services'; import { loadAssetUidsFromFile, loadBulkDeleteItemsFromFile, LoadAssetUidsError, } from '../../../utils/asset-uids-from-file'; -import { generateAmJobStatusUrl } from '../../../utils/bulk-publish-url-generator'; -import { AmBulkDeleteItem } from '../../../interfaces'; +import { generateCsAssetsJobStatusUrl } from '../../../utils/bulk-publish-url-generator'; +import { CsAssetsBulkDeleteItem } from '../../../interfaces'; const COMMAND_ID = 'cm:stacks:bulk-am-assets'; -type RegionWithOptionalAmUrl = { csAssetsUrl?: string }; +type RegionWithOptionalCsAssetsUrl = { csAssetsUrl?: string }; /** - * AM bulk delete (job) / bulk move — CS Assets API only; asset UIDs come from a JSON file `{ "uids": [...] }`. + * CS Assets bulk delete (job) / bulk move; asset UIDs come from a JSON file `{ "uids": [...] }`. */ -export default class BulkAmAssets extends BaseAmCommand { - static description = messages.BULK_AM_ASSETS_DESCRIPTION; +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', @@ -30,27 +30,27 @@ export default class BulkAmAssets extends BaseAmCommand { static flags: FlagInput = { operation: flags.string({ - description: messages.AM_OPERATION_FLAG, + description: messages.CS_ASSETS_OPERATION_FLAG, options: ['delete', 'move'], }), 'space-uid': flags.string({ - description: messages.AM_SPACE_UID_FLAG, + description: messages.CS_ASSETS_SPACE_UID_FLAG, }), 'org-uid': flags.string({ - description: messages.AM_ORG_UID_FLAG, + description: messages.CS_ASSETS_ORG_UID_FLAG, }), workspace: flags.string({ default: 'main', - description: messages.AM_WORKSPACE_FLAG, + description: messages.CS_ASSETS_WORKSPACE_FLAG, }), 'asset-uids-file': flags.string({ - description: messages.AM_ASSET_UIDS_FILE_FLAG, + description: messages.CS_ASSETS_ASSET_UIDS_FILE_FLAG, }), locale: flags.string({ - description: messages.AM_LOCALE_FLAG, + description: messages.CS_ASSETS_LOCALE_FLAG, }), 'target-folder-uid': flags.string({ - description: messages.AM_TARGET_FOLDER_FLAG, + description: messages.CS_ASSETS_TARGET_FOLDER_FLAG, }), yes: flags.boolean({ char: 'y', @@ -59,28 +59,28 @@ export default class BulkAmAssets extends BaseAmCommand { }), }; - private printAmSummary( + 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.AM_OPERATION_FAILED, { operation: op }), this.loggerContext); + 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.AM_DELETE_SUCCESS), this.loggerContext); - if (opts.jobId) log.info($t(messages.AM_DELETE_JOB_ID, { jobId: opts.jobId }), this.loggerContext); - log.info($t(messages.AM_DELETE_ASYNC_NOTE), this.loggerContext); - const statusUrl = generateAmJobStatusUrl(opts.spaceUid); + 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.AM_MOVE_SUCCESS), this.loggerContext); + log.success($t(messages.CS_ASSETS_MOVE_SUCCESS), this.loggerContext); if (opts.count !== undefined && opts.folderUid) { log.info( - $t(messages.AM_MOVE_ASSETS_COUNT, { count: opts.count, folderUid: opts.folderUid }), + $t(messages.CS_ASSETS_MOVE_ASSETS_COUNT, { count: opts.count, folderUid: opts.folderUid }), this.loggerContext ); } - const statusUrl = generateAmJobStatusUrl(opts.spaceUid); + const statusUrl = generateCsAssetsJobStatusUrl(opts.spaceUid); if (statusUrl) log.info(statusUrl, this.loggerContext); } if (opts.notice) log.info(opts.notice, this.loggerContext); @@ -90,11 +90,11 @@ export default class BulkAmAssets extends BaseAmCommand { const pathShown = e.filePath; if (e.kind === 'READ') { log.error( - $t(messages.AM_ASSET_UIDS_FILE_READ_FAILED, { path: pathShown, detail: e.message }), + $t(messages.CS_ASSETS_ASSET_UIDS_FILE_READ_FAILED, { path: pathShown, detail: e.message }), this.loggerContext ); } else { - log.error($t(messages.AM_ASSET_UIDS_FILE_INVALID, { path: pathShown, detail: e.message }), this.loggerContext); + log.error($t(messages.CS_ASSETS_ASSET_UIDS_FILE_INVALID, { path: pathShown, detail: e.message }), this.loggerContext); } process.exitCode = 1; } @@ -103,16 +103,16 @@ export default class BulkAmAssets extends BaseAmCommand { try { const f = this.parsedFlags; - const amBaseUrl = (this.region as RegionWithOptionalAmUrl).csAssetsUrl?.trim(); - if (!amBaseUrl) { - log.error($t(messages.AM_URL_NOT_CONFIGURED), this.loggerContext); + 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.AM_INVALID_OPERATION, { operation: String(op ?? '') }), this.loggerContext); + log.error($t(messages.CS_ASSETS_INVALID_OPERATION, { operation: String(op ?? '') }), this.loggerContext); process.exitCode = 1; return; } @@ -121,12 +121,12 @@ export default class BulkAmAssets extends BaseAmCommand { const orgUid = f['org-uid'].trim(); const assetUidsPath = f['asset-uids-file'].trim(); - let deleteRows: AmBulkDeleteItem[]; + let deleteRows: CsAssetsBulkDeleteItem[]; if (op === 'delete') { const locale = (f.locale ?? '').trim(); if (!locale) { - log.error($t(messages.AM_LOCALE_REQUIRED), this.loggerContext); + log.error($t(messages.CS_ASSETS_LOCALE_REQUIRED), this.loggerContext); process.exitCode = 1; return; } @@ -143,18 +143,18 @@ export default class BulkAmAssets extends BaseAmCommand { } createLogContext(this.context?.info?.command || COMMAND_ID, spaceUid, 'OAuth/Token'); - const amService = new AmAssetService(amBaseUrl, spaceUid, orgUid); + 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: AM bulk delete'); + 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 AM delete entries: ${deleteRows.length}\n`); + console.log(` Total CS Assets delete entries: ${deleteRows.length}\n`); const confirmed: boolean = await cliux.inquire({ type: 'confirm', @@ -168,19 +168,19 @@ export default class BulkAmAssets extends BaseAmCommand { } } - log.info($t(messages.AM_DELETING_ASSETS, { count: deleteRows.length, spaceUid }), this.loggerContext); - const result = await amService.bulkDelete(spaceUid, workspace, deleteRows); + 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.printAmSummary('delete', { error: result.error ?? 'AM bulk delete failed', spaceUid }); + this.printCsAssetsSummary('delete', { error: result.error ?? 'CS Assets bulk delete failed', spaceUid }); process.exitCode = 1; return; } - this.printAmSummary('delete', { jobId: result.jobId, notice: result.notice, spaceUid }); + this.printCsAssetsSummary('delete', { jobId: result.jobId, notice: result.notice, spaceUid }); return; } if (f.locale) { - log.error($t(messages.AM_LOCALE_NOT_ALLOWED_FOR_MOVE), this.loggerContext); + log.error($t(messages.CS_ASSETS_LOCALE_NOT_ALLOWED_FOR_MOVE), this.loggerContext); process.exitCode = 1; return; } @@ -206,12 +206,12 @@ export default class BulkAmAssets extends BaseAmCommand { } createLogContext(this.context?.info?.command || COMMAND_ID, spaceUid, 'OAuth/Token'); - const amService = new AmAssetService(amBaseUrl, spaceUid, orgUid); + 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: AM bulk move'); + console.log(' Operation: CS Assets bulk move'); console.log(` Space UID: ${spaceUid}`); console.log(` Organization UID: ${orgUid}`); console.log(` Workspace: ${workspace}`); @@ -232,16 +232,16 @@ export default class BulkAmAssets extends BaseAmCommand { } log.info( - $t(messages.AM_MOVING_ASSETS, { count: uids.length, targetFolderUid: moveFolderUid }), + $t(messages.CS_ASSETS_MOVING_ASSETS, { count: uids.length, targetFolderUid: moveFolderUid }), this.loggerContext ); - const result = await amService.bulkMove(spaceUid, workspace, uids, moveFolderUid); + const result = await csAssetsService.bulkMove(spaceUid, workspace, uids, moveFolderUid); if (!result.success) { - this.printAmSummary('move', { error: result.error ?? 'AM bulk move failed', spaceUid }); + this.printCsAssetsSummary('move', { error: result.error ?? 'CS Assets bulk move failed', spaceUid }); process.exitCode = 1; return; } - this.printAmSummary('move', { count: uids.length, folderUid: moveFolderUid, notice: result.notice, spaceUid }); + this.printCsAssetsSummary('move', { count: uids.length, folderUid: moveFolderUid, notice: result.notice, spaceUid }); } catch (error) { handleAndLogError(error); } diff --git a/packages/contentstack-bulk-operations/src/interfaces/index.ts b/packages/contentstack-bulk-operations/src/interfaces/index.ts index c13dbc3c4..04b3ed901 100644 --- a/packages/contentstack-bulk-operations/src/interfaces/index.ts +++ b/packages/contentstack-bulk-operations/src/interfaces/index.ts @@ -20,7 +20,7 @@ export enum ResourceType { ENTRY = 'entry', ASSET = 'asset', TAXONOMY = 'taxonomy', - AM_ASSET = 'am-asset', + CS_ASSETS = 'cs-assets', } export enum FilterType { @@ -198,7 +198,7 @@ export interface CommandFlags { // Asset-specific flags 'folder-uid'?: string; - /** AM bulk delete/move */ + /** CS Assets bulk delete/move */ 'space-uid'?: string; 'org-uid'?: string; workspace?: string; @@ -257,14 +257,14 @@ export interface AssetPublishData { publish_details?: PublishDetails[]; } -/** One row for AM bulk-delete payload `{ uid, locale }[]`. */ -export interface AmBulkDeleteItem { +/** One row for CS Assets bulk-delete payload `{ uid, locale }[]`. */ +export interface CsAssetsBulkDeleteItem { uid: string; locale: string; } -/** Normalized outcome from AM bulk delete/move calls (CLI layer). */ -export interface AmBulkOperationResult { +/** Normalized outcome from CS Assets bulk delete/move calls (CLI layer). */ +export interface CsAssetsBulkOperationResult { success: boolean; notice?: string; jobId?: string; @@ -272,7 +272,7 @@ export interface AmBulkOperationResult { } /** Typed flags for the bulk-am-assets command. */ -export interface AmAssetFlags { +export interface CsAssetsFlags { operation: string; 'space-uid': string; 'org-uid': string; diff --git a/packages/contentstack-bulk-operations/src/messages/index.ts b/packages/contentstack-bulk-operations/src/messages/index.ts index 49cbbde84..640b1b79d 100644 --- a/packages/contentstack-bulk-operations/src/messages/index.ts +++ b/packages/contentstack-bulk-operations/src/messages/index.ts @@ -213,52 +213,52 @@ const bulkAssetsMsg = { }; /** - * AM bulk delete/move (CS Assets API) messages + * CS Assets bulk delete/move messages */ -const amBulkAssetsMsg = { - BULK_AM_ASSETS_DESCRIPTION: - 'Bulk delete or move assets via Asset Management API (AM-enabled regions). Loads asset UIDs from a JSON file `{ "uids": [...] }`; pass organization via `--org-uid`.', - AM_URL_NOT_CONFIGURED: - 'AM operations require assetManagementUrl in your region settings. Ensure your region is configured correctly.', - SPACE_UID_REQUIRED: '--space-uid is required for AM operations', - ORG_UID_REQUIRED: '--org-uid is required for AM operations (organization_uid header)', +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', + ORG_UID_REQUIRED: '--org-uid is required for CS Assets operations (organization_uid header)', TARGET_FOLDER_REQUIRED: '--target-folder-uid is required for bulk move', - AM_LOCALE_REQUIRED: '--locale is required for bulk delete (AM deletes per asset and locale)', - AM_ASSET_UIDS_FILE_REQUIRED: '--asset-uids-file is required (path to JSON `{ "uids": string[] }`)', - AM_ASSET_UIDS_FILE_READ_FAILED: 'Failed to read asset UIDs file "{path}": {detail}', - AM_ASSET_UIDS_FILE_INVALID: 'Invalid asset UIDs file "{path}": {detail}', - AM_DELETING_ASSETS: 'Deleting {count} asset/locale pair(s) from space {spaceUid}...', - AM_MOVING_ASSETS: 'Moving {count} asset(s) to folder {targetFolderUid}...', - AM_DELETE_SUBMITTED: 'Bulk delete job submitted. Job ID: {jobId}', - AM_MOVE_SUBMITTED: 'Bulk move initiated successfully.', - AM_OPERATION_NOTICE: '{notice}', - AM_OPERATION_FLAG: 'Operation: delete (AM bulk delete) or move (AM bulk move)', - AM_SPACE_UID_FLAG: 'Asset Management space UID', - AM_ORG_UID_FLAG: 'Organization UID for AM API (organization_uid header)', - AM_WORKSPACE_FLAG: 'AM workspace query parameter (default: main)', - AM_ASSET_UIDS_FILE_FLAG: + CS_ASSETS_LOCALE_REQUIRED: '--locale is required for bulk delete (CS Assets deletes per asset and locale)', + CS_ASSETS_ASSET_UIDS_FILE_REQUIRED: '--asset-uids-file is required (path to JSON `{ "uids": string[] }`)', + CS_ASSETS_ASSET_UIDS_FILE_READ_FAILED: 'Failed to read asset UIDs file "{path}": {detail}', + CS_ASSETS_ASSET_UIDS_FILE_INVALID: 'Invalid asset UIDs file "{path}": {detail}', + CS_ASSETS_DELETING_ASSETS: 'Deleting {count} asset/locale pair(s) from space {spaceUid}...', + CS_ASSETS_MOVING_ASSETS: 'Moving {count} asset(s) to folder {targetFolderUid}...', + CS_ASSETS_DELETE_SUBMITTED: 'Bulk delete job submitted. Job ID: {jobId}', + CS_ASSETS_MOVE_SUBMITTED: 'Bulk move initiated successfully.', + CS_ASSETS_OPERATION_NOTICE: '{notice}', + CS_ASSETS_OPERATION_FLAG: 'Operation: delete (CS Assets bulk delete) or move (CS Assets bulk move)', + CS_ASSETS_SPACE_UID_FLAG: 'CS Assets space UID', + CS_ASSETS_ORG_UID_FLAG: 'Organization UID for CS Assets API (organization_uid header)', + CS_ASSETS_WORKSPACE_FLAG: 'CS Assets workspace query parameter (default: main)', + CS_ASSETS_ASSET_UIDS_FILE_FLAG: 'Path to UTF-8 JSON file: exactly `{ "uids": ["uid1", "uid2"] }` (non-empty string array, no trimming; large lists: see docs for NODE_OPTIONS)', - AM_LOCALE_FLAG: + CS_ASSETS_LOCALE_FLAG: 'Locale code for bulk delete only (single locale per run). Not applicable for move — move always relocates all locale variants of an asset.', - AM_LOCALE_NOT_ALLOWED_FOR_MOVE: + CS_ASSETS_LOCALE_NOT_ALLOWED_FOR_MOVE: '--locale is not applicable for the move operation. Move always relocates all locale variants of an asset. Remove --locale and try again.', - AM_TARGET_FOLDER_FLAG: 'Destination AM folder UID for bulk move. Use "root" to move assets to the root folder.', - AM_INVALID_OPERATION: 'Invalid operation: {operation}. Must be delete or move', - AM_CONFIRM_SUMMARY: 'Proceed with AM {operation} on {count} item(s)?', - AM_DELETE_SUCCESS: 'AM bulk delete job submitted successfully!', - AM_DELETE_JOB_ID: 'Job ID: {jobId}', - AM_DELETE_ASYNC_NOTE: 'The job runs asynchronously — check the bulk task queue for status:', - AM_MOVE_SUCCESS: 'AM bulk move completed successfully!', - AM_MOVE_ASSETS_COUNT: '{count} asset(s) moved to folder: {folderUid}', - AM_OPERATION_FAILED: 'AM {operation} failed.', + CS_ASSETS_TARGET_FOLDER_FLAG: 'Destination CS Assets folder UID for bulk move. Use "root" to move assets to the root folder.', + CS_ASSETS_INVALID_OPERATION: 'Invalid operation: {operation}. Must be delete or move', + CS_ASSETS_CONFIRM_SUMMARY: 'Proceed with CS Assets {operation} on {count} item(s)?', + CS_ASSETS_DELETE_SUCCESS: 'CS Assets bulk delete job submitted successfully!', + CS_ASSETS_DELETE_JOB_ID: 'Job ID: {jobId}', + CS_ASSETS_DELETE_ASYNC_NOTE: 'The job runs asynchronously — check the bulk task queue for status:', + CS_ASSETS_MOVE_SUCCESS: 'CS Assets bulk move completed successfully!', + CS_ASSETS_MOVE_ASSETS_COUNT: '{count} asset(s) moved to folder: {folderUid}', + CS_ASSETS_OPERATION_FAILED: 'CS Assets {operation} failed.', // Interactive prompts - AM_SELECT_OPERATION: 'Select AM operation:', - AM_ENTER_SPACE_UID: 'Enter AM space UID:', - AM_ENTER_ORG_UID: 'Enter organization UID:', - AM_ENTER_ASSET_UIDS_FILE: 'Enter path to asset UIDs JSON file (e.g. ./assets.json):', - AM_ENTER_LOCALE: 'Enter locale code for bulk delete (e.g. en-us):', - AM_ENTER_TARGET_FOLDER: 'Enter target folder UID for bulk move (use "root" to move to the root folder):', + 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):', + CS_ASSETS_ENTER_LOCALE: 'Enter locale code for bulk delete (e.g. en-us):', + CS_ASSETS_ENTER_TARGET_FOLDER: 'Enter target folder UID for bulk move (use "root" to move to the root folder):', }; /** @@ -420,7 +420,7 @@ const commandInfo = { BULK_ASSETS_DESCRIPTION: 'Bulk operations for assets (publish/unpublish/cross-publish)', BULK_TAXONOMIES_DESCRIPTION: 'Publish taxonomies to environments and locales (CMA POST /v3/taxonomies/publish; initiates a publish job)', - BULK_AM_ASSETS_DESCRIPTION: amBulkAssetsMsg.BULK_AM_ASSETS_DESCRIPTION, + BULK_CS_ASSETS_DESCRIPTION: csAssetsBulkMsg.BULK_CS_ASSETS_DESCRIPTION, }; /** @@ -437,7 +437,7 @@ const messages: typeof errors & typeof interactiveMsg & typeof flagDescriptions & typeof commandInfo & - typeof amBulkAssetsMsg = { + typeof csAssetsBulkMsg = { ...errors, ...commonMsg, ...entryServiceMsg, @@ -449,7 +449,7 @@ const messages: typeof errors & ...interactiveMsg, ...flagDescriptions, ...commandInfo, - ...amBulkAssetsMsg, + ...csAssetsBulkMsg, }; /** diff --git a/packages/contentstack-bulk-operations/src/services/am-asset-service.ts b/packages/contentstack-bulk-operations/src/services/am-asset-service.ts index 37f3b0773..d80e288f4 100644 --- a/packages/contentstack-bulk-operations/src/services/am-asset-service.ts +++ b/packages/contentstack-bulk-operations/src/services/am-asset-service.ts @@ -1,16 +1,16 @@ import { CSAssetsAdapter } from '@contentstack/cli-asset-management'; -import type { AmBulkDeleteItem, AmBulkOperationResult } from '../interfaces'; +import type { CsAssetsBulkDeleteItem, CsAssetsBulkOperationResult } from '../interfaces'; /** - * Thin wrapper around {@link CSAssetsAdapter} for AM bulk delete/move used by bulk-operations CLI. + * Thin wrapper around {@link CSAssetsAdapter} for CS Assets bulk delete/move used by bulk-operations CLI. */ -export class AmAssetService { +export class CsAssetsService { private readonly adapter: CSAssetsAdapter; - constructor(amBaseUrl: string, spaceUid: string, orgUid: string) { + constructor(csAssetsBaseUrl: string, spaceUid: string, orgUid: string) { this.adapter = new CSAssetsAdapter({ - baseURL: amBaseUrl, + baseURL: csAssetsBaseUrl, headers: { organization_uid: orgUid, space_key: spaceUid }, }); } @@ -18,8 +18,8 @@ export class AmAssetService { async bulkDelete( spaceUid: string, workspaceUid: string | undefined, - items: AmBulkDeleteItem[] - ): Promise { + items: CsAssetsBulkDeleteItem[] + ): Promise { try { const response = await this.adapter.bulkDeleteAssets(spaceUid, workspaceUid ?? 'main', { assets: items, @@ -42,7 +42,7 @@ export class AmAssetService { workspaceUid: string | undefined, assetUids: string[], targetFolderUid: string - ): Promise { + ): Promise { try { const response = await this.adapter.bulkMoveAssets(spaceUid, workspaceUid ?? 'main', { asset_uids: assetUids, diff --git a/packages/contentstack-bulk-operations/src/services/index.ts b/packages/contentstack-bulk-operations/src/services/index.ts index 4c6c3d49a..25640eff2 100644 --- a/packages/contentstack-bulk-operations/src/services/index.ts +++ b/packages/contentstack-bulk-operations/src/services/index.ts @@ -1,5 +1,5 @@ export { EntryService } from './entry-service'; export { AssetService } from './asset-service'; -export { AmAssetService } from './am-asset-service'; +export { CsAssetsService } from './am-asset-service'; export { BulkOperationService } from './bulk-operation-service'; export { TaxonomyService } from './taxonomy-service'; diff --git a/packages/contentstack-bulk-operations/src/utils/asset-uids-from-file.ts b/packages/contentstack-bulk-operations/src/utils/asset-uids-from-file.ts index 7d396c89e..32fc804a7 100644 --- a/packages/contentstack-bulk-operations/src/utils/asset-uids-from-file.ts +++ b/packages/contentstack-bulk-operations/src/utils/asset-uids-from-file.ts @@ -1,7 +1,7 @@ import * as fs from 'node:fs'; import path from 'node:path'; -import type { AmBulkDeleteItem } from '../interfaces'; +import type { CsAssetsBulkDeleteItem } from '../interfaces'; export type LoadAssetUidsErrorKind = 'READ' | 'PARSE' | 'SCHEMA'; @@ -83,16 +83,16 @@ export function validateAssetUidsParsedJson(parsed: unknown, filePathForErrors: } /** - * Validates `{ "uids": string[] }` and builds AM bulk-delete rows in one pass over `uids`. + * Validates `{ "uids": string[] }` and builds CS Assets bulk-delete rows in one pass over `uids`. * `locale` must be the final non-empty value from the CLI (caller trims). */ export function validateAndBuildBulkDeleteItems( parsed: unknown, locale: string, filePathForErrors: string -): AmBulkDeleteItem[] { +): CsAssetsBulkDeleteItem[] { const uids = parseValidatedUidsArray(parsed, filePathForErrors); - const items = new Array(uids.length); + const items = new Array(uids.length); for (let i = 0; i < uids.length; i++) { const uid = uids[i]; if (typeof uid !== 'string') { @@ -116,9 +116,9 @@ export function loadAssetUidsFromFile(filePath: string): string[] { } /** - * Reads asset UID file and returns `{ uid, locale }[]` for AM bulk delete (single pass over `uids` after parse). + * Reads asset UID file and returns `{ uid, locale }[]` for CS Assets bulk delete (single pass over `uids` after parse). */ -export function loadBulkDeleteItemsFromFile(filePath: string, locale: string): AmBulkDeleteItem[] { +export function loadBulkDeleteItemsFromFile(filePath: string, locale: string): CsAssetsBulkDeleteItem[] { const { resolved, parsed } = readResolvedAssetUidsJson(filePath); return validateAndBuildBulkDeleteItems(parsed, locale, resolved); } diff --git a/packages/contentstack-bulk-operations/src/utils/bulk-publish-url-generator.ts b/packages/contentstack-bulk-operations/src/utils/bulk-publish-url-generator.ts index a59448020..205b473d5 100644 --- a/packages/contentstack-bulk-operations/src/utils/bulk-publish-url-generator.ts +++ b/packages/contentstack-bulk-operations/src/utils/bulk-publish-url-generator.ts @@ -35,11 +35,11 @@ export function generateBulkPublishStatusUrl(apiKey?: string, branch?: string): } /** - * Generate the AM bulk task queue URL for checking job status - * @param spaceUid - AM space UID - * @returns The AM job status URL or null if spaceUid is not available + * Generate the CS Assets bulk task queue URL for checking job status + * @param spaceUid - CS Assets space UID + * @returns The CS Assets job status URL or null if spaceUid is not available */ -export function generateAmJobStatusUrl(spaceUid?: string): string | null { +export function generateCsAssetsJobStatusUrl(spaceUid?: string): string | null { if (!spaceUid) { return null; } diff --git a/packages/contentstack-bulk-operations/src/utils/index.ts b/packages/contentstack-bulk-operations/src/utils/index.ts index 9f2b6e310..aa1d8801f 100644 --- a/packages/contentstack-bulk-operations/src/utils/index.ts +++ b/packages/contentstack-bulk-operations/src/utils/index.ts @@ -35,7 +35,7 @@ import { buildBulkModeResult, handleOperationError, } from './command-helpers'; -import { fillMissingFlags, fillMissingAmFlags } from './interactive'; +import { fillMissingFlags, fillMissingCsAssetsFlags } from './interactive'; import { RATE_LIMITER_CONSTANTS, RETRY_STRATEGY_CONSTANTS, @@ -98,7 +98,7 @@ export { buildBulkModeResult, handleOperationError, fillMissingFlags, - fillMissingAmFlags, + fillMissingCsAssetsFlags, 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 586a1d928..2aa68cdc1 100644 --- a/packages/contentstack-bulk-operations/src/utils/interactive.ts +++ b/packages/contentstack-bulk-operations/src/utils/interactive.ts @@ -236,11 +236,11 @@ async function runInteractivePrompts(prompts: Array<() => Promise>): Promi /** * Fills in missing flags for the bulk-am-assets command by prompting the user. - * Handles AM-specific required flags including operation-conditional ones + * Handles CS Assets-specific required flags including operation-conditional ones * (locale for delete, target-folder-uid for move). * Throws in non-TTY environments when required flags are missing. */ -export async function fillMissingAmFlags(flags: any): Promise { +export async function fillMissingCsAssetsFlags(flags: any): Promise { const f = { ...flags }; const needsLocale = f.operation === 'delete' && !f.locale; @@ -271,10 +271,10 @@ export async function fillMissingAmFlags(flags: any): Promise { f.operation = await cliux.inquire({ type: 'list', name: 'operation', - message: messages.AM_SELECT_OPERATION, + message: messages.CS_ASSETS_SELECT_OPERATION, choices: [ - { name: 'Delete (AM bulk delete)', value: 'delete' }, - { name: 'Move (AM bulk move)', value: 'move' }, + { name: 'Delete (CS Assets bulk delete)', value: 'delete' }, + { name: 'Move (CS Assets bulk move)', value: 'move' }, ], }); } @@ -284,7 +284,7 @@ export async function fillMissingAmFlags(flags: any): Promise { f['space-uid'] = await cliux.inquire({ type: 'input', name: 'spaceUid', - message: messages.AM_ENTER_SPACE_UID, + message: messages.CS_ASSETS_ENTER_SPACE_UID, validate: (v: string) => (!v?.trim() ? messages.SPACE_UID_REQUIRED : true), }); } @@ -294,7 +294,7 @@ export async function fillMissingAmFlags(flags: any): Promise { f['org-uid'] = await cliux.inquire({ type: 'input', name: 'orgUid', - message: messages.AM_ENTER_ORG_UID, + message: messages.CS_ASSETS_ENTER_ORG_UID, validate: (v: string) => (!v?.trim() ? messages.ORG_UID_REQUIRED : true), }); } @@ -304,8 +304,8 @@ export async function fillMissingAmFlags(flags: any): Promise { f['asset-uids-file'] = await cliux.inquire({ type: 'input', name: 'assetUidsFile', - message: messages.AM_ENTER_ASSET_UIDS_FILE, - validate: (v: string) => (!v?.trim() ? messages.AM_ASSET_UIDS_FILE_REQUIRED : true), + message: messages.CS_ASSETS_ENTER_ASSET_UIDS_FILE, + validate: (v: string) => (!v?.trim() ? messages.CS_ASSETS_ASSET_UIDS_FILE_REQUIRED : true), }); } }, @@ -315,8 +315,8 @@ export async function fillMissingAmFlags(flags: any): Promise { f.locale = await cliux.inquire({ type: 'input', name: 'locale', - message: messages.AM_ENTER_LOCALE, - validate: (v: string) => (!v?.trim() ? messages.AM_LOCALE_REQUIRED : true), + message: messages.CS_ASSETS_ENTER_LOCALE, + validate: (v: string) => (!v?.trim() ? messages.CS_ASSETS_LOCALE_REQUIRED : true), }); } }, @@ -325,7 +325,7 @@ export async function fillMissingAmFlags(flags: any): Promise { f['target-folder-uid'] = await cliux.inquire({ type: 'input', name: 'targetFolderUid', - message: messages.AM_ENTER_TARGET_FOLDER, + message: messages.CS_ASSETS_ENTER_TARGET_FOLDER, validate: (v: string) => (!v?.trim() ? messages.TARGET_FOLDER_REQUIRED : true), }); }