Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions packages/contentstack-bulk-operations/src/base-am-command.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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<void> {
handleAndLogError(error);
}

abstract run(): Promise<void>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -145,15 +145,14 @@ export abstract class BaseBulkCommand extends Command {

this.parsedFlags = flags;

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'];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +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 { BaseAmCommand } from '../../../base-am-command';
import { AmAssetService } from '../../../services';
import {
loadAssetUidsFromFile,
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';
Expand All @@ -18,7 +19,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 BaseAmCommand {
static description = messages.BULK_AM_ASSETS_DESCRIPTION;

static examples = [
Expand All @@ -31,23 +32,19 @@ 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',
description: messages.AM_WORKSPACE_FLAG,
}),
'asset-uids-file': flags.string({
description: messages.AM_ASSET_UIDS_FILE_FLAG,
required: true,
}),
locale: flags.string({
description: messages.AM_LOCALE_FLAG,
Expand All @@ -62,7 +59,32 @@ export default class BulkAmAssets extends Command {
}),
};

private readonly loggerContext = { module: COMMAND_ID };
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);
} 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);
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);
}

private handleAssetUidsFileError(e: LoadAssetUidsError): void {
const pathShown = e.filePath;
Expand All @@ -79,7 +101,7 @@ export default class BulkAmAssets extends Command {

async run(): Promise<void> {
try {
const { flags: f } = await this.parse(BulkAmAssets);
const f = this.parsedFlags;

const amBaseUrl = (this.region as RegionWithOptionalAmUrl).csAssetsUrl?.trim();
if (!amBaseUrl) {
Expand All @@ -95,26 +117,9 @@ export default class BulkAmAssets extends Command {
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[];

Expand Down Expand Up @@ -166,16 +171,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', spaceUid });
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, spaceUid });
return;
}

if (f.locale) {
log.error($t(messages.AM_LOCALE_NOT_ALLOWED_FOR_MOVE), this.loggerContext);
process.exitCode = 1;
return;
}

Expand Down Expand Up @@ -231,14 +237,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', spaceUid });
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, spaceUid });
} catch (error) {
handleAndLogError(error);
}
Expand Down
13 changes: 13 additions & 0 deletions packages/contentstack-bulk-operations/src/interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export enum ResourceType {
ENTRY = 'entry',
ASSET = 'asset',
TAXONOMY = 'taxonomy',
AM_ASSET = 'am-asset',
}

export enum FilterType {
Expand Down Expand Up @@ -270,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;
Expand Down
21 changes: 19 additions & 2 deletions packages/contentstack-bulk-operations/src/messages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,10 +238,27 @@ 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 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.',

// 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):',
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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`;
}
3 changes: 2 additions & 1 deletion packages/contentstack-bulk-operations/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -98,6 +98,7 @@ export {
buildBulkModeResult,
handleOperationError,
fillMissingFlags,
fillMissingAmFlags,
fetchTaxonomyList,
RATE_LIMITER_CONSTANTS,
RETRY_STRATEGY_CONSTANTS,
Expand Down
Loading
Loading