Skip to content
22 changes: 21 additions & 1 deletion packages/contentstack-bulk-operations/src/base-am-command.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { Command } from '@contentstack/cli-command';
import { handleAndLogError } from '@contentstack/cli-utilities';
import {
handleAndLogError,
configHandler,
loadChalk,
CLIProgressManager,
clearProgressModuleSetting,
} from '@contentstack/cli-utilities';

import { fillMissingCsAssetsFlags } from './utils';
import type { CsAssetsFlags } from './interfaces';
Expand All @@ -16,6 +22,12 @@ export abstract class BaseCsAssetsCommand extends Command {

protected async init(): Promise<void> {
await super.init();

// Suppress timestamped console logs + load chalk (same UX as the other bulk commands).
// Must run before the first log call. CS Assets keeps its own printCsAssetsSummary output.
configHandler.set('log.progressSupportedModule', 'bulk-operations');
await loadChalk();

const { flags } = await this.parse(this.constructor as typeof BaseCsAssetsCommand);
this.loggerContext = { module: this.id ?? 'cm:stacks:bulk-am-assets' };
this.parsedFlags = (await fillMissingCsAssetsFlags(flags)) as CsAssetsFlags;
Expand All @@ -25,5 +37,13 @@ export abstract class BaseCsAssetsCommand extends Command {
handleAndLogError(error);
}

protected async finally(_error?: Error): Promise<void> {
// Clear progress state so the module flag never leaks into a later command in the process.
// (CS Assets doesn't initialize a global summary, so printGlobalSummary is a no-op here.)
CLIProgressManager.printGlobalSummary();
CLIProgressManager.clearGlobalSummary();
clearProgressModuleSetting();
}

abstract run(): Promise<void>;
}
118 changes: 110 additions & 8 deletions packages/contentstack-bulk-operations/src/base-bulk-command.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import chalk from 'chalk';
import { Command } from '@contentstack/cli-command';
import { flags, log, createLogContext, getLogPath, handleAndLogError, FlagInput } from '@contentstack/cli-utilities';
import {
flags,
log,
createLogContext,
getLogPath,
handleAndLogError,
FlagInput,
getChalk,
loadChalk,
configHandler,
CLIProgressManager,
clearProgressModuleSetting,
} from '@contentstack/cli-utilities';

import config from './config';
import messages, { $t } from './messages';
Expand Down Expand Up @@ -33,6 +44,7 @@ import {
generateBulkPublishStatusUrl,
validateBranch,
validateEnvironments,
aggregateBatchResults,
} from './utils';
import {
OperationType,
Expand Down Expand Up @@ -141,6 +153,10 @@ export abstract class BaseBulkCommand extends Command {
protected async init(): Promise<void> {
await super.init();

// Load chalk (ESM) up-front. The progress-module flag is set in buildConfig()
// (config layer, mirrors export-config-handler) before the first log call.
await loadChalk();

let { flags } = await this.parse(this.constructor as typeof BaseBulkCommand);

this.parsedFlags = flags;
Expand Down Expand Up @@ -220,6 +236,9 @@ export abstract class BaseBulkCommand extends Command {
await this.initializeComponents();

await this.handleRevertOrRetry(mergedFlags);
// This early exit bypasses finally(); print the run summary and clear the progress-module
// flag here so it doesn't leak into the persisted config / later commands.
this.finalizeProgressSummary();
process.exit(0);
}

Expand Down Expand Up @@ -268,6 +287,13 @@ export abstract class BaseBulkCommand extends Command {
* Build operation configuration
*/
protected async buildConfiguration(flags: any): Promise<void> {
// Enable the progress-bar UI + suppress the timestamped console logs for the whole command.
// Set here (command lifecycle, runs before the first log call) rather than in the pure
// buildConfig() util so it isn't triggered by direct/unit-test callers of buildConfig.
// Cleared on every exit path: finally() on normal runs, and finalizeProgressSummary() before
// the validation exit(1) below and the revert/retry exit(0).
configHandler.set('log.progressSupportedModule', 'bulk-operations');

this.bulkOperationConfig = buildConfig(flags);

// buildConfig splits comma-separated oclif `multiple` values; mirror onto flags so
Expand All @@ -280,6 +306,9 @@ export abstract class BaseBulkCommand extends Command {

const validation = validateFlags(this.bulkOperationConfig);
if (!validation.valid) {
// The progress-module flag was set above; this early exit bypasses finally(), so clear it
// here to avoid leaking the setting into the persisted config / later commands.
this.finalizeProgressSummary();
process.exit(1);
}

Expand Down Expand Up @@ -365,20 +394,91 @@ export abstract class BaseBulkCommand extends Command {
this.logger.debug($t(messages.EXECUTING_OPERATION, { count: items.length }), this.loggerContext);
const startTime = Date.now();

// Initialize the run-level summary + header once (progress-manager UX, same as import).
this.beginOperationSummary(items.length);

let result: BulkOperationResult;
try {
logOperationInfo(items, this.logger);

const publishMode = this.bulkOperationConfig.publishMode || PublishMode.BULK;
this.logger.debug(`Using ${publishMode.toUpperCase()} mode for operation`, this.loggerContext);

if (publishMode === PublishMode.SINGLE) {
return await this.executeSingleMode(items, startTime);
}

return await this.executeBulkMode(items, startTime);
result =
publishMode === PublishMode.SINGLE
? await this.executeSingleMode(items, startTime)
: await this.executeBulkMode(items, startTime);
} catch (error: any) {
return handleOperationError(error, items, startTime);
result = handleOperationError(error, items, startTime);
}

this.recordModuleSummary(result, items.length);
return result;
}

/**
* Initialize the run-level summary + header once. The label includes the branch, which
* defaults to 'main' from the --branch flag, so it normally reads e.g. "BULK PUBLISH-main" —
* the same "<OP>-<branch>" title shape export/import produce (SummaryManager uses the passed
* operationName verbatim). The branch is dropped from the label only in the edge case where
* it is unset. Shared with bulk-taxonomies via inheritance.
*/
protected beginOperationSummary(itemCount: number): void {
const operationLabel = (this.bulkOperationConfig?.operation || 'operation').toString().toUpperCase();
const branchName = this.bulkOperationConfig?.branch || '';
CLIProgressManager.initializeGlobalSummary(
branchName ? `BULK ${operationLabel}-${branchName}` : `BULK ${operationLabel}`,
branchName,
$t(messages.EXECUTING_OPERATION, { count: itemCount })
);
}

/**
* Record a per-module row in the summary so the final summary shows Module Details for this
* command (entry/asset/taxonomy).
*
* SINGLE mode processes items individually and returns real success/failed counts, so those
* are used directly. BULK mode submits async jobs — buildBulkModeResult reports success/failed
* as 0 because the publish runs server-side — so we count submission-level failures from
* batchResults (recorded as status:'failed') and treat the remaining submitted items as
* success. The printed status URL remains the source of truth for the real publish outcome.
*/
protected recordModuleSummary(result: BulkOperationResult, submittedCount: number): void {
const showConsoleLogs = Boolean(configHandler.get('log')?.showConsoleLogs);
const publishMode = this.bulkOperationConfig?.publishMode || PublishMode.BULK;
const total = result?.total || submittedCount || 0;

let success: number;
let failed: number;
if (publishMode === PublishMode.SINGLE) {
failed = result?.failed || 0;
success = typeof result?.success === 'number' ? result.success : Math.max(total - failed, 0);
} else {
// BULK: derive failures from batch submissions (result.failed is always 0 here).
failed = aggregateBatchResults(this.batchResults).totalFailed;
success = Math.max(total - failed, 0);
}

// Clamp so counts never over/under-report relative to total.
failed = Math.min(Math.max(failed, 0), total);
success = Math.min(Math.max(success, 0), total - failed);

const progress = CLIProgressManager.createSimple(this.resourceType, total, showConsoleLogs);
Comment thread
harshitha-cstk marked this conversation as resolved.
for (let i = 0; i < success; i++) progress.tick(true);
for (let i = 0; i < failed; i++) progress.tick(false);
progress.complete(failed === 0);
}

/**
* Print the run-level summary once and clear progress state. Idempotent: subclasses call
* finally() explicitly AND oclif calls it again, so clearing the summary after printing makes
* the second invocation a no-op. Also clears the progress-module flag so it never leaks into
* a later command in the same process (mirrors export/import/clone).
*/
protected finalizeProgressSummary(): void {
CLIProgressManager.printGlobalSummary();
CLIProgressManager.clearGlobalSummary();
clearProgressModuleSetting();
}

/**
Expand Down Expand Up @@ -444,6 +544,7 @@ export abstract class BaseBulkCommand extends Command {
* Called at the end of run() method in subclasses
*/
protected printOperationSummary(result: BulkOperationResult): void {
const chalk = getChalk();
const publishMode = this.bulkOperationConfig.publishMode || PublishMode.BULK;

console.log('');
Expand Down Expand Up @@ -555,6 +656,7 @@ export abstract class BaseBulkCommand extends Command {
abstract run(): Promise<void>;

protected async finally(_error: Error | undefined): Promise<void> {
this.finalizeProgressSummary();
await this.cleanup();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Command } from '@contentstack/cli-command';
import { flags, log, createLogContext, handleAndLogError } from '@contentstack/cli-utilities';
import { flags, log, createLogContext, handleAndLogError, loadChalk } from '@contentstack/cli-utilities';

import messages, { $t } from '../../../messages';
import { BaseBulkCommand } from '../../../base-bulk-command';
Expand Down Expand Up @@ -58,6 +58,10 @@ export default class BulkTaxonomies extends BaseBulkCommand {
// Call oclif Command init without running BaseBulkCommand.init (taxonomy uses its own prompts).
await (Command.prototype as unknown as { init(this: Command): Promise<void> }).init.call(this);

// Load chalk (ESM) up-front. The progress-module flag is set in buildConfig() (invoked by
// buildConfiguration below), before the first log call.
await loadChalk();

let { flags: parsed } = await this.parse(BulkTaxonomies);

if (parsed.revert || parsed['retry-failed']) {
Expand Down Expand Up @@ -161,6 +165,9 @@ export default class BulkTaxonomies extends BaseBulkCommand {
this.logger.debug($t(messages.EXECUTING_OPERATION, { count: items.length }), this.loggerContext);
const startTime = Date.now();

// Initialize the run-level summary + header once (inherited from BaseBulkCommand).
this.beginOperationSummary(items.length);

const operation = this.bulkOperationConfig.operation;
if (operation !== OperationType.PUBLISH && operation !== OperationType.UNPUBLISH) {
throw new Error($t(messages.UNSUPPORTED_OPERATION, { operation: operation ?? 'unknown' }));
Expand Down Expand Up @@ -190,12 +197,17 @@ export default class BulkTaxonomies extends BaseBulkCommand {
this.logger.info(String(response.notice));
}

return {
const result: BulkOperationResult = {
success: 0,
failed: 0,
total: items.length,
duration,
jobIds: jobId ? [jobId] : [],
};

// Record the taxonomy module row in the summary (inherited from BaseBulkCommand).
this.recordModuleSummary(result, items.length);

return result;
}
}
Loading
Loading