@contentstack/cli-cm-clone
Version:
Contentstack stack clone plugin
316 lines (315 loc) • 17.2 kB
JavaScript
;
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]';