UNPKG

@contentstack/cli-cm-clone

Version:
316 lines (315 loc) 17.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const cli_command_1 = require("@contentstack/cli-command"); const cli_utilities_1 = require("@contentstack/cli-utilities"); const clone_handler_1 = require("../../../core/util/clone-handler"); const path = tslib_1.__importStar(require("path")); const rimraf_1 = require("rimraf"); const merge_1 = tslib_1.__importDefault(require("merge")); const fs_1 = require("fs"); // Resolve path to package root (works in both src and lib contexts) const packageRoot = __dirname.includes('/src/') ? __dirname.split('/src/')[0] : __dirname.split('/lib/')[0]; const pathdir = path.join(packageRoot, 'contents'); let config = {}; class StackCloneCommand extends cli_command_1.Command { /** * Determine authentication method based on user preference */ determineAuthenticationMethod(sourceManagementTokenAlias, destinationManagementTokenAlias) { // Track authentication method let authenticationMethod = 'unknown'; // Determine authentication method based on user preference if (sourceManagementTokenAlias || destinationManagementTokenAlias) { authenticationMethod = 'Management Token'; } else if ((0, cli_utilities_1.isAuthenticated)()) { // Check if user is authenticated via OAuth const isOAuthUser = cli_utilities_1.configHandler.get('authorisationType') === 'OAUTH' || false; if (isOAuthUser) { authenticationMethod = 'OAuth'; } else { authenticationMethod = 'Basic Auth'; } } else { authenticationMethod = 'Basic Auth'; } return authenticationMethod; } /** * Create clone context object for logging */ createCloneContext(authenticationMethod) { var _a, _b, _c; return { command: ((_b = (_a = this.context) === null || _a === void 0 ? void 0 : _a.info) === null || _b === void 0 ? void 0 : _b.command) || 'cm:stacks:clone', module: 'clone', email: cli_utilities_1.configHandler.get('email') || '', sessionId: ((_c = this.context) === null || _c === void 0 ? void 0 : _c.sessionId) || '', authenticationMethod: authenticationMethod || 'Basic Auth', }; } async run() { try { const self = this; const { flags: cloneCommandFlags } = await self.parse(StackCloneCommand); const { yes, type: cloneType, 'stack-name': stackName, 'source-branch': sourceStackBranch, 'source-branch-alias': sourceStackBranchAlias, 'target-branch': targetStackBranch, 'target-branch-alias': targetStackBranchAlias, 'source-stack-api-key': sourceStackApiKey, 'destination-stack-api-key': destinationStackApiKey, 'source-management-token-alias': sourceManagementTokenAlias, 'destination-management-token-alias': destinationManagementTokenAlias, 'import-webhook-status': importWebhookStatus, config: externalConfigPath, } = cloneCommandFlags; const handleClone = async () => { const listOfTokens = cli_utilities_1.configHandler.get('tokens'); const authenticationMethod = this.determineAuthenticationMethod(sourceManagementTokenAlias, destinationManagementTokenAlias); const cloneContext = this.createCloneContext(authenticationMethod); cli_utilities_1.log.debug('Starting clone operation setup', cloneContext); if (externalConfigPath) { cli_utilities_1.log.debug(`Loading external configuration from: ${externalConfigPath}`, cloneContext); let externalConfig = (0, fs_1.readFileSync)(externalConfigPath, 'utf-8'); externalConfig = JSON.parse(externalConfig); config = merge_1.default.recursive(config, externalConfig); } config.forceStopMarketplaceAppsPrompt = yes; config.skipAudit = cloneCommandFlags['skip-audit']; cli_utilities_1.log.debug('Clone configuration prepared', Object.assign(Object.assign({}, cloneContext), { cloneType: config.cloneType, skipAudit: config.skipAudit, forceStopMarketplaceAppsPrompt: config.forceStopMarketplaceAppsPrompt })); if (cloneType) { config.cloneType = cloneType; } if (stackName) { config.stackName = stackName; } if (sourceStackBranch) { config.sourceStackBranch = sourceStackBranch; } if (sourceStackBranchAlias) { config.sourceStackBranchAlias = sourceStackBranchAlias; } if (targetStackBranch) { config.targetStackBranch = targetStackBranch; } if (targetStackBranchAlias) { config.targetStackBranchAlias = targetStackBranchAlias; } if (sourceStackApiKey) { config.source_stack = sourceStackApiKey; } if (destinationStackApiKey) { config.target_stack = destinationStackApiKey; } if (sourceManagementTokenAlias && (listOfTokens === null || listOfTokens === void 0 ? void 0 : listOfTokens[sourceManagementTokenAlias])) { config.source_alias = sourceManagementTokenAlias; config.source_stack = listOfTokens[sourceManagementTokenAlias].apiKey; cli_utilities_1.log.debug(`Using source token alias: ${sourceManagementTokenAlias}`, cloneContext); } else if (sourceManagementTokenAlias) { cli_utilities_1.log.warn(`Provided source token alias (${sourceManagementTokenAlias}) not found in your config.!`, cloneContext); } if (destinationManagementTokenAlias && (listOfTokens === null || listOfTokens === void 0 ? void 0 : listOfTokens[destinationManagementTokenAlias])) { config.destination_alias = destinationManagementTokenAlias; config.target_stack = listOfTokens[destinationManagementTokenAlias].apiKey; cli_utilities_1.log.debug(`Using destination token alias: ${destinationManagementTokenAlias}`, cloneContext); } else if (destinationManagementTokenAlias) { cli_utilities_1.log.warn(`Provided destination token alias (${destinationManagementTokenAlias}) not found in your config.!`, cloneContext); } if (importWebhookStatus) { config.importWebhookStatus = importWebhookStatus; } cli_utilities_1.log.debug('Management API client initialized successfully', cloneContext); cli_utilities_1.log.debug(`Content directory path: ${pathdir}`, cloneContext); await this.removeContentDirIfNotEmptyBeforeClone(pathdir, cloneContext); // NOTE remove if folder not empty before clone this.registerCleanupOnInterrupt(pathdir, cloneContext); config.auth_token = cli_utilities_1.configHandler.get('authtoken'); config.host = this.cmaHost; config.cdn = this.cdaHost; config.pathDir = pathdir; config.cloneContext = cloneContext; cli_utilities_1.log.debug('Clone configuration finalized', cloneContext); const cloneHandler = new clone_handler_1.CloneHandler(config); const managementAPIClient = await (0, cli_utilities_1.managementSDKClient)(config); cloneHandler.setClient(managementAPIClient); cli_utilities_1.log.debug('Starting clone operation', cloneContext); cloneHandler.execute().catch((error) => { (0, cli_utilities_1.handleAndLogError)(error, cloneContext); }); }; if (sourceManagementTokenAlias && destinationManagementTokenAlias) { if (sourceStackBranch || targetStackBranch) { if ((0, cli_utilities_1.isAuthenticated)()) { handleClone(); } else { cli_utilities_1.log.error('Log in to execute this command,csdx auth:login', this.createCloneContext('unknown')); this.exit(1); } } else { handleClone(); } } else if ((0, cli_utilities_1.isAuthenticated)()) { handleClone(); } else { cli_utilities_1.log.error('Please login to execute this command, csdx auth:login', this.createCloneContext('unknown')); this.exit(1); } } catch (error) { if (error) { await this.cleanUp(pathdir, null, this.createCloneContext('unknown')); cli_utilities_1.log.error('Stack clone command failed', Object.assign(Object.assign({}, this.createCloneContext('unknown')), { error: (error === null || error === void 0 ? void 0 : error.message) || error })); } } } async removeContentDirIfNotEmptyBeforeClone(dir, cloneContext) { try { cli_utilities_1.log.debug('Checking if content directory is empty', Object.assign(Object.assign({}, cloneContext), { dir })); const files = await fs_1.promises.readdir(dir); if (files.length) { cli_utilities_1.log.debug('Content directory is not empty, cleaning up', Object.assign(Object.assign({}, cloneContext), { dir })); await this.cleanUp(dir, null, cloneContext); } } catch (error) { const omit = ['ENOENT']; // NOTE add emittable error codes in the array if (!omit.includes(error.code)) { cli_utilities_1.log.error('Error checking content directory', Object.assign(Object.assign({}, cloneContext), { error: error === null || error === void 0 ? void 0 : error.message, code: error.code })); } } } async cleanUp(pathDir, message, cloneContext) { try { cli_utilities_1.log.debug('Starting cleanup', Object.assign(Object.assign({}, cloneContext), { pathDir })); await (0, rimraf_1.rimraf)(pathDir); if (message) { cli_utilities_1.log.info(message, cloneContext); } cli_utilities_1.log.debug('Cleanup completed', Object.assign(Object.assign({}, cloneContext), { pathDir })); } catch (err) { if (err) { cli_utilities_1.log.debug('Cleaning up', cloneContext); const skipCodeArr = ['ENOENT', 'EBUSY', 'EPERM', 'EMFILE', 'ENOTEMPTY']; if (skipCodeArr.includes(err.code)) { cli_utilities_1.log.debug('Cleanup error code is in skip list, exiting', Object.assign(Object.assign({}, cloneContext), { code: err === null || err === void 0 ? void 0 : err.code })); process.exit(); } } } } registerCleanupOnInterrupt(pathDir, cloneContext) { const interrupt = ['SIGINT', 'SIGQUIT', 'SIGTERM']; const exceptions = ['unhandledRejection', 'uncaughtException']; const cleanUp = async (exitOrError) => { if (exitOrError) { cli_utilities_1.log.debug('Cleaning up on interrupt', cloneContext); await this.cleanUp(pathDir, null, cloneContext); cli_utilities_1.log.info('Cleanup done', cloneContext); if (exitOrError instanceof Promise) { exitOrError.catch((error) => { cli_utilities_1.log.error('Error during cleanup', Object.assign(Object.assign({}, cloneContext), { error: (error && (error === null || error === void 0 ? void 0 : error.message)) || '' })); }); } else if (exitOrError.message) { cli_utilities_1.log.error('Cleanup error', Object.assign(Object.assign({}, cloneContext), { error: exitOrError === null || exitOrError === void 0 ? void 0 : exitOrError.message })); } else if (exitOrError.errorMessage) { cli_utilities_1.log.error('Cleanup error', Object.assign(Object.assign({}, cloneContext), { error: exitOrError === null || exitOrError === void 0 ? void 0 : exitOrError.errorMessage })); } if (exitOrError === true) process.exit(); } }; exceptions.forEach((event) => process.on(event, cleanUp)); interrupt.forEach((signal) => process.on(signal, () => cleanUp(true))); } } exports.default = StackCloneCommand; StackCloneCommand.description = `Clone data (structure/content or both) of a stack into another stack Use this plugin to automate the process of cloning a stack in few steps. `; StackCloneCommand.examples = [ 'csdx cm:stacks:clone', 'csdx cm:stacks:clone --source-branch <source-branch-name> --target-branch <target-branch-name> --yes', 'csdx cm:stacks:clone --source-stack-api-key <apiKey> --destination-stack-api-key <apiKey>', 'csdx cm:stacks:clone --source-management-token-alias <management token alias> --destination-management-token-alias <management token alias>', 'csdx cm:stacks:clone --source-branch --target-branch --source-management-token-alias <management token alias> --destination-management-token-alias <management token alias>', 'csdx cm:stacks:clone --source-branch --target-branch --source-management-token-alias <management token alias> --destination-management-token-alias <management token alias> --type <value a or b>', ]; StackCloneCommand.aliases = ['cm:stack-clone']; StackCloneCommand.flags = { 'source-branch': cli_utilities_1.flags.string({ required: false, multiple: false, description: 'Branch of the source stack.', exclusive: ['source-branch-alias'], }), 'source-branch-alias': cli_utilities_1.flags.string({ required: false, multiple: false, description: 'Alias of Branch of the source stack.', exclusive: ['source-branch'], }), 'target-branch': cli_utilities_1.flags.string({ required: false, multiple: false, description: 'Branch of the target stack.', exclusive: ['target-branch-alias'], }), 'target-branch-alias': cli_utilities_1.flags.string({ required: false, multiple: false, description: 'Alias of Branch of the target stack.', exclusive: ['target-branch'], }), 'source-management-token-alias': cli_utilities_1.flags.string({ required: false, multiple: false, description: 'Source management token alias.', }), 'destination-management-token-alias': cli_utilities_1.flags.string({ required: false, multiple: false, description: 'Destination management token alias.', }), 'stack-name': cli_utilities_1.flags.string({ char: 'n', required: false, multiple: false, description: 'Provide a name for the new stack to store the cloned content.', }), type: cli_utilities_1.flags.string({ required: false, multiple: false, options: ['a', 'b'], description: ` Type of data to clone. You can select option a or b. a) Structure (all modules except entries & assets). b) Structure with content (all modules including entries & assets). `, }), 'source-stack-api-key': cli_utilities_1.flags.string({ description: 'Source stack API key', }), 'destination-stack-api-key': cli_utilities_1.flags.string({ description: 'Destination stack API key', }), 'import-webhook-status': cli_utilities_1.flags.string({ description: '[default: disable] (optional) The status of the import webhook. <options: disable|current>', options: ['disable', 'current'], required: false, default: 'disable', }), yes: cli_utilities_1.flags.boolean({ char: 'y', required: false, description: 'Force override all Marketplace prompts.', }), 'skip-audit': cli_utilities_1.flags.boolean({ description: ' (optional) Skips the audit fix that occurs during an import operation.', }), config: cli_utilities_1.flags.string({ char: 'c', required: false, description: 'Path for the external configuration', }), }; StackCloneCommand.usage = 'cm:stacks:clone [--source-branch <value>] [--target-branch <value>] [--source-management-token-alias <value>] [--destination-management-token-alias <value>] [-n <value>] [--type a|b] [--source-stack-api-key <value>] [--destination-stack-api-key <value>] [--import-webhook-status disable|current]';