diff --git a/packages/contentstack-bulk-operations/src/base-am-command.ts b/packages/contentstack-bulk-operations/src/base-am-command.ts index 0d23b9c93..769a1374b 100644 --- a/packages/contentstack-bulk-operations/src/base-am-command.ts +++ b/packages/contentstack-bulk-operations/src/base-am-command.ts @@ -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'; @@ -16,6 +22,12 @@ export abstract class BaseCsAssetsCommand extends Command { protected async init(): Promise { 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; @@ -25,5 +37,13 @@ export abstract class BaseCsAssetsCommand extends Command { handleAndLogError(error); } + protected async finally(_error?: Error): Promise { + // 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; } diff --git a/packages/contentstack-bulk-operations/src/base-bulk-command.ts b/packages/contentstack-bulk-operations/src/base-bulk-command.ts index 55436fc33..a3a70cbaa 100644 --- a/packages/contentstack-bulk-operations/src/base-bulk-command.ts +++ b/packages/contentstack-bulk-operations/src/base-bulk-command.ts @@ -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'; @@ -33,6 +44,7 @@ import { generateBulkPublishStatusUrl, validateBranch, validateEnvironments, + aggregateBatchResults, } from './utils'; import { OperationType, @@ -141,6 +153,10 @@ export abstract class BaseBulkCommand extends Command { protected async init(): Promise { 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; @@ -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); } @@ -268,6 +287,13 @@ export abstract class BaseBulkCommand extends Command { * Build operation configuration */ protected async buildConfiguration(flags: any): Promise { + // 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 @@ -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); } @@ -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 "-" 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); + 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(); } /** @@ -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(''); @@ -555,6 +656,7 @@ export abstract class BaseBulkCommand extends Command { abstract run(): Promise; protected async finally(_error: Error | undefined): Promise { + this.finalizeProgressSummary(); await this.cleanup(); } } diff --git a/packages/contentstack-bulk-operations/src/commands/cm/stacks/bulk-taxonomies.ts b/packages/contentstack-bulk-operations/src/commands/cm/stacks/bulk-taxonomies.ts index 5d0cf4bb9..cfee2fda7 100644 --- a/packages/contentstack-bulk-operations/src/commands/cm/stacks/bulk-taxonomies.ts +++ b/packages/contentstack-bulk-operations/src/commands/cm/stacks/bulk-taxonomies.ts @@ -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'; @@ -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 }).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']) { @@ -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' })); @@ -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; } }